Compare commits
	
		
			12 Commits
		
	
	
		
			breaking-c
			...
			middleware
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 38b4ccf2bc | ||
|   | 8b970dd490 | ||
|   | c9be17e8da | ||
|   | 19f642b364 | ||
|   | c4c39cb082 | ||
|   | c7bac72137 | ||
|   | beb5c62767 | ||
|   | 09b59d34fe | ||
|   | 78bc475bb1 | ||
|   | b59131504b | ||
|   | 782e0881e5 | ||
|   | c72cbe4326 | 
| @@ -9,7 +9,6 @@ omit = | |||||||
|     sanic/simple.py |     sanic/simple.py | ||||||
|     sanic/utils.py |     sanic/utils.py | ||||||
|     sanic/cli |     sanic/cli | ||||||
|     sanic/pages |  | ||||||
|  |  | ||||||
| [html] | [html] | ||||||
| directory = coverage | directory = coverage | ||||||
|   | |||||||
							
								
								
									
										66
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										66
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,66 +0,0 @@ | |||||||
| name: 🐞 Bug report |  | ||||||
| description: Create a report to help us improve |  | ||||||
| labels: ["bug", "triage"] |  | ||||||
| body: |  | ||||||
|   - type: checkboxes |  | ||||||
|     id: existing |  | ||||||
|     attributes: |  | ||||||
|       label: Is there an existing issue for this? |  | ||||||
|       description: Please search to see if an issue already exists for the bug you encountered. |  | ||||||
|       options: |  | ||||||
|       - label: I have searched the existing issues |  | ||||||
|         required: true |  | ||||||
|   - type: textarea |  | ||||||
|     id: description |  | ||||||
|     attributes: |  | ||||||
|       label: Describe the bug |  | ||||||
|       description: A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks using markdown code-block syntax to make it easier to read. |  | ||||||
|     validations: |  | ||||||
|       required: true |  | ||||||
|   - type: textarea |  | ||||||
|     id: code |  | ||||||
|     attributes: |  | ||||||
|       label: Code snippet |  | ||||||
|       description: Relevant source code, make sure to remove what is not necessary. |  | ||||||
|     validations: |  | ||||||
|       required: false |  | ||||||
|   - type: textarea |  | ||||||
|     id: expected |  | ||||||
|     attributes: |  | ||||||
|       label: Expected Behavior |  | ||||||
|       description: A concise description of what you expected to happen. |  | ||||||
|     validations: |  | ||||||
|       required: false |  | ||||||
|   - type: dropdown |  | ||||||
|     id: running |  | ||||||
|     attributes: |  | ||||||
|       label: How do you run Sanic? |  | ||||||
|       options: |  | ||||||
|         - Sanic CLI |  | ||||||
|         - As a module |  | ||||||
|         - As a script (`app.run` or `Sanic.serve`) |  | ||||||
|         - ASGI |  | ||||||
|     validations: |  | ||||||
|       required: true |  | ||||||
|   - type: input |  | ||||||
|     id: os |  | ||||||
|     attributes: |  | ||||||
|       label: Operating System |  | ||||||
|       description: What OS? |  | ||||||
|     validations: |  | ||||||
|       required: true |  | ||||||
|   - type: input |  | ||||||
|     id: version |  | ||||||
|     attributes: |  | ||||||
|       label: Sanic Version |  | ||||||
|       description: Check startup logs or try `sanic --version` |  | ||||||
|     validations: |  | ||||||
|       required: true |  | ||||||
|   - type: textarea |  | ||||||
|     id: additional |  | ||||||
|     attributes: |  | ||||||
|       label: Additional context |  | ||||||
|       description: Add any other context about the problem here. |  | ||||||
|     validations: |  | ||||||
|       required: false |  | ||||||
|  |  | ||||||
							
								
								
									
										27
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | --- | ||||||
|  | name: Bug report | ||||||
|  | about: Create a report to help us improve | ||||||
|  | labels: ["bug"] | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | **Describe the bug** | ||||||
|  | <!-- A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks. --> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **Code snippet** | ||||||
|  | <!-- Relevant source code, make sure to remove what is not necessary. --> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **Expected behavior** | ||||||
|  | <!-- A clear and concise description of what you expected to happen. --> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **Environment (please complete the following information):** | ||||||
|  | <!-- Please provide the information below. Instead, you can copy and paste the message that Sanic shows on startup. If you do, please remember to format it with ``` --> | ||||||
|  |  - OS:  | ||||||
|  |  - Sanic Version: | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **Additional context** | ||||||
|  | <!-- Add any other context about the problem here. --> | ||||||
							
								
								
									
										2
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | |||||||
| blank_issues_enabled: false | blank_issues_enabled: true | ||||||
| contact_links: | contact_links: | ||||||
|   - name: Questions and Help |   - name: Questions and Help | ||||||
|     url: https://community.sanicframework.org/c/questions-and-help |     url: https://community.sanicframework.org/c/questions-and-help | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								.github/ISSUE_TEMPLATE/feature-request.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										34
									
								
								.github/ISSUE_TEMPLATE/feature-request.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,34 +0,0 @@ | |||||||
| name: 🌟 Feature request |  | ||||||
| description: Suggest an enhancement for Sanic |  | ||||||
| labels: ["feature request"] |  | ||||||
| body: |  | ||||||
|   - type: checkboxes |  | ||||||
|     id: existing |  | ||||||
|     attributes: |  | ||||||
|       label: Is there an existing issue for this? |  | ||||||
|       description: Please search to see if an issue already exists for the enhancement you are proposing. |  | ||||||
|       options: |  | ||||||
|       - label: I have searched the existing issues |  | ||||||
|         required: true |  | ||||||
|   - type: textarea |  | ||||||
|     id: description |  | ||||||
|     attributes: |  | ||||||
|       label: Is your feature request related to a problem? Please describe. |  | ||||||
|       description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] |  | ||||||
|     validations: |  | ||||||
|       required: false |  | ||||||
|   - type: textarea |  | ||||||
|     id: code |  | ||||||
|     attributes: |  | ||||||
|       label: Describe the solution you'd like |  | ||||||
|       description: A clear and concise description of what you want to happen. |  | ||||||
|     validations: |  | ||||||
|       required: true |  | ||||||
|   - type: textarea |  | ||||||
|     id: additional |  | ||||||
|     attributes: |  | ||||||
|       label: Additional context |  | ||||||
|       description: Add any other context about the problem here. |  | ||||||
|     validations: |  | ||||||
|       required: false |  | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | --- | ||||||
|  | name: Feature request | ||||||
|  | about: Suggest an idea for Sanic | ||||||
|  | labels: ["feature request"] | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | **Is your feature request related to a problem? Please describe.** | ||||||
|  | <!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] --> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **Describe the solution you'd like** | ||||||
|  | <!-- A clear and concise description of what you want to happen. --> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | **Additional context** | ||||||
|  | <!-- Add any other context or sample code about the feature request here. --> | ||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							| @@ -20,7 +20,6 @@ jobs: | |||||||
|           - { python-version: 3.8, tox-env: security} |           - { python-version: 3.8, tox-env: security} | ||||||
|           - { python-version: 3.9, tox-env: security} |           - { python-version: 3.9, tox-env: security} | ||||||
|           - { python-version: "3.10", tox-env: security} |           - { python-version: "3.10", tox-env: security} | ||||||
|           - { python-version: "3.11", tox-env: security} |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
								
							| @@ -14,7 +14,7 @@ jobs: | |||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         config: |         config: | ||||||
|           - {python-version: "3.10", tox-env: "docs"} |           - {python-version: "3.8", tox-env: "docs"} | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
								
							| @@ -16,7 +16,7 @@ jobs: | |||||||
|       matrix: |       matrix: | ||||||
|         os: [ubuntu-latest] |         os: [ubuntu-latest] | ||||||
|         config: |         config: | ||||||
|           - { python-version: "3.10", tox-env: lint} |           - { python-version: 3.8, tox-env: lint} | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2 | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								.github/workflows/pr-python311.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										47
									
								
								.github/workflows/pr-python311.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,47 +0,0 @@ | |||||||
| name: Python 3.11 Tests |  | ||||||
| on: |  | ||||||
|   pull_request: |  | ||||||
|     branches: |  | ||||||
|       - main |  | ||||||
|       - "*LTS" |  | ||||||
|     types: [opened, synchronize, reopened, ready_for_review] |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   testPy311: |  | ||||||
|     if: github.event.pull_request.draft == false |  | ||||||
|     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} |  | ||||||
|     runs-on: ${{ matrix.os }} |  | ||||||
|     strategy: |  | ||||||
|       fail-fast: false |  | ||||||
|       matrix: |  | ||||||
|         # os: [ubuntu-latest, macos-latest] |  | ||||||
|         os: [ubuntu-latest] |  | ||||||
|         config: |  | ||||||
|           - { |  | ||||||
|               python-version: "3.11", |  | ||||||
|               tox-env: py311, |  | ||||||
|               ignore-error-flake: "false", |  | ||||||
|               command-timeout: "0", |  | ||||||
|             } |  | ||||||
|           - { |  | ||||||
|               python-version: "3.11", |  | ||||||
|               tox-env: py311-no-ext, |  | ||||||
|               ignore-error-flake: "true", |  | ||||||
|               command-timeout: "600000", |  | ||||||
|             } |  | ||||||
|     steps: |  | ||||||
|       - name: Checkout the Repository |  | ||||||
|         uses: actions/checkout@v2 |  | ||||||
|         id: checkout-branch |  | ||||||
|  |  | ||||||
|       - name: Run Unit Tests |  | ||||||
|         uses: harshanarayana/custom-actions@main |  | ||||||
|         with: |  | ||||||
|           python-version: ${{ matrix.config.python-version }} |  | ||||||
|           test-infra-tool: tox |  | ||||||
|           test-infra-version: latest |  | ||||||
|           action: tests |  | ||||||
|           test-additional-args: "-e=${{ matrix.config.tox-env }},-vv=''" |  | ||||||
|           experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}" |  | ||||||
|           command-timeout: "${{ matrix.config.command-timeout }}" |  | ||||||
|           test-failure-retry: "3" |  | ||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							| @@ -20,7 +20,6 @@ jobs: | |||||||
|           - { python-version: 3.8, tox-env: type-checking} |           - { python-version: 3.8, tox-env: type-checking} | ||||||
|           - { python-version: 3.9, tox-env: type-checking} |           - { python-version: 3.9, tox-env: type-checking} | ||||||
|           - { python-version: "3.10", tox-env: type-checking} |           - { python-version: "3.10", tox-env: type-checking} | ||||||
|           - { python-version: "3.11", tox-env: type-checking} |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2 | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
								
							| @@ -19,7 +19,6 @@ jobs: | |||||||
|           - { python-version: 3.8, tox-env: py38-no-ext } |           - { python-version: 3.8, tox-env: py38-no-ext } | ||||||
|           - { python-version: 3.9, tox-env: py39-no-ext } |           - { python-version: 3.9, tox-env: py39-no-ext } | ||||||
|           - { python-version: "3.10", tox-env: py310-no-ext } |           - { python-version: "3.10", tox-env: py310-no-ext } | ||||||
|           - { python-version: "3.11", tox-env: py310-no-ext } |  | ||||||
|           - { python-version: pypy-3.7, tox-env: pypy37-no-ext } |           - { python-version: pypy-3.7, tox-env: pypy37-no-ext } | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/publish-images.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/publish-images.yml
									
									
									
									
										vendored
									
									
								
							| @@ -14,7 +14,7 @@ jobs: | |||||||
|     strategy: |     strategy: | ||||||
|       fail-fast: true |       fail-fast: true | ||||||
|       matrix: |       matrix: | ||||||
|         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] |         python-version: ["3.7", "3.8", "3.9", "3.10"] | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout repository |       - name: Checkout repository | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/publish-package.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/publish-package.yml
									
									
									
									
										vendored
									
									
								
							| @@ -11,7 +11,7 @@ jobs: | |||||||
|     strategy: |     strategy: | ||||||
|       fail-fast: true |       fail-fast: true | ||||||
|       matrix: |       matrix: | ||||||
|         python-version: ["3.10"] |         python-version: ["3.8"] | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout Repository |       - name: Checkout Repository | ||||||
|   | |||||||
| @@ -313,8 +313,8 @@ Version 21.3.0 | |||||||
|     `#2074 <https://github.com/sanic-org/sanic/pull/2074>`_ |     `#2074 <https://github.com/sanic-org/sanic/pull/2074>`_ | ||||||
|     Performance adjustments in ``handle_request_`` |     Performance adjustments in ``handle_request_`` | ||||||
|  |  | ||||||
| Version 20.12.3 | Version 20.12.3 🔷 | ||||||
| --------------- | ------------------ | ||||||
|  |  | ||||||
| `Current LTS version` | `Current LTS version` | ||||||
|  |  | ||||||
| @@ -350,8 +350,8 @@ Version 19.12.5 | |||||||
|     `#2027 <https://github.com/sanic-org/sanic/pull/2027>`_ |     `#2027 <https://github.com/sanic-org/sanic/pull/2027>`_ | ||||||
|     Remove old chardet requirement, add in hard multidict requirement |     Remove old chardet requirement, add in hard multidict requirement | ||||||
|  |  | ||||||
| Version 20.12.0 | Version 20.12.0 🔹 | ||||||
| --------------- | ----------------- | ||||||
|  |  | ||||||
| **Features** | **Features** | ||||||
|  |  | ||||||
|   | |||||||
| @@ -102,6 +102,9 @@ Installation | |||||||
|   If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to |   If you are running on a clean install of Fedora 28 or above, please make sure you have the ``redhat-rpm-config`` package installed in case if you want to | ||||||
|   use ``sanic`` with ``ujson`` dependency. |   use ``sanic`` with ``ujson`` dependency. | ||||||
|  |  | ||||||
|  | .. note:: | ||||||
|  |  | ||||||
|  |   Windows support is currently "experimental" and on a best-effort basis. Multiple workers are also not currently supported on Windows (see `Issue #1517 <https://github.com/sanic-org/sanic/issues/1517>`_), but setting ``workers=1`` should launch the server successfully. | ||||||
|  |  | ||||||
| Hello World Example | Hello World Example | ||||||
| ------------------- | ------------------- | ||||||
|   | |||||||
| @@ -7,15 +7,13 @@ Sanic releases long term support release once a year in December. LTS releases r | |||||||
|  |  | ||||||
| | Version | LTS           | Supported               | | | Version | LTS           | Supported               | | ||||||
| | ------- | ------------- | ----------------------- | | | ------- | ------------- | ----------------------- | | ||||||
| | 22.12   | until 2024-12 | :white_check_mark:      | | | 22.6    |               | :white_check_mark:      | | ||||||
| | 22.9    |               | :x:                     | |  | ||||||
| | 22.6    |               | :x:                     | |  | ||||||
| | 22.3    |               | :x:                     | | | 22.3    |               | :x:                     | | ||||||
| | 21.12   | until 2023-12 | :ballot_box_with_check: | | | 21.12   | until 2023-12 | :white_check_mark:      | | ||||||
| | 21.9    |               | :x:                     | | | 21.9    |               | :x:                     | | ||||||
| | 21.6    |               | :x:                     | | | 21.6    |               | :x:                     | | ||||||
| | 21.3    |               | :x:                     | | | 21.3    |               | :x:                     | | ||||||
| | 20.12   |               | :x:                     | | | 20.12   | until 2022-12 | :ballot_box_with_check: | | ||||||
| | 20.9    |               | :x:                     | | | 20.9    |               | :x:                     | | ||||||
| | 20.6    |               | :x:                     | | | 20.6    |               | :x:                     | | ||||||
| | 20.3    |               | :x:                     | | | 20.3    |               | :x:                     | | ||||||
|   | |||||||
| @@ -17,8 +17,7 @@ ignore: | |||||||
|   - "sanic/compat.py" |   - "sanic/compat.py" | ||||||
|   - "sanic/simple.py" |   - "sanic/simple.py" | ||||||
|   - "sanic/utils.py" |   - "sanic/utils.py" | ||||||
|   - "sanic/cli/" |   - "sanic/cli" | ||||||
|   - "sanic/pages/" |  | ||||||
|   - ".github/" |   - ".github/" | ||||||
|   - "changelogs/" |   - "changelogs/" | ||||||
|   - "docker/" |   - "docker/" | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								docs/_static/custom.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								docs/_static/custom.css
									
									
									
									
										vendored
									
									
								
							| @@ -2,12 +2,3 @@ | |||||||
| .wy-nav-top { | .wy-nav-top { | ||||||
|   background: #444444; |   background: #444444; | ||||||
| } | } | ||||||
|  |  | ||||||
| #changelog section { |  | ||||||
|   padding-left: 3rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #changelog section h2, |  | ||||||
| #changelog section h3 { |  | ||||||
|   margin-left: -3rem; |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -1,8 +1,6 @@ | |||||||
| 📜 Changelog | 📜 Changelog | ||||||
| ============ | ============ | ||||||
|  |  | ||||||
| .. mdinclude:: ./releases/22/22.12.md |  | ||||||
| .. mdinclude:: ./releases/22/22.9.md |  | ||||||
| .. mdinclude:: ./releases/22/22.6.md | .. mdinclude:: ./releases/22/22.6.md | ||||||
| .. mdinclude:: ./releases/22/22.3.md | .. mdinclude:: ./releases/22/22.3.md | ||||||
| .. mdinclude:: ./releases/21/21.12.md | .. mdinclude:: ./releases/21/21.12.md | ||||||
|   | |||||||
| @@ -1,55 +0,0 @@ | |||||||
| ## Version 22.12.0 🔶 |  | ||||||
|  |  | ||||||
| _Current version_ |  | ||||||
|  |  | ||||||
| ### Features |  | ||||||
|  |  | ||||||
| - [#2569](https://github.com/sanic-org/sanic/pull/2569) Add `JSONResponse` class with some convenient methods when updating a response object |  | ||||||
| - [#2598](https://github.com/sanic-org/sanic/pull/2598) Change `uvloop` requirement to `>=0.15.0` |  | ||||||
| - [#2609](https://github.com/sanic-org/sanic/pull/2609) Add compatibility with `websockets` v11.0 |  | ||||||
| - [#2610](https://github.com/sanic-org/sanic/pull/2610) Kill server early on worker error |  | ||||||
|     - Raise deadlock timeout to 30s |  | ||||||
| - [#2617](https://github.com/sanic-org/sanic/pull/2617) Scale number of running server workers |  | ||||||
| - [#2621](https://github.com/sanic-org/sanic/pull/2621) [#2634](https://github.com/sanic-org/sanic/pull/2634) Send `SIGKILL` on subsequent `ctrl+c` to force worker exit |  | ||||||
| - [#2622](https://github.com/sanic-org/sanic/pull/2622) Add API to restart all workers from the multiplexer |  | ||||||
| - [#2624](https://github.com/sanic-org/sanic/pull/2624) Default to `spawn` for all subprocesses unless specifically set: |  | ||||||
|     ```python |  | ||||||
|     from sanic import Sanic |  | ||||||
|      |  | ||||||
|     Sanic.start_method = "fork" |  | ||||||
|     ``` |  | ||||||
| - [#2625](https://github.com/sanic-org/sanic/pull/2625) Filename normalisation of form-data/multipart file uploads |  | ||||||
| - [#2626](https://github.com/sanic-org/sanic/pull/2626) Move to HTTP Inspector: |  | ||||||
|     - Remote access to inspect running Sanic instances |  | ||||||
|     - TLS support for encrypted calls to Inspector |  | ||||||
|     - Authentication to Inspector with API key |  | ||||||
|     - Ability to extend Inspector with custom commands |  | ||||||
| - [#2632](https://github.com/sanic-org/sanic/pull/2632) Control order of restart operations |  | ||||||
| - [#2633](https://github.com/sanic-org/sanic/pull/2633) Move reload interval to class variable |  | ||||||
| - [#2636](https://github.com/sanic-org/sanic/pull/2636) Add `priority` to `register_middleware` method |  | ||||||
| - [#2639](https://github.com/sanic-org/sanic/pull/2639) Add `unquote` to `add_route` method |  | ||||||
| - [#2640](https://github.com/sanic-org/sanic/pull/2640) ASGI websockets to receive `text` or `bytes` |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bugfixes |  | ||||||
|  |  | ||||||
| - [#2607](https://github.com/sanic-org/sanic/pull/2607) Force socket shutdown before close to allow rebinding |  | ||||||
| - [#2590](https://github.com/sanic-org/sanic/pull/2590) Use actual `StrEnum` in Python 3.11+ |  | ||||||
| - [#2615](https://github.com/sanic-org/sanic/pull/2615) Ensure middleware executes only once per request timeout |  | ||||||
| - [#2627](https://github.com/sanic-org/sanic/pull/2627) Crash ASGI application on lifespan failure |  | ||||||
| - [#2635](https://github.com/sanic-org/sanic/pull/2635) Resolve error with low-level server creation on Windows |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Deprecations and Removals |  | ||||||
|  |  | ||||||
| - [#2608](https://github.com/sanic-org/sanic/pull/2608) [#2630](https://github.com/sanic-org/sanic/pull/2630) Signal conditions and triggers saved on `signal.extra`  |  | ||||||
| - [#2626](https://github.com/sanic-org/sanic/pull/2626) Move to HTTP Inspector |  | ||||||
|     - 🚨 *BREAKING CHANGE*: Moves the Inspector to a Sanic app from a simple TCP socket with a custom protocol |  | ||||||
|     - *DEPRECATE*: The `--inspect*` commands have been deprecated in favor of `inspect ...` commands |  | ||||||
| - [#2628](https://github.com/sanic-org/sanic/pull/2628) Replace deprecated `distutils.strtobool` |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Developer infrastructure |  | ||||||
|  |  | ||||||
| - [#2612](https://github.com/sanic-org/sanic/pull/2612) Add CI testing for Python 3.11 |  | ||||||
|  |  | ||||||
| @@ -1,17 +1,6 @@ | |||||||
| ## Version 22.6.2 | ## Version 22.6.0 🔶 | ||||||
|  |  | ||||||
| ### Bugfixes | _Current version_ | ||||||
|  |  | ||||||
| - [#2522](https://github.com/sanic-org/sanic/pull/2522) Always show server location in ASGI |  | ||||||
|  |  | ||||||
| ## Version 22.6.1 |  | ||||||
|  |  | ||||||
| ### Bugfixes |  | ||||||
|  |  | ||||||
| - [#2477](https://github.com/sanic-org/sanic/pull/2477) Sanic static directory fails when folder name ends with ".." |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ## Version 22.6.0 |  | ||||||
|  |  | ||||||
| ### Features | ### Features | ||||||
| - [#2378](https://github.com/sanic-org/sanic/pull/2378) Introduce HTTP/3 and autogeneration of TLS certificates in `DEBUG` mode | - [#2378](https://github.com/sanic-org/sanic/pull/2378) Introduce HTTP/3 and autogeneration of TLS certificates in `DEBUG` mode | ||||||
|   | |||||||
| @@ -1,74 +0,0 @@ | |||||||
| ## Version 22.9.1 |  | ||||||
|  |  | ||||||
| ### Features |  | ||||||
|  |  | ||||||
| - [#2585](https://github.com/sanic-org/sanic/pull/2585) Improved error message when no applications have been registered |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Bugfixes |  | ||||||
|  |  | ||||||
| - [#2578](https://github.com/sanic-org/sanic/pull/2578) Add certificate loader for in process certificate creation |  | ||||||
| - [#2591](https://github.com/sanic-org/sanic/pull/2591) Do not use sentinel identity for `spawn` compatibility |  | ||||||
| - [#2592](https://github.com/sanic-org/sanic/pull/2592) Fix properties in nested blueprint groups |  | ||||||
| - [#2595](https://github.com/sanic-org/sanic/pull/2595) Introduce sleep interval on new worker reloader |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Deprecations and Removals |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Developer infrastructure |  | ||||||
|  |  | ||||||
| - [#2588](https://github.com/sanic-org/sanic/pull/2588) Markdown templates on issue forms |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Improved Documentation |  | ||||||
|  |  | ||||||
| - [#2556](https://github.com/sanic-org/sanic/pull/2556) v22.9 documentation |  | ||||||
| - [#2582](https://github.com/sanic-org/sanic/pull/2582) Cleanup documentation on Windows support |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ## Version 22.9.0 |  | ||||||
|  |  | ||||||
| ### Features |  | ||||||
|  |  | ||||||
| - [#2445](https://github.com/sanic-org/sanic/pull/2445) Add custom loads function  |  | ||||||
| - [#2490](https://github.com/sanic-org/sanic/pull/2490) Make `WebsocketImplProtocol` async iterable |  | ||||||
| - [#2499](https://github.com/sanic-org/sanic/pull/2499) Sanic Server WorkerManager refactor |  | ||||||
| - [#2506](https://github.com/sanic-org/sanic/pull/2506) Use `pathlib` for path resolution (for static file serving) |  | ||||||
| - [#2508](https://github.com/sanic-org/sanic/pull/2508) Use `path.parts` instead of `match` (for static file serving) |  | ||||||
| - [#2513](https://github.com/sanic-org/sanic/pull/2513) Better request cancel handling |  | ||||||
| - [#2516](https://github.com/sanic-org/sanic/pull/2516) Add request properties for HTTP method info: |  | ||||||
|     - `request.is_safe` |  | ||||||
|     - `request.is_idempotent` |  | ||||||
|     - `request.is_cacheable` |  | ||||||
|     - *See* [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) *for more information about when these apply* |  | ||||||
| - [#2522](https://github.com/sanic-org/sanic/pull/2522) Always show server location in ASGI |  | ||||||
| - [#2526](https://github.com/sanic-org/sanic/pull/2526) Cache control support for static files for returning 304 when appropriate |  | ||||||
| - [#2533](https://github.com/sanic-org/sanic/pull/2533) Refactor `_static_request_handler` |  | ||||||
| - [#2540](https://github.com/sanic-org/sanic/pull/2540) Add signals before and after handler execution |  | ||||||
|     - `http.handler.before` |  | ||||||
|     - `http.handler.after` |  | ||||||
| - [#2542](https://github.com/sanic-org/sanic/pull/2542) Add *[redacted]* to CLI :) |  | ||||||
| - [#2546](https://github.com/sanic-org/sanic/pull/2546) Add deprecation warning filter |  | ||||||
| - [#2550](https://github.com/sanic-org/sanic/pull/2550) Middleware priority and performance enhancements |  | ||||||
|  |  | ||||||
| ### Bugfixes |  | ||||||
|  |  | ||||||
| - [#2495](https://github.com/sanic-org/sanic/pull/2495) Prevent directory traversion with static files |  | ||||||
| - [#2515](https://github.com/sanic-org/sanic/pull/2515) Do not apply double slash to paths in certain static dirs in Blueprints |  | ||||||
|  |  | ||||||
| ### Deprecations and Removals |  | ||||||
|  |  | ||||||
| - [#2525](https://github.com/sanic-org/sanic/pull/2525) Warn on duplicate route names, will be prevented outright in v23.3 |  | ||||||
| - [#2537](https://github.com/sanic-org/sanic/pull/2537) Raise warning and deprecation notice on duplicate exceptions, will be prevented outright in v23.3 |  | ||||||
|  |  | ||||||
| ### Developer infrastructure |  | ||||||
|  |  | ||||||
| - [#2504](https://github.com/sanic-org/sanic/pull/2504) Cleanup test suite |  | ||||||
| - [#2505](https://github.com/sanic-org/sanic/pull/2505) Replace Unsupported Python Version Number from the Contributing Doc |  | ||||||
| - [#2530](https://github.com/sanic-org/sanic/pull/2530) Do not include tests folder in installed package resolver |  | ||||||
|  |  | ||||||
| ### Improved Documentation |  | ||||||
|  |  | ||||||
| - [#2502](https://github.com/sanic-org/sanic/pull/2502) Fix a few typos |  | ||||||
| - [#2517](https://github.com/sanic-org/sanic/pull/2517) [#2536](https://github.com/sanic-org/sanic/pull/2536) Add some type hints |  | ||||||
| @@ -22,8 +22,5 @@ module = [ | |||||||
|     "httptools.*", |     "httptools.*", | ||||||
|     "trustme.*", |     "trustme.*", | ||||||
|     "sanic_routing.*", |     "sanic_routing.*", | ||||||
|     "aioquic.*", |  | ||||||
|     "html5tagger.*", |  | ||||||
|     "tracerite.*", |  | ||||||
| ] | ] | ||||||
| ignore_missing_imports = true | ignore_missing_imports = true | ||||||
|   | |||||||
| @@ -1,10 +1 @@ | |||||||
| __version__ = "23.3.0" | __version__ = "22.9.1" | ||||||
| __compatibility__ = "22.12" |  | ||||||
|  |  | ||||||
| from inspect import currentframe, stack |  | ||||||
|  |  | ||||||
| for frame_info in stack(): |  | ||||||
|     if frame_info.frame is not currentframe(): |  | ||||||
|         value = frame_info.frame.f_globals.get("__SANIC_COMPATIBILITY__") |  | ||||||
|         if value: |  | ||||||
|             __compatibility__ = value |  | ||||||
|   | |||||||
							
								
								
									
										474
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										474
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -21,7 +21,6 @@ from functools import partial | |||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
| from os import environ | from os import environ | ||||||
| from socket import socket | from socket import socket | ||||||
| from traceback import format_exc |  | ||||||
| from types import SimpleNamespace | from types import SimpleNamespace | ||||||
| from typing import ( | from typing import ( | ||||||
|     TYPE_CHECKING, |     TYPE_CHECKING, | ||||||
| @@ -47,21 +46,16 @@ from sanic_routing.exceptions import FinalizationError, NotFound | |||||||
| from sanic_routing.route import Route | from sanic_routing.route import Route | ||||||
|  |  | ||||||
| from sanic.application.ext import setup_ext | from sanic.application.ext import setup_ext | ||||||
| from sanic.application.state import ApplicationState, ServerStage | from sanic.application.state import ApplicationState, Mode, ServerStage | ||||||
| from sanic.asgi import ASGIApp | from sanic.asgi import ASGIApp | ||||||
| from sanic.base.root import BaseSanic | from sanic.base.root import BaseSanic | ||||||
| from sanic.blueprint_group import BlueprintGroup | from sanic.blueprint_group import BlueprintGroup | ||||||
| from sanic.blueprints import Blueprint | from sanic.blueprints import Blueprint | ||||||
| from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support | from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support | ||||||
| from sanic.config import SANIC_PREFIX, Config | from sanic.config import SANIC_PREFIX, Config | ||||||
| from sanic.exceptions import ( | from sanic.exceptions import BadRequest, SanicException, URLBuildError | ||||||
|     BadRequest, |  | ||||||
|     SanicException, |  | ||||||
|     ServerError, |  | ||||||
|     URLBuildError, |  | ||||||
| ) |  | ||||||
| from sanic.handlers import ErrorHandler | from sanic.handlers import ErrorHandler | ||||||
| from sanic.helpers import Default, _default | from sanic.helpers import _default | ||||||
| from sanic.http import Stage | from sanic.http import Stage | ||||||
| from sanic.log import ( | from sanic.log import ( | ||||||
|     LOGGING_CONFIG_DEFAULTS, |     LOGGING_CONFIG_DEFAULTS, | ||||||
| @@ -69,10 +63,8 @@ from sanic.log import ( | |||||||
|     error_logger, |     error_logger, | ||||||
|     logger, |     logger, | ||||||
| ) | ) | ||||||
| from sanic.middleware import Middleware, MiddlewareLocation |  | ||||||
| from sanic.mixins.listeners import ListenerEvent | from sanic.mixins.listeners import ListenerEvent | ||||||
| from sanic.mixins.startup import StartupMixin | from sanic.mixins.startup import StartupMixin | ||||||
| from sanic.mixins.static import StaticHandleMixin |  | ||||||
| from sanic.models.futures import ( | from sanic.models.futures import ( | ||||||
|     FutureException, |     FutureException, | ||||||
|     FutureListener, |     FutureListener, | ||||||
| @@ -80,11 +72,12 @@ from sanic.models.futures import ( | |||||||
|     FutureRegistry, |     FutureRegistry, | ||||||
|     FutureRoute, |     FutureRoute, | ||||||
|     FutureSignal, |     FutureSignal, | ||||||
|  |     FutureStatic, | ||||||
| ) | ) | ||||||
| from sanic.models.handler_types import ListenerType, MiddlewareType | from sanic.models.handler_types import ListenerType, MiddlewareType | ||||||
| from sanic.models.handler_types import Sanic as SanicVar | from sanic.models.handler_types import Sanic as SanicVar | ||||||
| from sanic.request import Request | from sanic.request import Request | ||||||
| from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream | from sanic.response import BaseHTTPResponse | ||||||
| from sanic.router import Router | from sanic.router import Router | ||||||
| from sanic.server.websockets.impl import ConnectionClosed | from sanic.server.websockets.impl import ConnectionClosed | ||||||
| from sanic.signals import Signal, SignalRouter | from sanic.signals import Signal, SignalRouter | ||||||
| @@ -106,7 +99,7 @@ if OS_IS_WINDOWS:  # no cov | |||||||
|     enable_windows_color_support() |     enable_windows_color_support() | ||||||
|  |  | ||||||
|  |  | ||||||
| class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta): | ||||||
|     """ |     """ | ||||||
|     The main application instance |     The main application instance | ||||||
|     """ |     """ | ||||||
| @@ -141,7 +134,6 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|         "configure_logging", |         "configure_logging", | ||||||
|         "ctx", |         "ctx", | ||||||
|         "error_handler", |         "error_handler", | ||||||
|         "inspector_class", |  | ||||||
|         "go_fast", |         "go_fast", | ||||||
|         "listeners", |         "listeners", | ||||||
|         "multiplexer", |         "multiplexer", | ||||||
| @@ -160,11 +152,12 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     _app_registry: Dict[str, "Sanic"] = {} |     _app_registry: Dict[str, "Sanic"] = {} | ||||||
|  |     _uvloop_setting = None  # TODO: Remove in v22.6 | ||||||
|     test_mode = False |     test_mode = False | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         name: Optional[str] = None, |         name: str = None, | ||||||
|         config: Optional[Config] = None, |         config: Optional[Config] = None, | ||||||
|         ctx: Optional[Any] = None, |         ctx: Optional[Any] = None, | ||||||
|         router: Optional[Router] = None, |         router: Optional[Router] = None, | ||||||
| @@ -178,7 +171,6 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|         dumps: Optional[Callable[..., AnyStr]] = None, |         dumps: Optional[Callable[..., AnyStr]] = None, | ||||||
|         loads: Optional[Callable[..., Any]] = None, |         loads: Optional[Callable[..., Any]] = None, | ||||||
|         inspector: bool = False, |         inspector: bool = False, | ||||||
|         inspector_class: Optional[Type[Inspector]] = None, |  | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         super().__init__(name=name) |         super().__init__(name=name) | ||||||
|         # logging |         # logging | ||||||
| @@ -214,7 +206,6 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|         self.configure_logging: bool = configure_logging |         self.configure_logging: bool = configure_logging | ||||||
|         self.ctx: Any = ctx or SimpleNamespace() |         self.ctx: Any = ctx or SimpleNamespace() | ||||||
|         self.error_handler: ErrorHandler = error_handler or ErrorHandler() |         self.error_handler: ErrorHandler = error_handler or ErrorHandler() | ||||||
|         self.inspector_class: Type[Inspector] = inspector_class or Inspector |  | ||||||
|         self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list) |         self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list) | ||||||
|         self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {} |         self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {} | ||||||
|         self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {} |         self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {} | ||||||
| @@ -295,12 +286,8 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|         return listener |         return listener | ||||||
|  |  | ||||||
|     def register_middleware( |     def register_middleware( | ||||||
|         self, |         self, middleware: MiddlewareType, attach_to: str = "request" | ||||||
|         middleware: Union[MiddlewareType, Middleware], |     ) -> MiddlewareType: | ||||||
|         attach_to: str = "request", |  | ||||||
|         *, |  | ||||||
|         priority: Union[Default, int] = _default, |  | ||||||
|     ) -> Union[MiddlewareType, Middleware]: |  | ||||||
|         """ |         """ | ||||||
|         Register an application level middleware that will be attached |         Register an application level middleware that will be attached | ||||||
|         to all the API URLs registered under this application. |         to all the API URLs registered under this application. | ||||||
| @@ -316,37 +303,19 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|             **response** - Invoke before the response is returned back |             **response** - Invoke before the response is returned back | ||||||
|         :return: decorated method |         :return: decorated method | ||||||
|         """ |         """ | ||||||
|         retval = middleware |         if attach_to == "request": | ||||||
|         location = MiddlewareLocation[attach_to.upper()] |  | ||||||
|  |  | ||||||
|         if not isinstance(middleware, Middleware): |  | ||||||
|             middleware = Middleware( |  | ||||||
|                 middleware, |  | ||||||
|                 location=location, |  | ||||||
|                 priority=priority if isinstance(priority, int) else 0, |  | ||||||
|             ) |  | ||||||
|         elif middleware.priority != priority and isinstance(priority, int): |  | ||||||
|             middleware = Middleware( |  | ||||||
|                 middleware.func, |  | ||||||
|                 location=middleware.location, |  | ||||||
|                 priority=priority, |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         if location is MiddlewareLocation.REQUEST: |  | ||||||
|             if middleware not in self.request_middleware: |             if middleware not in self.request_middleware: | ||||||
|                 self.request_middleware.append(middleware) |                 self.request_middleware.append(middleware) | ||||||
|         if location is MiddlewareLocation.RESPONSE: |         if attach_to == "response": | ||||||
|             if middleware not in self.response_middleware: |             if middleware not in self.response_middleware: | ||||||
|                 self.response_middleware.appendleft(middleware) |                 self.response_middleware.appendleft(middleware) | ||||||
|         return retval |         return middleware | ||||||
|  |  | ||||||
|     def register_named_middleware( |     def register_named_middleware( | ||||||
|         self, |         self, | ||||||
|         middleware: MiddlewareType, |         middleware: MiddlewareType, | ||||||
|         route_names: Iterable[str], |         route_names: Iterable[str], | ||||||
|         attach_to: str = "request", |         attach_to: str = "request", | ||||||
|         *, |  | ||||||
|         priority: Union[Default, int] = _default, |  | ||||||
|     ): |     ): | ||||||
|         """ |         """ | ||||||
|         Method for attaching middleware to specific routes. This is mainly an |         Method for attaching middleware to specific routes. This is mainly an | ||||||
| @@ -360,35 +329,19 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|             defaults to "request" |             defaults to "request" | ||||||
|         :type attach_to: str, optional |         :type attach_to: str, optional | ||||||
|         """ |         """ | ||||||
|         retval = middleware |         if attach_to == "request": | ||||||
|         location = MiddlewareLocation[attach_to.upper()] |  | ||||||
|  |  | ||||||
|         if not isinstance(middleware, Middleware): |  | ||||||
|             middleware = Middleware( |  | ||||||
|                 middleware, |  | ||||||
|                 location=location, |  | ||||||
|                 priority=priority if isinstance(priority, int) else 0, |  | ||||||
|             ) |  | ||||||
|         elif middleware.priority != priority and isinstance(priority, int): |  | ||||||
|             middleware = Middleware( |  | ||||||
|                 middleware.func, |  | ||||||
|                 location=middleware.location, |  | ||||||
|                 priority=priority, |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         if location is MiddlewareLocation.REQUEST: |  | ||||||
|             for _rn in route_names: |             for _rn in route_names: | ||||||
|                 if _rn not in self.named_request_middleware: |                 if _rn not in self.named_request_middleware: | ||||||
|                     self.named_request_middleware[_rn] = deque() |                     self.named_request_middleware[_rn] = deque() | ||||||
|                 if middleware not in self.named_request_middleware[_rn]: |                 if middleware not in self.named_request_middleware[_rn]: | ||||||
|                     self.named_request_middleware[_rn].append(middleware) |                     self.named_request_middleware[_rn].append(middleware) | ||||||
|         if location is MiddlewareLocation.RESPONSE: |         if attach_to == "response": | ||||||
|             for _rn in route_names: |             for _rn in route_names: | ||||||
|                 if _rn not in self.named_response_middleware: |                 if _rn not in self.named_response_middleware: | ||||||
|                     self.named_response_middleware[_rn] = deque() |                     self.named_response_middleware[_rn] = deque() | ||||||
|                 if middleware not in self.named_response_middleware[_rn]: |                 if middleware not in self.named_response_middleware[_rn]: | ||||||
|                     self.named_response_middleware[_rn].appendleft(middleware) |                     self.named_response_middleware[_rn].appendleft(middleware) | ||||||
|         return retval |         return middleware | ||||||
|  |  | ||||||
|     def _apply_exception_handler( |     def _apply_exception_handler( | ||||||
|         self, |         self, | ||||||
| @@ -435,12 +388,15 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|             routes = [routes] |             routes = [routes] | ||||||
|  |  | ||||||
|         for r in routes: |         for r in routes: | ||||||
|             r.extra.websocket = websocket |             r.ctx.websocket = websocket | ||||||
|             r.extra.static = params.get("static", False) |             r.ctx.static = params.get("static", False) | ||||||
|             r.ctx.__dict__.update(ctx) |             r.ctx.__dict__.update(ctx) | ||||||
|  |  | ||||||
|         return routes |         return routes | ||||||
|  |  | ||||||
|  |     def _apply_static(self, static: FutureStatic) -> Route: | ||||||
|  |         return self._register_static(static) | ||||||
|  |  | ||||||
|     def _apply_middleware( |     def _apply_middleware( | ||||||
|         self, |         self, | ||||||
|         middleware: FutureMiddleware, |         middleware: FutureMiddleware, | ||||||
| @@ -519,14 +475,15 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|             for item in blueprint: |             for item in blueprint: | ||||||
|                 params = {**options} |                 params = {**options} | ||||||
|                 if isinstance(blueprint, BlueprintGroup): |                 if isinstance(blueprint, BlueprintGroup): | ||||||
|  |                     if blueprint.url_prefix: | ||||||
|                         merge_from = [ |                         merge_from = [ | ||||||
|                             options.get("url_prefix", ""), |                             options.get("url_prefix", ""), | ||||||
|                         blueprint.url_prefix or "", |                             blueprint.url_prefix, | ||||||
|                         ] |                         ] | ||||||
|                         if not isinstance(item, BlueprintGroup): |                         if not isinstance(item, BlueprintGroup): | ||||||
|                             merge_from.append(item.url_prefix or "") |                             merge_from.append(item.url_prefix or "") | ||||||
|                         merged_prefix = "/".join( |                         merged_prefix = "/".join( | ||||||
|                         u.strip("/") for u in merge_from if u |                             u.strip("/") for u in merge_from | ||||||
|                         ).rstrip("/") |                         ).rstrip("/") | ||||||
|                         params["url_prefix"] = f"/{merged_prefix}" |                         params["url_prefix"] = f"/{merged_prefix}" | ||||||
|  |  | ||||||
| @@ -626,7 +583,7 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|  |  | ||||||
|         uri = route.path |         uri = route.path | ||||||
|  |  | ||||||
|         if getattr(route.extra, "static", None): |         if getattr(route.ctx, "static", None): | ||||||
|             filename = kwargs.pop("filename", "") |             filename = kwargs.pop("filename", "") | ||||||
|             # it's static folder |             # it's static folder | ||||||
|             if "__file_uri__" in uri: |             if "__file_uri__" in uri: | ||||||
| @@ -659,18 +616,18 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|         host = kwargs.pop("_host", None) |         host = kwargs.pop("_host", None) | ||||||
|         external = kwargs.pop("_external", False) or bool(host) |         external = kwargs.pop("_external", False) or bool(host) | ||||||
|         scheme = kwargs.pop("_scheme", "") |         scheme = kwargs.pop("_scheme", "") | ||||||
|         if route.extra.hosts and external: |         if route.ctx.hosts and external: | ||||||
|             if not host and len(route.extra.hosts) > 1: |             if not host and len(route.ctx.hosts) > 1: | ||||||
|                 raise ValueError( |                 raise ValueError( | ||||||
|                     f"Host is ambiguous: {', '.join(route.extra.hosts)}" |                     f"Host is ambiguous: {', '.join(route.ctx.hosts)}" | ||||||
|                 ) |                 ) | ||||||
|             elif host and host not in route.extra.hosts: |             elif host and host not in route.ctx.hosts: | ||||||
|                 raise ValueError( |                 raise ValueError( | ||||||
|                     f"Requested host ({host}) is not available for this " |                     f"Requested host ({host}) is not available for this " | ||||||
|                     f"route: {route.extra.hosts}" |                     f"route: {route.ctx.hosts}" | ||||||
|                 ) |                 ) | ||||||
|             elif not host: |             elif not host: | ||||||
|                 host = list(route.extra.hosts)[0] |                 host = list(route.ctx.hosts)[0] | ||||||
|  |  | ||||||
|         if scheme and not external: |         if scheme and not external: | ||||||
|             raise ValueError("When specifying _scheme, _external must be True") |             raise ValueError("When specifying _scheme, _external must be True") | ||||||
| @@ -751,277 +708,10 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|         exception: BaseException, |         exception: BaseException, | ||||||
|         run_middleware: bool = True, |         run_middleware: bool = True, | ||||||
|     ):  # no cov |     ):  # no cov | ||||||
|         """ |         raise NotImplementedError | ||||||
|         A handler that catches specific exceptions and outputs a response. |  | ||||||
|  |  | ||||||
|         :param request: The current request object |  | ||||||
|         :param exception: The exception that was raised |  | ||||||
|         :raises ServerError: response 500 |  | ||||||
|         """ |  | ||||||
|         response = None |  | ||||||
|         await self.dispatch( |  | ||||||
|             "http.lifecycle.exception", |  | ||||||
|             inline=True, |  | ||||||
|             context={"request": request, "exception": exception}, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         if ( |  | ||||||
|             request.stream is not None |  | ||||||
|             and request.stream.stage is not Stage.HANDLER |  | ||||||
|         ): |  | ||||||
|             error_logger.exception(exception, exc_info=True) |  | ||||||
|             logger.error( |  | ||||||
|                 "The error response will not be sent to the client for " |  | ||||||
|                 f'the following exception:"{exception}". A previous response ' |  | ||||||
|                 "has at least partially been sent." |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|             handler = self.error_handler._lookup( |  | ||||||
|                 exception, request.name if request else None |  | ||||||
|             ) |  | ||||||
|             if handler: |  | ||||||
|                 logger.warning( |  | ||||||
|                     "An error occurred while handling the request after at " |  | ||||||
|                     "least some part of the response was sent to the client. " |  | ||||||
|                     "The response from your custom exception handler " |  | ||||||
|                     f"{handler.__name__} will not be sent to the client." |  | ||||||
|                     "Exception handlers should only be used to generate the " |  | ||||||
|                     "exception responses. If you would like to perform any " |  | ||||||
|                     "other action on a raised exception, consider using a " |  | ||||||
|                     "signal handler like " |  | ||||||
|                     '`@app.signal("http.lifecycle.exception")`\n' |  | ||||||
|                     "For further information, please see the docs: " |  | ||||||
|                     "https://sanicframework.org/en/guide/advanced/" |  | ||||||
|                     "signals.html", |  | ||||||
|                 ) |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         # -------------------------------------------- # |  | ||||||
|         # Request Middleware |  | ||||||
|         # -------------------------------------------- # |  | ||||||
|         if run_middleware: |  | ||||||
|             middleware = ( |  | ||||||
|                 request.route and request.route.extra.request_middleware |  | ||||||
|             ) or self.request_middleware |  | ||||||
|             response = await self._run_request_middleware(request, middleware) |  | ||||||
|         # No middleware results |  | ||||||
|         if not response: |  | ||||||
|             try: |  | ||||||
|                 response = self.error_handler.response(request, exception) |  | ||||||
|                 if isawaitable(response): |  | ||||||
|                     response = await response |  | ||||||
|             except Exception as e: |  | ||||||
|                 if isinstance(e, SanicException): |  | ||||||
|                     response = self.error_handler.default(request, e) |  | ||||||
|                 elif self.debug: |  | ||||||
|                     response = HTTPResponse( |  | ||||||
|                         ( |  | ||||||
|                             f"Error while handling error: {e}\n" |  | ||||||
|                             f"Stack: {format_exc()}" |  | ||||||
|                         ), |  | ||||||
|                         status=500, |  | ||||||
|                     ) |  | ||||||
|                 else: |  | ||||||
|                     response = HTTPResponse( |  | ||||||
|                         "An error occurred while handling an error", status=500 |  | ||||||
|                     ) |  | ||||||
|         if response is not None: |  | ||||||
|             try: |  | ||||||
|                 request.reset_response() |  | ||||||
|                 response = await request.respond(response) |  | ||||||
|             except BaseException: |  | ||||||
|                 # Skip response middleware |  | ||||||
|                 if request.stream: |  | ||||||
|                     request.stream.respond(response) |  | ||||||
|                 await response.send(end_stream=True) |  | ||||||
|                 raise |  | ||||||
|         else: |  | ||||||
|             if request.stream: |  | ||||||
|                 response = request.stream.response |  | ||||||
|  |  | ||||||
|         # Marked for cleanup and DRY with handle_request/handle_exception |  | ||||||
|         # when ResponseStream is no longer supporder |  | ||||||
|         if isinstance(response, BaseHTTPResponse): |  | ||||||
|             await self.dispatch( |  | ||||||
|                 "http.lifecycle.response", |  | ||||||
|                 inline=True, |  | ||||||
|                 context={ |  | ||||||
|                     "request": request, |  | ||||||
|                     "response": response, |  | ||||||
|                 }, |  | ||||||
|             ) |  | ||||||
|             await response.send(end_stream=True) |  | ||||||
|         elif isinstance(response, ResponseStream): |  | ||||||
|             resp = await response(request) |  | ||||||
|             await self.dispatch( |  | ||||||
|                 "http.lifecycle.response", |  | ||||||
|                 inline=True, |  | ||||||
|                 context={ |  | ||||||
|                     "request": request, |  | ||||||
|                     "response": resp, |  | ||||||
|                 }, |  | ||||||
|             ) |  | ||||||
|             await response.eof() |  | ||||||
|         else: |  | ||||||
|             raise ServerError( |  | ||||||
|                 f"Invalid response type {response!r} (need HTTPResponse)" |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|     async def handle_request(self, request: Request):  # no cov |     async def handle_request(self, request: Request):  # no cov | ||||||
|         """Take a request from the HTTP Server and return a response object |         raise NotImplementedError | ||||||
|         to be sent back The HTTP Server only expects a response object, so |  | ||||||
|         exception handling must be done here |  | ||||||
|  |  | ||||||
|         :param request: HTTP Request object |  | ||||||
|         :return: Nothing |  | ||||||
|         """ |  | ||||||
|         __tracebackhide__ = True |  | ||||||
|  |  | ||||||
|         await self.dispatch( |  | ||||||
|             "http.lifecycle.handle", |  | ||||||
|             inline=True, |  | ||||||
|             context={"request": request}, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Define `response` var here to remove warnings about |  | ||||||
|         # allocation before assignment below. |  | ||||||
|         response: Optional[ |  | ||||||
|             Union[ |  | ||||||
|                 BaseHTTPResponse, |  | ||||||
|                 Coroutine[Any, Any, Optional[BaseHTTPResponse]], |  | ||||||
|                 ResponseStream, |  | ||||||
|             ] |  | ||||||
|         ] = None |  | ||||||
|         run_middleware = True |  | ||||||
|         try: |  | ||||||
|             await self.dispatch( |  | ||||||
|                 "http.routing.before", |  | ||||||
|                 inline=True, |  | ||||||
|                 context={"request": request}, |  | ||||||
|             ) |  | ||||||
|             # Fetch handler from router |  | ||||||
|             route, handler, kwargs = self.router.get( |  | ||||||
|                 request.path, |  | ||||||
|                 request.method, |  | ||||||
|                 request.headers.getone("host", None), |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|             request._match_info = {**kwargs} |  | ||||||
|             request.route = route |  | ||||||
|  |  | ||||||
|             await self.dispatch( |  | ||||||
|                 "http.routing.after", |  | ||||||
|                 inline=True, |  | ||||||
|                 context={ |  | ||||||
|                     "request": request, |  | ||||||
|                     "route": route, |  | ||||||
|                     "kwargs": kwargs, |  | ||||||
|                     "handler": handler, |  | ||||||
|                 }, |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|             if ( |  | ||||||
|                 request.stream |  | ||||||
|                 and request.stream.request_body |  | ||||||
|                 and not route.extra.ignore_body |  | ||||||
|             ): |  | ||||||
|                 if hasattr(handler, "is_stream"): |  | ||||||
|                     # Streaming handler: lift the size limit |  | ||||||
|                     request.stream.request_max_size = float("inf") |  | ||||||
|                 else: |  | ||||||
|                     # Non-streaming handler: preload body |  | ||||||
|                     await request.receive_body() |  | ||||||
|  |  | ||||||
|             # -------------------------------------------- # |  | ||||||
|             # Request Middleware |  | ||||||
|             # -------------------------------------------- # |  | ||||||
|             run_middleware = False |  | ||||||
|             if request.route.extra.request_middleware: |  | ||||||
|                 response = await self._run_request_middleware( |  | ||||||
|                     request, request.route.extra.request_middleware |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|             # No middleware results |  | ||||||
|             if not response: |  | ||||||
|                 # -------------------------------------------- # |  | ||||||
|                 # Execute Handler |  | ||||||
|                 # -------------------------------------------- # |  | ||||||
|  |  | ||||||
|                 if handler is None: |  | ||||||
|                     raise ServerError( |  | ||||||
|                         ( |  | ||||||
|                             "'None' was returned while requesting a " |  | ||||||
|                             "handler from the router" |  | ||||||
|                         ) |  | ||||||
|                     ) |  | ||||||
|  |  | ||||||
|                 # Run response handler |  | ||||||
|                 await self.dispatch( |  | ||||||
|                     "http.handler.before", |  | ||||||
|                     inline=True, |  | ||||||
|                     context={"request": request}, |  | ||||||
|                 ) |  | ||||||
|                 response = handler(request, **request.match_info) |  | ||||||
|                 if isawaitable(response): |  | ||||||
|                     response = await response |  | ||||||
|                 await self.dispatch( |  | ||||||
|                     "http.handler.after", |  | ||||||
|                     inline=True, |  | ||||||
|                     context={"request": request}, |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|             if request.responded: |  | ||||||
|                 if response is not None: |  | ||||||
|                     error_logger.error( |  | ||||||
|                         "The response object returned by the route handler " |  | ||||||
|                         "will not be sent to client. The request has already " |  | ||||||
|                         "been responded to." |  | ||||||
|                     ) |  | ||||||
|                 if request.stream is not None: |  | ||||||
|                     response = request.stream.response |  | ||||||
|             elif response is not None: |  | ||||||
|                 response = await request.respond(response)  # type: ignore |  | ||||||
|             elif not hasattr(handler, "is_websocket"): |  | ||||||
|                 response = request.stream.response  # type: ignore |  | ||||||
|  |  | ||||||
|             # Marked for cleanup and DRY with handle_request/handle_exception |  | ||||||
|             # when ResponseStream is no longer supporder |  | ||||||
|             if isinstance(response, BaseHTTPResponse): |  | ||||||
|                 await self.dispatch( |  | ||||||
|                     "http.lifecycle.response", |  | ||||||
|                     inline=True, |  | ||||||
|                     context={ |  | ||||||
|                         "request": request, |  | ||||||
|                         "response": response, |  | ||||||
|                     }, |  | ||||||
|                 ) |  | ||||||
|                 ... |  | ||||||
|                 await response.send(end_stream=True) |  | ||||||
|             elif isinstance(response, ResponseStream): |  | ||||||
|                 resp = await response(request) |  | ||||||
|                 await self.dispatch( |  | ||||||
|                     "http.lifecycle.response", |  | ||||||
|                     inline=True, |  | ||||||
|                     context={ |  | ||||||
|                         "request": request, |  | ||||||
|                         "response": resp, |  | ||||||
|                     }, |  | ||||||
|                 ) |  | ||||||
|                 await response.eof() |  | ||||||
|             else: |  | ||||||
|                 if not hasattr(handler, "is_websocket"): |  | ||||||
|                     raise ServerError( |  | ||||||
|                         f"Invalid response type {response!r} " |  | ||||||
|                         "(need HTTPResponse)" |  | ||||||
|                     ) |  | ||||||
|  |  | ||||||
|         except CancelledError: |  | ||||||
|             raise |  | ||||||
|         except Exception as e: |  | ||||||
|             # Response Generation Failed |  | ||||||
|             await self.handle_exception( |  | ||||||
|                 request, e, run_middleware=run_middleware |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|     async def _websocket_handler( |     async def _websocket_handler( | ||||||
|         self, handler, request, *args, subprotocols=None, **kwargs |         self, handler, request, *args, subprotocols=None, **kwargs | ||||||
| @@ -1384,6 +1074,18 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|     def debug(self): |     def debug(self): | ||||||
|         return self.state.is_debug |         return self.state.is_debug | ||||||
|  |  | ||||||
|  |     @debug.setter | ||||||
|  |     def debug(self, value: bool): | ||||||
|  |         deprecation( | ||||||
|  |             "Setting the value of a Sanic application's debug value directly " | ||||||
|  |             "is deprecated and will be removed in v22.9. Please set it using " | ||||||
|  |             "the CLI, app.run, app.prepare, or directly set " | ||||||
|  |             "app.state.mode to Mode.DEBUG.", | ||||||
|  |             22.9, | ||||||
|  |         ) | ||||||
|  |         mode = Mode.DEBUG if value else Mode.PRODUCTION | ||||||
|  |         self.state.mode = mode | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def auto_reload(self): |     def auto_reload(self): | ||||||
|         return self.config.AUTO_RELOAD |         return self.config.AUTO_RELOAD | ||||||
| @@ -1400,6 +1102,58 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|         """ |         """ | ||||||
|         return self._state |         return self._state | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def is_running(self): | ||||||
|  |         deprecation( | ||||||
|  |             "Use of the is_running property is no longer used by Sanic " | ||||||
|  |             "internally. The property is now deprecated and will be removed " | ||||||
|  |             "in version 22.9. You may continue to set the property for your " | ||||||
|  |             "own needs until that time. If you would like to check whether " | ||||||
|  |             "the application is operational, please use app.state.stage. More " | ||||||
|  |             "information is available at ___.", | ||||||
|  |             22.9, | ||||||
|  |         ) | ||||||
|  |         return self.state.is_running | ||||||
|  |  | ||||||
|  |     @is_running.setter | ||||||
|  |     def is_running(self, value: bool): | ||||||
|  |         deprecation( | ||||||
|  |             "Use of the is_running property is no longer used by Sanic " | ||||||
|  |             "internally. The property is now deprecated and will be removed " | ||||||
|  |             "in version 22.9. You may continue to set the property for your " | ||||||
|  |             "own needs until that time. If you would like to check whether " | ||||||
|  |             "the application is operational, please use app.state.stage. More " | ||||||
|  |             "information is available at ___.", | ||||||
|  |             22.9, | ||||||
|  |         ) | ||||||
|  |         self.state.is_running = value | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def is_stopping(self): | ||||||
|  |         deprecation( | ||||||
|  |             "Use of the is_stopping property is no longer used by Sanic " | ||||||
|  |             "internally. The property is now deprecated and will be removed " | ||||||
|  |             "in version 22.9. You may continue to set the property for your " | ||||||
|  |             "own needs until that time. If you would like to check whether " | ||||||
|  |             "the application is operational, please use app.state.stage. More " | ||||||
|  |             "information is available at ___.", | ||||||
|  |             22.9, | ||||||
|  |         ) | ||||||
|  |         return self.state.is_stopping | ||||||
|  |  | ||||||
|  |     @is_stopping.setter | ||||||
|  |     def is_stopping(self, value: bool): | ||||||
|  |         deprecation( | ||||||
|  |             "Use of the is_stopping property is no longer used by Sanic " | ||||||
|  |             "internally. The property is now deprecated and will be removed " | ||||||
|  |             "in version 22.9. You may continue to set the property for your " | ||||||
|  |             "own needs until that time. If you would like to check whether " | ||||||
|  |             "the application is operational, please use app.state.stage. More " | ||||||
|  |             "information is available at ___.", | ||||||
|  |             22.9, | ||||||
|  |         ) | ||||||
|  |         self.state.is_stopping = value | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def reload_dirs(self): |     def reload_dirs(self): | ||||||
|         return self.state.reload_dirs |         return self.state.reload_dirs | ||||||
| @@ -1492,24 +1246,7 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|                 return cls.get_app("__mp_main__", force_create=force_create) |                 return cls.get_app("__mp_main__", force_create=force_create) | ||||||
|             if force_create: |             if force_create: | ||||||
|                 return cls(name) |                 return cls(name) | ||||||
|             raise SanicException( |             raise SanicException(f'Sanic app name "{name}" not found.') | ||||||
|                 f"Sanic app name '{name}' not found.\n" |  | ||||||
|                 "App instantiation must occur outside " |  | ||||||
|                 "if __name__ == '__main__' " |  | ||||||
|                 "block or by using an AppLoader.\nSee " |  | ||||||
|                 "https://sanic.dev/en/guide/deployment/app-loader.html" |  | ||||||
|                 " for more details." |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def _check_uvloop_conflict(cls) -> None: |  | ||||||
|         values = {app.config.USE_UVLOOP for app in cls._app_registry.values()} |  | ||||||
|         if len(values) > 1: |  | ||||||
|             error_logger.warning( |  | ||||||
|                 "It looks like you're running several apps with different " |  | ||||||
|                 "uvloop settings. This is not supported and may lead to " |  | ||||||
|                 "unintended behaviour." |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|     # Lifecycle |     # Lifecycle | ||||||
| @@ -1541,7 +1278,7 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|  |  | ||||||
|         if self.state.is_debug and self.config.TOUCHUP is not True: |         if self.state.is_debug and self.config.TOUCHUP is not True: | ||||||
|             self.config.TOUCHUP = False |             self.config.TOUCHUP = False | ||||||
|         elif isinstance(self.config.TOUCHUP, Default): |         elif self.config.TOUCHUP is _default: | ||||||
|             self.config.TOUCHUP = True |             self.config.TOUCHUP = True | ||||||
|  |  | ||||||
|         # Setup routers |         # Setup routers | ||||||
| @@ -1560,7 +1297,17 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|                 23.3, |                 23.3, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         Sanic._check_uvloop_conflict() |         # TODO: Replace in v22.6 to check against apps in app registry | ||||||
|  |         if ( | ||||||
|  |             self.__class__._uvloop_setting is not None | ||||||
|  |             and self.__class__._uvloop_setting != self.config.USE_UVLOOP | ||||||
|  |         ): | ||||||
|  |             error_logger.warning( | ||||||
|  |                 "It looks like you're running several apps with different " | ||||||
|  |                 "uvloop settings. This is not supported and may lead to " | ||||||
|  |                 "unintended behaviour." | ||||||
|  |             ) | ||||||
|  |         self.__class__._uvloop_setting = self.config.USE_UVLOOP | ||||||
|  |  | ||||||
|         # Startup time optimizations |         # Startup time optimizations | ||||||
|         if self.state.primary: |         if self.state.primary: | ||||||
| @@ -1571,7 +1318,6 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | |||||||
|  |  | ||||||
|         self.state.is_started = True |         self.state.is_started = True | ||||||
|  |  | ||||||
|     def ack(self): |  | ||||||
|         if hasattr(self, "multiplexer"): |         if hasattr(self, "multiplexer"): | ||||||
|             self.multiplexer.ack() |             self.multiplexer.ack() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,6 +8,11 @@ from typing import TYPE_CHECKING | |||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from sanic import Sanic |     from sanic import Sanic | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         from sanic_ext import Extend  # type: ignore | ||||||
|  |     except ImportError: | ||||||
|  |         ... | ||||||
|  |  | ||||||
|  |  | ||||||
| def setup_ext(app: Sanic, *, fail: bool = False, **kwargs): | def setup_ext(app: Sanic, *, fail: bool = False, **kwargs): | ||||||
|     if not app.config.AUTO_EXTEND: |     if not app.config.AUTO_EXTEND: | ||||||
| @@ -28,7 +33,7 @@ def setup_ext(app: Sanic, *, fail: bool = False, **kwargs): | |||||||
|         return |         return | ||||||
|  |  | ||||||
|     if not getattr(app, "_ext", None): |     if not getattr(app, "_ext", None): | ||||||
|         Ext = getattr(sanic_ext, "Extend") |         Ext: Extend = getattr(sanic_ext, "Extend") | ||||||
|         app._ext = Ext(app, **kwargs) |         app._ext = Ext(app, **kwargs) | ||||||
|  |  | ||||||
|         return app.ext |         return app.ext | ||||||
|   | |||||||
| @@ -40,8 +40,6 @@ FULL_COLOR_LOGO = """ | |||||||
|  |  | ||||||
| """  # noqa | """  # noqa | ||||||
|  |  | ||||||
| SVG_LOGO_SIMPLE = """<svg id=logo-simple viewBox="0 0 964 279"><desc>Sanic</desc><path d="M107 222c9-2 10-20 1-22s-20-2-30-2-17 7-16 14 6 10 15 10h30zm115-1c16-2 30-11 35-23s6-24 2-33-6-14-15-20-24-11-38-10c-7 3-10 13-5 19s17-1 24 4 15 14 13 24-5 15-14 18-50 0-74 0h-17c-6 4-10 15-4 20s16 2 23 3zM251 83q9-1 9-7 0-15-10-16h-13c-10 6-10 20 0 22zM147 60c-4 0-10 3-11 11s5 13 10 12 42 0 67 0c5-3 7-10 6-15s-4-8-9-8zm-33 1c-8 0-16 0-24 3s-20 10-25 20-6 24-4 36 15 22 26 27 78 8 94 3c4-4 4-12 0-18s-69 8-93-10c-8-7-9-23 0-30s12-10 20-10 12 2 16-3 1-15-5-18z" fill="#ff0d68"/><path d="M676 74c0-14-18-9-20 0s0 30 0 39 20 9 20 2zm-297-10c-12 2-15 12-23 23l-41 58H340l22-30c8-12 23-13 30-4s20 24 24 38-10 10-17 10l-68 2q-17 1-48 30c-7 6-10 20 0 24s15-8 20-13 20 -20 58-21h50 c20 2 33 9 52 30 8 10 24-4 16-13L384 65q-3-2-5-1zm131 0c-10 1-12 12-11 20v96c1 10-3 23 5 32s20-5 17-15c0-23-3-46 2-67 5-12 22-14 32-5l103 87c7 5 19 1 18-9v-64c-3-10-20-9-21 2s-20 22-30 13l-97-80c-5-4-10-10-18-10zM701 76v128c2 10 15 12 20 4s0-102 0-124s-20-18-20-7z M850 63c-35 0-69-2-86 15s-20 60-13 66 13 8 16 0 1-10 1-27 12-26 20-32 66-5 85-5 31 4 31-10-18-7-54-7M764 159c-6-2-15-2-16 12s19 37 33 43 23 8 25-4-4-11-11-14q-9-3-22-18c-4-7-3-16-10-19zM828 196c-4 0-8 1-10 5s-4 12 0 15 8 2 12 2h60c5 0 10-2 12-6 3-7-1-16-8-16z" fill="#1f1f1f"/></svg>"""  # noqa |  | ||||||
|  |  | ||||||
| ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") | ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,9 +7,10 @@ from urllib.parse import quote | |||||||
|  |  | ||||||
| from sanic.compat import Header | from sanic.compat import Header | ||||||
| from sanic.exceptions import ServerError | from sanic.exceptions import ServerError | ||||||
| from sanic.helpers import Default | from sanic.handlers import RequestManager | ||||||
|  | from sanic.helpers import _default | ||||||
| from sanic.http import Stage | from sanic.http import Stage | ||||||
| from sanic.log import error_logger, logger | from sanic.log import logger | ||||||
| from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport | from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport | ||||||
| from sanic.request import Request | from sanic.request import Request | ||||||
| from sanic.response import BaseHTTPResponse | from sanic.response import BaseHTTPResponse | ||||||
| @@ -61,7 +62,7 @@ class Lifespan: | |||||||
|         await self.asgi_app.sanic_app._server_event("init", "before") |         await self.asgi_app.sanic_app._server_event("init", "before") | ||||||
|         await self.asgi_app.sanic_app._server_event("init", "after") |         await self.asgi_app.sanic_app._server_event("init", "after") | ||||||
|  |  | ||||||
|         if not isinstance(self.asgi_app.sanic_app.config.USE_UVLOOP, Default): |         if self.asgi_app.sanic_app.config.USE_UVLOOP is not _default: | ||||||
|             warnings.warn( |             warnings.warn( | ||||||
|                 "You have set the USE_UVLOOP configuration option, but Sanic " |                 "You have set the USE_UVLOOP configuration option, but Sanic " | ||||||
|                 "cannot control the event loop when running in ASGI mode." |                 "cannot control the event loop when running in ASGI mode." | ||||||
| @@ -85,26 +86,12 @@ class Lifespan: | |||||||
|     ) -> None: |     ) -> None: | ||||||
|         message = await receive() |         message = await receive() | ||||||
|         if message["type"] == "lifespan.startup": |         if message["type"] == "lifespan.startup": | ||||||
|             try: |  | ||||||
|             await self.startup() |             await self.startup() | ||||||
|             except Exception as e: |  | ||||||
|                 error_logger.exception(e) |  | ||||||
|                 await send( |  | ||||||
|                     {"type": "lifespan.startup.failed", "message": str(e)} |  | ||||||
|                 ) |  | ||||||
|             else: |  | ||||||
|             await send({"type": "lifespan.startup.complete"}) |             await send({"type": "lifespan.startup.complete"}) | ||||||
|  |  | ||||||
|         message = await receive() |         message = await receive() | ||||||
|         if message["type"] == "lifespan.shutdown": |         if message["type"] == "lifespan.shutdown": | ||||||
|             try: |  | ||||||
|             await self.shutdown() |             await self.shutdown() | ||||||
|             except Exception as e: |  | ||||||
|                 error_logger.exception(e) |  | ||||||
|                 await send( |  | ||||||
|                     {"type": "lifespan.shutdown.failed", "message": str(e)} |  | ||||||
|                 ) |  | ||||||
|             else: |  | ||||||
|             await send({"type": "lifespan.shutdown.complete"}) |             await send({"type": "lifespan.shutdown.complete"}) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -244,11 +231,9 @@ class ASGIApp: | |||||||
|         """ |         """ | ||||||
|         Handle the incoming request. |         Handle the incoming request. | ||||||
|         """ |         """ | ||||||
|  |         manager = RequestManager.create(self.request) | ||||||
|         try: |         try: | ||||||
|             self.stage = Stage.HANDLER |             self.stage = Stage.HANDLER | ||||||
|             await self.sanic_app.handle_request(self.request) |             await manager.handle() | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             try: |             await manager.error(e) | ||||||
|                 await self.sanic_app.handle_exception(self.request, e) |  | ||||||
|             except Exception as exc: |  | ||||||
|                 await self.sanic_app.handle_exception(self.request, exc, False) |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import re | import re | ||||||
|  |  | ||||||
| from typing import Any, Optional | from typing import Any | ||||||
|  |  | ||||||
| from sanic.base.meta import SanicMeta | from sanic.base.meta import SanicMeta | ||||||
| from sanic.exceptions import SanicException | from sanic.exceptions import SanicException | ||||||
| @@ -9,7 +9,6 @@ from sanic.mixins.listeners import ListenerMixin | |||||||
| from sanic.mixins.middleware import MiddlewareMixin | from sanic.mixins.middleware import MiddlewareMixin | ||||||
| from sanic.mixins.routes import RouteMixin | from sanic.mixins.routes import RouteMixin | ||||||
| from sanic.mixins.signals import SignalMixin | from sanic.mixins.signals import SignalMixin | ||||||
| from sanic.mixins.static import StaticMixin |  | ||||||
|  |  | ||||||
|  |  | ||||||
| VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$") | VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$") | ||||||
| @@ -17,7 +16,6 @@ VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$") | |||||||
|  |  | ||||||
| class BaseSanic( | class BaseSanic( | ||||||
|     RouteMixin, |     RouteMixin, | ||||||
|     StaticMixin, |  | ||||||
|     MiddlewareMixin, |     MiddlewareMixin, | ||||||
|     ListenerMixin, |     ListenerMixin, | ||||||
|     ExceptionMixin, |     ExceptionMixin, | ||||||
| @@ -26,9 +24,7 @@ class BaseSanic( | |||||||
| ): | ): | ||||||
|     __slots__ = ("name",) |     __slots__ = ("name",) | ||||||
|  |  | ||||||
|     def __init__( |     def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None: | ||||||
|         self, name: Optional[str] = None, *args: Any, **kwargs: Any |  | ||||||
|     ) -> None: |  | ||||||
|         class_name = self.__class__.__name__ |         class_name = self.__class__.__name__ | ||||||
|  |  | ||||||
|         if name is None: |         if name is None: | ||||||
|   | |||||||
| @@ -304,6 +304,9 @@ class Blueprint(BaseSanic): | |||||||
|  |  | ||||||
|         # Routes |         # Routes | ||||||
|         for future in self._future_routes: |         for future in self._future_routes: | ||||||
|  |             # attach the blueprint name to the handler so that it can be | ||||||
|  |             # prefixed properly in the router | ||||||
|  |             future.handler.__blueprintname__ = self.name | ||||||
|             # Prepend the blueprint URI prefix if available |             # Prepend the blueprint URI prefix if available | ||||||
|             uri = self._setup_uri(future.uri, url_prefix) |             uri = self._setup_uri(future.uri, url_prefix) | ||||||
|  |  | ||||||
| @@ -403,7 +406,7 @@ class Blueprint(BaseSanic): | |||||||
|  |  | ||||||
|         self.routes += [route for route in routes if isinstance(route, Route)] |         self.routes += [route for route in routes if isinstance(route, Route)] | ||||||
|         self.websocket_routes += [ |         self.websocket_routes += [ | ||||||
|             route for route in self.routes if route.extra.websocket |             route for route in self.routes if route.ctx.websocket | ||||||
|         ] |         ] | ||||||
|         self.middlewares += middleware |         self.middlewares += middleware | ||||||
|         self.exceptions += exception_handlers |         self.exceptions += exception_handlers | ||||||
| @@ -439,7 +442,7 @@ class Blueprint(BaseSanic): | |||||||
|             events.add(signal.ctx.event) |             events.add(signal.ctx.event) | ||||||
|  |  | ||||||
|         return asyncio.wait( |         return asyncio.wait( | ||||||
|             [asyncio.create_task(event.wait()) for event in events], |             [event.wait() for event in events], | ||||||
|             return_when=asyncio.FIRST_COMPLETED, |             return_when=asyncio.FIRST_COMPLETED, | ||||||
|             timeout=timeout, |             timeout=timeout, | ||||||
|         ) |         ) | ||||||
|   | |||||||
| @@ -1,18 +0,0 @@ | |||||||
| from sanic.__version__ import __compatibility__ |  | ||||||
|  |  | ||||||
| if __compatibility__ == "22.12": |  | ||||||
|     from .v22_12.request import ( |  | ||||||
|         File, |  | ||||||
|         Request, |  | ||||||
|         RequestParameters, |  | ||||||
|         parse_multipart_form, |  | ||||||
|     ) |  | ||||||
| elif __compatibility__ == "23.3": |  | ||||||
|     from .v23_3.request import ( |  | ||||||
|         File, |  | ||||||
|         Request, |  | ||||||
|         RequestParameters, |  | ||||||
|         parse_multipart_form, |  | ||||||
|     ) |  | ||||||
| else: |  | ||||||
|     raise RuntimeError(f"Unknown compatibility value: {__compatibility__}") |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,9 +0,0 @@ | |||||||
| from ..v22_12.request import File |  | ||||||
| from ..v22_12.request import Request as LegacyRequest |  | ||||||
| from ..v22_12.request import RequestParameters, parse_multipart_form |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Request(LegacyRequest): |  | ||||||
|     @property |  | ||||||
|     def something_new(self): |  | ||||||
|         return 123 |  | ||||||
							
								
								
									
										112
									
								
								sanic/cli/app.py
									
									
									
									
									
								
							
							
						
						
									
										112
									
								
								sanic/cli/app.py
									
									
									
									
									
								
							| @@ -3,21 +3,23 @@ import os | |||||||
| import shutil | import shutil | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from argparse import Namespace | from argparse import ArgumentParser, RawTextHelpFormatter | ||||||
| from functools import partial | from functools import partial | ||||||
| from textwrap import indent | from textwrap import indent | ||||||
| from typing import List, Union, cast | from typing import Any, List, Union | ||||||
|  |  | ||||||
| from sanic.app import Sanic | from sanic.app import Sanic | ||||||
| from sanic.application.logo import get_logo | from sanic.application.logo import get_logo | ||||||
| from sanic.cli.arguments import Group | from sanic.cli.arguments import Group | ||||||
| from sanic.cli.base import SanicArgumentParser, SanicHelpFormatter | from sanic.log import error_logger | ||||||
| from sanic.cli.inspector import make_inspector_parser | from sanic.worker.inspector import inspect | ||||||
| from sanic.cli.inspector_client import InspectorClient |  | ||||||
| from sanic.log import Colors, error_logger |  | ||||||
| from sanic.worker.loader import AppLoader | from sanic.worker.loader import AppLoader | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SanicArgumentParser(ArgumentParser): | ||||||
|  |     ... | ||||||
|  |  | ||||||
|  |  | ||||||
| class SanicCLI: | class SanicCLI: | ||||||
|     DESCRIPTION = indent( |     DESCRIPTION = indent( | ||||||
|         f""" |         f""" | ||||||
| @@ -44,7 +46,7 @@ Or, a path to a directory to run as a simple HTTP server: | |||||||
|         self.parser = SanicArgumentParser( |         self.parser = SanicArgumentParser( | ||||||
|             prog="sanic", |             prog="sanic", | ||||||
|             description=self.DESCRIPTION, |             description=self.DESCRIPTION, | ||||||
|             formatter_class=lambda prog: SanicHelpFormatter( |             formatter_class=lambda prog: RawTextHelpFormatter( | ||||||
|                 prog, |                 prog, | ||||||
|                 max_help_position=36 if width > 96 else 24, |                 max_help_position=36 if width > 96 else 24, | ||||||
|                 indent_increment=4, |                 indent_increment=4, | ||||||
| @@ -56,27 +58,16 @@ Or, a path to a directory to run as a simple HTTP server: | |||||||
|         self.main_process = ( |         self.main_process = ( | ||||||
|             os.environ.get("SANIC_RELOADER_PROCESS", "") != "true" |             os.environ.get("SANIC_RELOADER_PROCESS", "") != "true" | ||||||
|         ) |         ) | ||||||
|         self.args: Namespace = Namespace() |         self.args: List[Any] = [] | ||||||
|         self.groups: List[Group] = [] |         self.groups: List[Group] = [] | ||||||
|         self.inspecting = False |  | ||||||
|  |  | ||||||
|     def attach(self): |     def attach(self): | ||||||
|         if len(sys.argv) > 1 and sys.argv[1] == "inspect": |  | ||||||
|             self.inspecting = True |  | ||||||
|             self.parser.description = get_logo(True) |  | ||||||
|             make_inspector_parser(self.parser) |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         for group in Group._registry: |         for group in Group._registry: | ||||||
|             instance = group.create(self.parser) |             instance = group.create(self.parser) | ||||||
|             instance.attach() |             instance.attach() | ||||||
|             self.groups.append(instance) |             self.groups.append(instance) | ||||||
|  |  | ||||||
|     def run(self, parse_args=None): |     def run(self, parse_args=None): | ||||||
|         if self.inspecting: |  | ||||||
|             self._inspector() |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         legacy_version = False |         legacy_version = False | ||||||
|         if not parse_args: |         if not parse_args: | ||||||
|             # This is to provide backwards compat -v to display version |             # This is to provide backwards compat -v to display version | ||||||
| @@ -95,21 +86,36 @@ Or, a path to a directory to run as a simple HTTP server: | |||||||
|         self.args = self.parser.parse_args(args=parse_args) |         self.args = self.parser.parse_args(args=parse_args) | ||||||
|         self._precheck() |         self._precheck() | ||||||
|         app_loader = AppLoader( |         app_loader = AppLoader( | ||||||
|             self.args.module, self.args.factory, self.args.simple, self.args |             self.args.module, | ||||||
|  |             self.args.factory, | ||||||
|  |             self.args.simple, | ||||||
|  |             self.args, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         if self.args.inspect or self.args.inspect_raw or self.args.trigger: |  | ||||||
|             self._inspector_legacy(app_loader) |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             app = self._get_app(app_loader) |             app = self._get_app(app_loader) | ||||||
|             kwargs = self._build_run_kwargs() |             kwargs = self._build_run_kwargs() | ||||||
|         except ValueError as e: |         except ValueError as e: | ||||||
|             error_logger.exception(f"Failed to run app: {e}") |             error_logger.exception(f"Failed to run app: {e}") | ||||||
|  |         else: | ||||||
|  |             if self.args.inspect or self.args.inspect_raw or self.args.trigger: | ||||||
|  |                 os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "true" | ||||||
|             else: |             else: | ||||||
|                 for http_version in self.args.http: |                 for http_version in self.args.http: | ||||||
|                     app.prepare(**kwargs, version=http_version) |                     app.prepare(**kwargs, version=http_version) | ||||||
|  |  | ||||||
|  |             if self.args.inspect or self.args.inspect_raw or self.args.trigger: | ||||||
|  |                 action = self.args.trigger or ( | ||||||
|  |                     "raw" if self.args.inspect_raw else "pretty" | ||||||
|  |                 ) | ||||||
|  |                 inspect( | ||||||
|  |                     app.config.INSPECTOR_HOST, | ||||||
|  |                     app.config.INSPECTOR_PORT, | ||||||
|  |                     action, | ||||||
|  |                 ) | ||||||
|  |                 del os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] | ||||||
|  |                 return | ||||||
|  |  | ||||||
|             if self.args.single: |             if self.args.single: | ||||||
|                 serve = Sanic.serve_single |                 serve = Sanic.serve_single | ||||||
|             elif self.args.legacy: |             elif self.args.legacy: | ||||||
| @@ -118,64 +124,6 @@ Or, a path to a directory to run as a simple HTTP server: | |||||||
|                 serve = partial(Sanic.serve, app_loader=app_loader) |                 serve = partial(Sanic.serve, app_loader=app_loader) | ||||||
|             serve(app) |             serve(app) | ||||||
|  |  | ||||||
|     def _inspector_legacy(self, app_loader: AppLoader): |  | ||||||
|         host = port = None |  | ||||||
|         module = cast(str, self.args.module) |  | ||||||
|         if ":" in module: |  | ||||||
|             maybe_host, maybe_port = module.rsplit(":", 1) |  | ||||||
|             if maybe_port.isnumeric(): |  | ||||||
|                 host, port = maybe_host, int(maybe_port) |  | ||||||
|         if not host: |  | ||||||
|             app = self._get_app(app_loader) |  | ||||||
|             host, port = app.config.INSPECTOR_HOST, app.config.INSPECTOR_PORT |  | ||||||
|  |  | ||||||
|         action = self.args.trigger or "info" |  | ||||||
|  |  | ||||||
|         InspectorClient( |  | ||||||
|             str(host), int(port or 6457), False, self.args.inspect_raw, "" |  | ||||||
|         ).do(action) |  | ||||||
|         sys.stdout.write( |  | ||||||
|             f"\n{Colors.BOLD}{Colors.YELLOW}WARNING:{Colors.END} " |  | ||||||
|             "You are using the legacy CLI command that will be removed in " |  | ||||||
|             f"{Colors.RED}v23.3{Colors.END}. See " |  | ||||||
|             "https://sanic.dev/en/guide/release-notes/v22.12.html" |  | ||||||
|             "#deprecations-and-removals or checkout the new " |  | ||||||
|             "style commands:\n\n\t" |  | ||||||
|             f"{Colors.YELLOW}sanic inspect --help{Colors.END}\n" |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def _inspector(self): |  | ||||||
|         args = sys.argv[2:] |  | ||||||
|         self.args, unknown = self.parser.parse_known_args(args=args) |  | ||||||
|         if unknown: |  | ||||||
|             for arg in unknown: |  | ||||||
|                 if arg.startswith("--"): |  | ||||||
|                     try: |  | ||||||
|                         key, value = arg.split("=") |  | ||||||
|                         key = key.lstrip("-") |  | ||||||
|                     except ValueError: |  | ||||||
|                         value = False if arg.startswith("--no-") else True |  | ||||||
|                         key = ( |  | ||||||
|                             arg.replace("--no-", "") |  | ||||||
|                             .lstrip("-") |  | ||||||
|                             .replace("-", "_") |  | ||||||
|                         ) |  | ||||||
|                     setattr(self.args, key, value) |  | ||||||
|  |  | ||||||
|         kwargs = {**self.args.__dict__} |  | ||||||
|         host = kwargs.pop("host") |  | ||||||
|         port = kwargs.pop("port") |  | ||||||
|         secure = kwargs.pop("secure") |  | ||||||
|         raw = kwargs.pop("raw") |  | ||||||
|         action = kwargs.pop("action") or "info" |  | ||||||
|         api_key = kwargs.pop("api_key") |  | ||||||
|         positional = kwargs.pop("positional", None) |  | ||||||
|         if action == "<custom>" and positional: |  | ||||||
|             action = positional[0] |  | ||||||
|             if len(positional) > 1: |  | ||||||
|                 kwargs["args"] = positional[1:] |  | ||||||
|         InspectorClient(host, port, secure, raw, api_key).do(action, **kwargs) |  | ||||||
|  |  | ||||||
|     def _precheck(self): |     def _precheck(self): | ||||||
|         # Custom TLS mismatch handling for better diagnostics |         # Custom TLS mismatch handling for better diagnostics | ||||||
|         if self.main_process and ( |         if self.main_process and ( | ||||||
|   | |||||||
| @@ -1,35 +0,0 @@ | |||||||
| from argparse import ( |  | ||||||
|     SUPPRESS, |  | ||||||
|     Action, |  | ||||||
|     ArgumentParser, |  | ||||||
|     RawTextHelpFormatter, |  | ||||||
|     _SubParsersAction, |  | ||||||
| ) |  | ||||||
| from typing import Any |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SanicArgumentParser(ArgumentParser): |  | ||||||
|     def _check_value(self, action: Action, value: Any) -> None: |  | ||||||
|         if isinstance(action, SanicSubParsersAction): |  | ||||||
|             return |  | ||||||
|         super()._check_value(action, value) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SanicHelpFormatter(RawTextHelpFormatter): |  | ||||||
|     def add_usage(self, usage, actions, groups, prefix=None): |  | ||||||
|         if not usage: |  | ||||||
|             usage = SUPPRESS |  | ||||||
|             # Add one linebreak, but not two |  | ||||||
|             self.add_text("\x1b[1A") |  | ||||||
|         super().add_usage(usage, actions, groups, prefix) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SanicSubParsersAction(_SubParsersAction): |  | ||||||
|     def __call__(self, parser, namespace, values, option_string=None): |  | ||||||
|         self._name_parser_map |  | ||||||
|         parser_name = values[0] |  | ||||||
|         if parser_name not in self._name_parser_map: |  | ||||||
|             self._name_parser_map[parser_name] = parser |  | ||||||
|             values = ["<custom>", *values] |  | ||||||
|  |  | ||||||
|         super().__call__(parser, namespace, values, option_string) |  | ||||||
| @@ -1,105 +0,0 @@ | |||||||
| from argparse import ArgumentParser |  | ||||||
|  |  | ||||||
| from sanic.application.logo import get_logo |  | ||||||
| from sanic.cli.base import SanicHelpFormatter, SanicSubParsersAction |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def _add_shared(parser: ArgumentParser) -> None: |  | ||||||
|     parser.add_argument( |  | ||||||
|         "--host", |  | ||||||
|         "-H", |  | ||||||
|         default="localhost", |  | ||||||
|         help="Inspector host address [default 127.0.0.1]", |  | ||||||
|     ) |  | ||||||
|     parser.add_argument( |  | ||||||
|         "--port", |  | ||||||
|         "-p", |  | ||||||
|         default=6457, |  | ||||||
|         type=int, |  | ||||||
|         help="Inspector port [default 6457]", |  | ||||||
|     ) |  | ||||||
|     parser.add_argument( |  | ||||||
|         "--secure", |  | ||||||
|         "-s", |  | ||||||
|         action="store_true", |  | ||||||
|         help="Whether to access the Inspector via TLS encryption", |  | ||||||
|     ) |  | ||||||
|     parser.add_argument("--api-key", "-k", help="Inspector authentication key") |  | ||||||
|     parser.add_argument( |  | ||||||
|         "--raw", |  | ||||||
|         action="store_true", |  | ||||||
|         help="Whether to output the raw response information", |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class InspectorSubParser(ArgumentParser): |  | ||||||
|     def __init__(self, *args, **kwargs): |  | ||||||
|         super().__init__(*args, **kwargs) |  | ||||||
|         _add_shared(self) |  | ||||||
|         if not self.description: |  | ||||||
|             self.description = "" |  | ||||||
|         self.description = get_logo(True) + self.description |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def make_inspector_parser(parser: ArgumentParser) -> None: |  | ||||||
|     _add_shared(parser) |  | ||||||
|     subparsers = parser.add_subparsers( |  | ||||||
|         action=SanicSubParsersAction, |  | ||||||
|         dest="action", |  | ||||||
|         description=( |  | ||||||
|             "Run one or none of the below subcommands. Using inspect without " |  | ||||||
|             "a subcommand will fetch general information about the state " |  | ||||||
|             "of the application instance.\n\n" |  | ||||||
|             "Or, you can optionally follow inspect with a subcommand. " |  | ||||||
|             "If you have created a custom " |  | ||||||
|             "Inspector instance, then you can run custom commands. See " |  | ||||||
|             "https://sanic.dev/en/guide/deployment/inspector.html " |  | ||||||
|             "for more details." |  | ||||||
|         ), |  | ||||||
|         title="  Subcommands", |  | ||||||
|         parser_class=InspectorSubParser, |  | ||||||
|     ) |  | ||||||
|     reloader = subparsers.add_parser( |  | ||||||
|         "reload", |  | ||||||
|         help="Trigger a reload of the server workers", |  | ||||||
|         formatter_class=SanicHelpFormatter, |  | ||||||
|     ) |  | ||||||
|     reloader.add_argument( |  | ||||||
|         "--zero-downtime", |  | ||||||
|         action="store_true", |  | ||||||
|         help=( |  | ||||||
|             "Whether to wait for the new process to be online before " |  | ||||||
|             "terminating the old" |  | ||||||
|         ), |  | ||||||
|     ) |  | ||||||
|     subparsers.add_parser( |  | ||||||
|         "shutdown", |  | ||||||
|         help="Shutdown the application and all processes", |  | ||||||
|         formatter_class=SanicHelpFormatter, |  | ||||||
|     ) |  | ||||||
|     scale = subparsers.add_parser( |  | ||||||
|         "scale", |  | ||||||
|         help="Scale the number of workers", |  | ||||||
|         formatter_class=SanicHelpFormatter, |  | ||||||
|     ) |  | ||||||
|     scale.add_argument( |  | ||||||
|         "replicas", |  | ||||||
|         type=int, |  | ||||||
|         help="Number of workers requested", |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     custom = subparsers.add_parser( |  | ||||||
|         "<custom>", |  | ||||||
|         help="Run a custom command", |  | ||||||
|         description=( |  | ||||||
|             "keyword arguments:\n  When running a custom command, you can " |  | ||||||
|             "add keyword arguments by appending them to your command\n\n" |  | ||||||
|             "\tsanic inspect foo --one=1 --two=2" |  | ||||||
|         ), |  | ||||||
|         formatter_class=SanicHelpFormatter, |  | ||||||
|     ) |  | ||||||
|     custom.add_argument( |  | ||||||
|         "positional", |  | ||||||
|         nargs="*", |  | ||||||
|         help="Add one or more non-keyword args to your custom command", |  | ||||||
|     ) |  | ||||||
| @@ -1,119 +0,0 @@ | |||||||
| from __future__ import annotations |  | ||||||
|  |  | ||||||
| import sys |  | ||||||
|  |  | ||||||
| from http.client import RemoteDisconnected |  | ||||||
| from textwrap import indent |  | ||||||
| from typing import Any, Dict, Optional |  | ||||||
| from urllib.error import URLError |  | ||||||
| from urllib.request import Request as URequest |  | ||||||
| from urllib.request import urlopen |  | ||||||
|  |  | ||||||
| from sanic.application.logo import get_logo |  | ||||||
| from sanic.application.motd import MOTDTTY |  | ||||||
| from sanic.log import Colors |  | ||||||
|  |  | ||||||
|  |  | ||||||
| try:  # no cov |  | ||||||
|     from ujson import dumps, loads |  | ||||||
| except ModuleNotFoundError:  # no cov |  | ||||||
|     from json import dumps, loads  # type: ignore |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class InspectorClient: |  | ||||||
|     def __init__( |  | ||||||
|         self, |  | ||||||
|         host: str, |  | ||||||
|         port: int, |  | ||||||
|         secure: bool, |  | ||||||
|         raw: bool, |  | ||||||
|         api_key: Optional[str], |  | ||||||
|     ) -> None: |  | ||||||
|         self.scheme = "https" if secure else "http" |  | ||||||
|         self.host = host |  | ||||||
|         self.port = port |  | ||||||
|         self.raw = raw |  | ||||||
|         self.api_key = api_key |  | ||||||
|  |  | ||||||
|         for scheme in ("http", "https"): |  | ||||||
|             full = f"{scheme}://" |  | ||||||
|             if self.host.startswith(full): |  | ||||||
|                 self.scheme = scheme |  | ||||||
|                 self.host = self.host[len(full) :]  # noqa E203 |  | ||||||
|  |  | ||||||
|     def do(self, action: str, **kwargs: Any) -> None: |  | ||||||
|         if action == "info": |  | ||||||
|             self.info() |  | ||||||
|             return |  | ||||||
|         result = self.request(action, **kwargs).get("result") |  | ||||||
|         if result: |  | ||||||
|             out = ( |  | ||||||
|                 dumps(result) |  | ||||||
|                 if isinstance(result, (list, dict)) |  | ||||||
|                 else str(result) |  | ||||||
|             ) |  | ||||||
|             sys.stdout.write(out + "\n") |  | ||||||
|  |  | ||||||
|     def info(self) -> None: |  | ||||||
|         out = sys.stdout.write |  | ||||||
|         response = self.request("", "GET") |  | ||||||
|         if self.raw or not response: |  | ||||||
|             return |  | ||||||
|         data = response["result"] |  | ||||||
|         display = data.pop("info") |  | ||||||
|         extra = display.pop("extra", {}) |  | ||||||
|         display["packages"] = ", ".join(display["packages"]) |  | ||||||
|         MOTDTTY(get_logo(), self.base_url, display, extra).display( |  | ||||||
|             version=False, |  | ||||||
|             action="Inspecting", |  | ||||||
|             out=out, |  | ||||||
|         ) |  | ||||||
|         for name, info in data["workers"].items(): |  | ||||||
|             info = "\n".join( |  | ||||||
|                 f"\t{key}: {Colors.BLUE}{value}{Colors.END}" |  | ||||||
|                 for key, value in info.items() |  | ||||||
|             ) |  | ||||||
|             out( |  | ||||||
|                 "\n" |  | ||||||
|                 + indent( |  | ||||||
|                     "\n".join( |  | ||||||
|                         [ |  | ||||||
|                             f"{Colors.BOLD}{Colors.SANIC}{name}{Colors.END}", |  | ||||||
|                             info, |  | ||||||
|                         ] |  | ||||||
|                     ), |  | ||||||
|                     "  ", |  | ||||||
|                 ) |  | ||||||
|                 + "\n" |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|     def request(self, action: str, method: str = "POST", **kwargs: Any) -> Any: |  | ||||||
|         url = f"{self.base_url}/{action}" |  | ||||||
|         params: Dict[str, Any] = {"method": method, "headers": {}} |  | ||||||
|         if kwargs: |  | ||||||
|             params["data"] = dumps(kwargs).encode() |  | ||||||
|             params["headers"]["content-type"] = "application/json" |  | ||||||
|         if self.api_key: |  | ||||||
|             params["headers"]["authorization"] = f"Bearer {self.api_key}" |  | ||||||
|         request = URequest(url, **params) |  | ||||||
|  |  | ||||||
|         try: |  | ||||||
|             with urlopen(request) as response:  # nosec B310 |  | ||||||
|                 raw = response.read() |  | ||||||
|                 loaded = loads(raw) |  | ||||||
|                 if self.raw: |  | ||||||
|                     sys.stdout.write(dumps(loaded.get("result")) + "\n") |  | ||||||
|                     return {} |  | ||||||
|                 return loaded |  | ||||||
|         except (URLError, RemoteDisconnected) as e: |  | ||||||
|             sys.stderr.write( |  | ||||||
|                 f"{Colors.RED}Could not connect to inspector at: " |  | ||||||
|                 f"{Colors.YELLOW}{self.base_url}{Colors.END}\n" |  | ||||||
|                 "Either the application is not running, or it did not start " |  | ||||||
|                 f"an inspector instance.\n{e}\n" |  | ||||||
|             ) |  | ||||||
|             sys.exit(1) |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def base_url(self): |  | ||||||
|         return f"{self.scheme}://{self.host}:{self.port}" |  | ||||||
| @@ -3,23 +3,10 @@ import os | |||||||
| import signal | import signal | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from contextlib import contextmanager | from typing import Awaitable | ||||||
| from enum import Enum |  | ||||||
| from typing import Awaitable, Union |  | ||||||
|  |  | ||||||
| from multidict import CIMultiDict  # type: ignore | from multidict import CIMultiDict  # type: ignore | ||||||
|  |  | ||||||
| from sanic.helpers import Default |  | ||||||
|  |  | ||||||
|  |  | ||||||
| if sys.version_info < (3, 8):  # no cov |  | ||||||
|     StartMethod = Union[Default, str] |  | ||||||
| else:  # no cov |  | ||||||
|     from typing import Literal |  | ||||||
|  |  | ||||||
|     StartMethod = Union[ |  | ||||||
|         Default, Literal["fork"], Literal["forkserver"], Literal["spawn"] |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
| OS_IS_WINDOWS = os.name == "nt" | OS_IS_WINDOWS = os.name == "nt" | ||||||
| UVLOOP_INSTALLED = False | UVLOOP_INSTALLED = False | ||||||
| @@ -31,40 +18,6 @@ try: | |||||||
| except ImportError: | except ImportError: | ||||||
|     pass |     pass | ||||||
|  |  | ||||||
| # Python 3.11 changed the way Enum formatting works for mixed-in types. |  | ||||||
| if sys.version_info < (3, 11, 0): |  | ||||||
|  |  | ||||||
|     class StrEnum(str, Enum): |  | ||||||
|         pass |  | ||||||
|  |  | ||||||
| else: |  | ||||||
|     from enum import StrEnum  # type: ignore # noqa |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class UpperStrEnum(StrEnum): |  | ||||||
|     def _generate_next_value_(name, start, count, last_values): |  | ||||||
|         return name.upper() |  | ||||||
|  |  | ||||||
|     def __eq__(self, value: object) -> bool: |  | ||||||
|         value = str(value).upper() |  | ||||||
|         return super().__eq__(value) |  | ||||||
|  |  | ||||||
|     def __hash__(self) -> int: |  | ||||||
|         return hash(self.value) |  | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |  | ||||||
|         return self.value |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @contextmanager |  | ||||||
| def use_context(method: StartMethod): |  | ||||||
|     from sanic import Sanic |  | ||||||
|  |  | ||||||
|     orig = Sanic.start_method |  | ||||||
|     Sanic.start_method = method |  | ||||||
|     yield |  | ||||||
|     Sanic.start_method = orig |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def enable_windows_color_support(): | def enable_windows_color_support(): | ||||||
|     import ctypes |     import ctypes | ||||||
| @@ -88,12 +41,6 @@ class Header(CIMultiDict): | |||||||
|     very similar to a regular dictionary. |     very similar to a regular dictionary. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def __getattr__(self, key: str) -> str: |  | ||||||
|         if key.startswith("_"): |  | ||||||
|             return self.__getattribute__(key) |  | ||||||
|         key = key.rstrip("_").replace("_", "-") |  | ||||||
|         return ",".join(self.getall(key, default=[])) |  | ||||||
|  |  | ||||||
|     def get_all(self, key: str): |     def get_all(self, key: str): | ||||||
|         """ |         """ | ||||||
|         Convenience method mapped to ``getall()``. |         Convenience method mapped to ``getall()``. | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ from __future__ import annotations | |||||||
|  |  | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from abc import ABCMeta |  | ||||||
| from inspect import getmembers, isclass, isdatadescriptor | from inspect import getmembers, isclass, isdatadescriptor | ||||||
| from os import environ | from os import environ | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| @@ -13,7 +12,7 @@ from sanic.constants import LocalCertCreator | |||||||
| from sanic.errorpages import DEFAULT_FORMAT, check_error_format | from sanic.errorpages import DEFAULT_FORMAT, check_error_format | ||||||
| from sanic.helpers import Default, _default | from sanic.helpers import Default, _default | ||||||
| from sanic.http import Http | from sanic.http import Http | ||||||
| from sanic.log import error_logger | from sanic.log import deprecation, error_logger | ||||||
| from sanic.utils import load_module_from_file_location, str_to_bool | from sanic.utils import load_module_from_file_location, str_to_bool | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -47,9 +46,6 @@ DEFAULT_CONFIG = { | |||||||
|     "INSPECTOR": False, |     "INSPECTOR": False, | ||||||
|     "INSPECTOR_HOST": "localhost", |     "INSPECTOR_HOST": "localhost", | ||||||
|     "INSPECTOR_PORT": 6457, |     "INSPECTOR_PORT": 6457, | ||||||
|     "INSPECTOR_TLS_KEY": _default, |  | ||||||
|     "INSPECTOR_TLS_CERT": _default, |  | ||||||
|     "INSPECTOR_API_KEY": "", |  | ||||||
|     "KEEP_ALIVE_TIMEOUT": 5,  # 5 seconds |     "KEEP_ALIVE_TIMEOUT": 5,  # 5 seconds | ||||||
|     "KEEP_ALIVE": True, |     "KEEP_ALIVE": True, | ||||||
|     "LOCAL_CERT_CREATOR": LocalCertCreator.AUTO, |     "LOCAL_CERT_CREATOR": LocalCertCreator.AUTO, | ||||||
| @@ -75,8 +71,12 @@ DEFAULT_CONFIG = { | |||||||
|     "WEBSOCKET_PING_TIMEOUT": 20, |     "WEBSOCKET_PING_TIMEOUT": 20, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | # These values will be removed from the Config object in v22.6 and moved | ||||||
|  | # to the application state | ||||||
|  | DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES") | ||||||
|  |  | ||||||
| class DescriptorMeta(ABCMeta): |  | ||||||
|  | class DescriptorMeta(type): | ||||||
|     def __init__(cls, *_): |     def __init__(cls, *_): | ||||||
|         cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)} |         cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)} | ||||||
|  |  | ||||||
| @@ -97,9 +97,6 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|     INSPECTOR: bool |     INSPECTOR: bool | ||||||
|     INSPECTOR_HOST: str |     INSPECTOR_HOST: str | ||||||
|     INSPECTOR_PORT: int |     INSPECTOR_PORT: int | ||||||
|     INSPECTOR_TLS_KEY: Union[Path, str, Default] |  | ||||||
|     INSPECTOR_TLS_CERT: Union[Path, str, Default] |  | ||||||
|     INSPECTOR_API_KEY: str |  | ||||||
|     KEEP_ALIVE_TIMEOUT: int |     KEEP_ALIVE_TIMEOUT: int | ||||||
|     KEEP_ALIVE: bool |     KEEP_ALIVE: bool | ||||||
|     LOCAL_CERT_CREATOR: Union[str, LocalCertCreator] |     LOCAL_CERT_CREATOR: Union[str, LocalCertCreator] | ||||||
| @@ -127,9 +124,7 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         defaults: Optional[ |         defaults: Dict[str, Union[str, bool, int, float, None]] = None, | ||||||
|             Dict[str, Union[str, bool, int, float, None]] |  | ||||||
|         ] = None, |  | ||||||
|         env_prefix: Optional[str] = SANIC_PREFIX, |         env_prefix: Optional[str] = SANIC_PREFIX, | ||||||
|         keep_alive: Optional[bool] = None, |         keep_alive: Optional[bool] = None, | ||||||
|         *, |         *, | ||||||
| @@ -137,7 +132,6 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|     ): |     ): | ||||||
|         defaults = defaults or {} |         defaults = defaults or {} | ||||||
|         super().__init__({**DEFAULT_CONFIG, **defaults}) |         super().__init__({**DEFAULT_CONFIG, **defaults}) | ||||||
|         self._configure_warnings() |  | ||||||
|  |  | ||||||
|         self._converters = [str, str_to_bool, float, int] |         self._converters = [str, str_to_bool, float, int] | ||||||
|  |  | ||||||
| @@ -155,6 +149,7 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|             self.load_environment_vars(SANIC_PREFIX) |             self.load_environment_vars(SANIC_PREFIX) | ||||||
|  |  | ||||||
|         self._configure_header_size() |         self._configure_header_size() | ||||||
|  |         self._configure_warnings() | ||||||
|         self._check_error_format() |         self._check_error_format() | ||||||
|         self._init = True |         self._init = True | ||||||
|  |  | ||||||
| @@ -208,7 +203,7 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def FALLBACK_ERROR_FORMAT(self) -> str: |     def FALLBACK_ERROR_FORMAT(self) -> str: | ||||||
|         if isinstance(self._FALLBACK_ERROR_FORMAT, Default): |         if self._FALLBACK_ERROR_FORMAT is _default: | ||||||
|             return DEFAULT_FORMAT |             return DEFAULT_FORMAT | ||||||
|         return self._FALLBACK_ERROR_FORMAT |         return self._FALLBACK_ERROR_FORMAT | ||||||
|  |  | ||||||
| @@ -216,7 +211,7 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|     def FALLBACK_ERROR_FORMAT(self, value): |     def FALLBACK_ERROR_FORMAT(self, value): | ||||||
|         self._check_error_format(value) |         self._check_error_format(value) | ||||||
|         if ( |         if ( | ||||||
|             not isinstance(self._FALLBACK_ERROR_FORMAT, Default) |             self._FALLBACK_ERROR_FORMAT is not _default | ||||||
|             and value != self._FALLBACK_ERROR_FORMAT |             and value != self._FALLBACK_ERROR_FORMAT | ||||||
|         ): |         ): | ||||||
|             error_logger.warning( |             error_logger.warning( | ||||||
| @@ -246,9 +241,7 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|         """ |         """ | ||||||
|         Looks for prefixed environment variables and applies them to the |         Looks for prefixed environment variables and applies them to the | ||||||
|         configuration if present. This is called automatically when Sanic |         configuration if present. This is called automatically when Sanic | ||||||
|         starts up to load environment variables into config. Environment |         starts up to load environment variables into config. | ||||||
|         variables should start with the defined prefix and should only |  | ||||||
|         contain uppercase letters. |  | ||||||
|  |  | ||||||
|         It will automatically hydrate the following types: |         It will automatically hydrate the following types: | ||||||
|  |  | ||||||
| @@ -274,9 +267,12 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|         `See user guide re: config |         `See user guide re: config | ||||||
|         <https://sanicframework.org/guide/deployment/configuration.html>`__ |         <https://sanicframework.org/guide/deployment/configuration.html>`__ | ||||||
|         """ |         """ | ||||||
|  |         lower_case_var_found = False | ||||||
|         for key, value in environ.items(): |         for key, value in environ.items(): | ||||||
|             if not key.startswith(prefix) or not key.isupper(): |             if not key.startswith(prefix): | ||||||
|                 continue |                 continue | ||||||
|  |             if not key.isupper(): | ||||||
|  |                 lower_case_var_found = True | ||||||
|  |  | ||||||
|             _, config_key = key.split(prefix, 1) |             _, config_key = key.split(prefix, 1) | ||||||
|  |  | ||||||
| @@ -286,6 +282,12 @@ class Config(dict, metaclass=DescriptorMeta): | |||||||
|                     break |                     break | ||||||
|                 except ValueError: |                 except ValueError: | ||||||
|                     pass |                     pass | ||||||
|  |         if lower_case_var_found: | ||||||
|  |             deprecation( | ||||||
|  |                 "Lowercase environment variables will not be " | ||||||
|  |                 "loaded into Sanic config beginning in v22.9.", | ||||||
|  |                 22.9, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|     def update_config(self, config: Union[bytes, str, dict, Any]): |     def update_config(self, config: Union[bytes, str, dict, Any]): | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -1,9 +1,20 @@ | |||||||
| from enum import auto | from enum import Enum, auto | ||||||
|  |  | ||||||
| from sanic.compat import UpperStrEnum |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class HTTPMethod(UpperStrEnum): | class HTTPMethod(str, Enum): | ||||||
|  |     def _generate_next_value_(name, start, count, last_values): | ||||||
|  |         return name.upper() | ||||||
|  |  | ||||||
|  |     def __eq__(self, value: object) -> bool: | ||||||
|  |         value = str(value).upper() | ||||||
|  |         return super().__eq__(value) | ||||||
|  |  | ||||||
|  |     def __hash__(self) -> int: | ||||||
|  |         return hash(self.value) | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return self.value | ||||||
|  |  | ||||||
|     GET = auto() |     GET = auto() | ||||||
|     POST = auto() |     POST = auto() | ||||||
|     PUT = auto() |     PUT = auto() | ||||||
| @@ -13,7 +24,10 @@ class HTTPMethod(UpperStrEnum): | |||||||
|     DELETE = auto() |     DELETE = auto() | ||||||
|  |  | ||||||
|  |  | ||||||
| class LocalCertCreator(UpperStrEnum): | class LocalCertCreator(str, Enum): | ||||||
|  |     def _generate_next_value_(name, start, count, last_values): | ||||||
|  |         return name.upper() | ||||||
|  |  | ||||||
|     AUTO = auto() |     AUTO = auto() | ||||||
|     TRUSTME = auto() |     TRUSTME = auto() | ||||||
|     MKCERT = auto() |     MKCERT = auto() | ||||||
|   | |||||||
| @@ -12,7 +12,6 @@ Setting ``app.config.FALLBACK_ERROR_FORMAT = "auto"`` will enable a switch that | |||||||
| will attempt to provide an appropriate response format based upon the | will attempt to provide an appropriate response format based upon the | ||||||
| request type. | request type. | ||||||
| """ | """ | ||||||
| from __future__ import annotations |  | ||||||
|  |  | ||||||
| import sys | import sys | ||||||
| import typing as t | import typing as t | ||||||
| @@ -22,9 +21,8 @@ from traceback import extract_tb | |||||||
|  |  | ||||||
| from sanic.exceptions import BadRequest, SanicException | from sanic.exceptions import BadRequest, SanicException | ||||||
| from sanic.helpers import STATUS_CODES | from sanic.helpers import STATUS_CODES | ||||||
| from sanic.log import deprecation, logger | from sanic.request import Request | ||||||
| from sanic.pages.error import ErrorPage | from sanic.response import HTTPResponse, html, json, text | ||||||
| from sanic.response import html, json, text |  | ||||||
|  |  | ||||||
|  |  | ||||||
| dumps: t.Callable[..., str] | dumps: t.Callable[..., str] | ||||||
| @@ -35,15 +33,13 @@ try: | |||||||
| except ImportError:  # noqa | except ImportError:  # noqa | ||||||
|     from json import dumps |     from json import dumps | ||||||
|  |  | ||||||
| if t.TYPE_CHECKING: |  | ||||||
|     from sanic import HTTPResponse, Request |  | ||||||
|  |  | ||||||
| DEFAULT_FORMAT = "auto" | DEFAULT_FORMAT = "auto" | ||||||
| FALLBACK_TEXT = """\ | FALLBACK_TEXT = ( | ||||||
| The application encountered an unexpected error and could not continue.\ |     "The server encountered an internal error and " | ||||||
| """ |     "cannot complete your request." | ||||||
|  | ) | ||||||
| FALLBACK_STATUS = 500 | FALLBACK_STATUS = 500 | ||||||
| JSON = "application/json" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class BaseRenderer: | class BaseRenderer: | ||||||
| @@ -117,18 +113,134 @@ class HTMLRenderer(BaseRenderer): | |||||||
|     The default fallback type. |     The default fallback type. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def full(self) -> HTTPResponse: |     TRACEBACK_STYLE = """ | ||||||
|         page = ErrorPage( |         html { font-family: sans-serif } | ||||||
|             debug=self.debug, |         h2 { color: #888; } | ||||||
|             title=super().title, |         .tb-wrapper p, dl, dd { margin: 0 } | ||||||
|             text=super().text, |         .frame-border { margin: 1rem } | ||||||
|             request=self.request, |         .frame-line > *, dt, dd { padding: 0.3rem 0.6rem } | ||||||
|             exc=self.exception, |         .frame-line, dl { margin-bottom: 0.3rem } | ||||||
|  |         .frame-code, dd { font-size: 16px; padding-left: 4ch } | ||||||
|  |         .tb-wrapper, dl { border: 1px solid #eee } | ||||||
|  |         .tb-header,.obj-header { | ||||||
|  |             background: #eee; padding: 0.3rem; font-weight: bold | ||||||
|  |         } | ||||||
|  |         .frame-descriptor, dt { background: #e2eafb; font-size: 14px } | ||||||
|  |     """ | ||||||
|  |     TRACEBACK_WRAPPER_HTML = ( | ||||||
|  |         "<div class=tb-header>{exc_name}: {exc_value}</div>" | ||||||
|  |         "<div class=tb-wrapper>{frame_html}</div>" | ||||||
|  |     ) | ||||||
|  |     TRACEBACK_BORDER = ( | ||||||
|  |         "<div class=frame-border>" | ||||||
|  |         "The above exception was the direct cause of the following exception:" | ||||||
|  |         "</div>" | ||||||
|  |     ) | ||||||
|  |     TRACEBACK_LINE_HTML = ( | ||||||
|  |         "<div class=frame-line>" | ||||||
|  |         "<p class=frame-descriptor>" | ||||||
|  |         "File {0.filename}, line <i>{0.lineno}</i>, " | ||||||
|  |         "in <code><b>{0.name}</b></code>" | ||||||
|  |         "<p class=frame-code><code>{0.line}</code>" | ||||||
|  |         "</div>" | ||||||
|  |     ) | ||||||
|  |     OBJECT_WRAPPER_HTML = ( | ||||||
|  |         "<div class=obj-header>{title}</div>" | ||||||
|  |         "<dl class={obj_type}>{display_html}</dl>" | ||||||
|  |     ) | ||||||
|  |     OBJECT_DISPLAY_HTML = "<dt>{key}</dt><dd><code>{value}</code></dd>" | ||||||
|  |     OUTPUT_HTML = ( | ||||||
|  |         "<!DOCTYPE html><html lang=en>" | ||||||
|  |         "<meta charset=UTF-8><title>{title}</title>\n" | ||||||
|  |         "<style>{style}</style>\n" | ||||||
|  |         "<h1>{title}</h1><p>{text}\n" | ||||||
|  |         "{body}" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     def full(self) -> HTTPResponse: | ||||||
|  |         return html( | ||||||
|  |             self.OUTPUT_HTML.format( | ||||||
|  |                 title=self.title, | ||||||
|  |                 text=self.text, | ||||||
|  |                 style=self.TRACEBACK_STYLE, | ||||||
|  |                 body=self._generate_body(full=True), | ||||||
|  |             ), | ||||||
|  |             status=self.status, | ||||||
|         ) |         ) | ||||||
|         return html(page.render(), status=self.status, headers=self.headers) |  | ||||||
|  |  | ||||||
|     def minimal(self) -> HTTPResponse: |     def minimal(self) -> HTTPResponse: | ||||||
|         return self.full() |         return html( | ||||||
|  |             self.OUTPUT_HTML.format( | ||||||
|  |                 title=self.title, | ||||||
|  |                 text=self.text, | ||||||
|  |                 style=self.TRACEBACK_STYLE, | ||||||
|  |                 body=self._generate_body(full=False), | ||||||
|  |             ), | ||||||
|  |             status=self.status, | ||||||
|  |             headers=self.headers, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def text(self): | ||||||
|  |         return escape(super().text) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def title(self): | ||||||
|  |         return escape(f"⚠️ {super().title}") | ||||||
|  |  | ||||||
|  |     def _generate_body(self, *, full): | ||||||
|  |         lines = [] | ||||||
|  |         if full: | ||||||
|  |             _, exc_value, __ = sys.exc_info() | ||||||
|  |             exceptions = [] | ||||||
|  |             while exc_value: | ||||||
|  |                 exceptions.append(self._format_exc(exc_value)) | ||||||
|  |                 exc_value = exc_value.__cause__ | ||||||
|  |  | ||||||
|  |             traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions)) | ||||||
|  |             appname = escape(self.request.app.name) | ||||||
|  |             name = escape(self.exception.__class__.__name__) | ||||||
|  |             value = escape(self.exception) | ||||||
|  |             path = escape(self.request.path) | ||||||
|  |             lines += [ | ||||||
|  |                 f"<h2>Traceback of {appname} " "(most recent call last):</h2>", | ||||||
|  |                 f"{traceback_html}", | ||||||
|  |                 "<div class=summary><p>", | ||||||
|  |                 f"<b>{name}: {value}</b> " | ||||||
|  |                 f"while handling path <code>{path}</code>", | ||||||
|  |                 "</div>", | ||||||
|  |             ] | ||||||
|  |  | ||||||
|  |         for attr, display in (("context", True), ("extra", bool(full))): | ||||||
|  |             info = getattr(self.exception, attr, None) | ||||||
|  |             if info and display: | ||||||
|  |                 lines.append(self._generate_object_display(info, attr)) | ||||||
|  |  | ||||||
|  |         return "\n".join(lines) | ||||||
|  |  | ||||||
|  |     def _generate_object_display( | ||||||
|  |         self, obj: t.Dict[str, t.Any], descriptor: str | ||||||
|  |     ) -> str: | ||||||
|  |         display = "".join( | ||||||
|  |             self.OBJECT_DISPLAY_HTML.format(key=key, value=value) | ||||||
|  |             for key, value in obj.items() | ||||||
|  |         ) | ||||||
|  |         return self.OBJECT_WRAPPER_HTML.format( | ||||||
|  |             title=descriptor.title(), | ||||||
|  |             display_html=display, | ||||||
|  |             obj_type=descriptor.lower(), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def _format_exc(self, exc): | ||||||
|  |         frames = extract_tb(exc.__traceback__) | ||||||
|  |         frame_html = "".join( | ||||||
|  |             self.TRACEBACK_LINE_HTML.format(frame) for frame in frames | ||||||
|  |         ) | ||||||
|  |         return self.TRACEBACK_WRAPPER_HTML.format( | ||||||
|  |             exc_name=escape(exc.__class__.__name__), | ||||||
|  |             exc_value=escape(exc), | ||||||
|  |             frame_html=frame_html, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class TextRenderer(BaseRenderer): | class TextRenderer(BaseRenderer): | ||||||
| @@ -276,26 +388,32 @@ def escape(text): | |||||||
|     return f"{text}".replace("&", "&").replace("<", "<") |     return f"{text}".replace("&", "&").replace("<", "<") | ||||||
|  |  | ||||||
|  |  | ||||||
| MIME_BY_CONFIG = { | RENDERERS_BY_CONFIG = { | ||||||
|     "text": "text/plain", |     "html": HTMLRenderer, | ||||||
|     "json": "application/json", |     "json": JSONRenderer, | ||||||
|     "html": "text/html", |     "text": TextRenderer, | ||||||
| } | } | ||||||
| CONFIG_BY_MIME = {v: k for k, v in MIME_BY_CONFIG.items()} |  | ||||||
| RENDERERS_BY_CONTENT_TYPE = { | RENDERERS_BY_CONTENT_TYPE = { | ||||||
|     "text/plain": TextRenderer, |     "text/plain": TextRenderer, | ||||||
|     "application/json": JSONRenderer, |     "application/json": JSONRenderer, | ||||||
|     "multipart/form-data": HTMLRenderer, |     "multipart/form-data": HTMLRenderer, | ||||||
|     "text/html": HTMLRenderer, |     "text/html": HTMLRenderer, | ||||||
| } | } | ||||||
|  | CONTENT_TYPE_BY_RENDERERS = { | ||||||
|  |     v: k for k, v in RENDERERS_BY_CONTENT_TYPE.items() | ||||||
|  | } | ||||||
|  |  | ||||||
| # Handler source code is checked for which response types it returns with the |  | ||||||
| # route error_format="auto" (default) to determine which format to use. |  | ||||||
| RESPONSE_MAPPING = { | RESPONSE_MAPPING = { | ||||||
|  |     "empty": "html", | ||||||
|     "json": "json", |     "json": "json", | ||||||
|     "text": "text", |     "text": "text", | ||||||
|  |     "raw": "text", | ||||||
|     "html": "html", |     "html": "html", | ||||||
|     "JSONResponse": "json", |     "file": "html", | ||||||
|  |     "file_stream": "text", | ||||||
|  |     "stream": "text", | ||||||
|  |     "redirect": "html", | ||||||
|     "text/plain": "text", |     "text/plain": "text", | ||||||
|     "text/html": "html", |     "text/html": "html", | ||||||
|     "application/json": "json", |     "application/json": "json", | ||||||
| @@ -303,7 +421,7 @@ RESPONSE_MAPPING = { | |||||||
|  |  | ||||||
|  |  | ||||||
| def check_error_format(format): | def check_error_format(format): | ||||||
|     if format not in MIME_BY_CONFIG and format != "auto": |     if format not in RENDERERS_BY_CONFIG and format != "auto": | ||||||
|         raise SanicException(f"Unknown format: {format}") |         raise SanicException(f"Unknown format: {format}") | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -318,68 +436,98 @@ def exception_response( | |||||||
|     """ |     """ | ||||||
|     Render a response for the default FALLBACK exception handler. |     Render a response for the default FALLBACK exception handler. | ||||||
|     """ |     """ | ||||||
|  |     content_type = None | ||||||
|  |  | ||||||
|     if not renderer: |     if not renderer: | ||||||
|         mt = guess_mime(request, fallback) |         # Make sure we have something set | ||||||
|         renderer = RENDERERS_BY_CONTENT_TYPE.get(mt, base) |         renderer = base | ||||||
|  |         render_format = fallback | ||||||
|  |  | ||||||
|  |         if request: | ||||||
|  |             # If there is a request, try and get the format | ||||||
|  |             # from the route | ||||||
|  |             if request.route: | ||||||
|  |                 try: | ||||||
|  |                     if request.route.ctx.error_format: | ||||||
|  |                         render_format = request.route.ctx.error_format | ||||||
|  |                 except AttributeError: | ||||||
|  |                     ... | ||||||
|  |  | ||||||
|  |             content_type = request.headers.getone("content-type", "").split( | ||||||
|  |                 ";" | ||||||
|  |             )[0] | ||||||
|  |  | ||||||
|  |             acceptable = request.accept | ||||||
|  |  | ||||||
|  |             # If the format is auto still, make a guess | ||||||
|  |             if render_format == "auto": | ||||||
|  |                 # First, if there is an Accept header, check if text/html | ||||||
|  |                 # is the first option | ||||||
|  |                 # According to MDN Web Docs, all major browsers use text/html | ||||||
|  |                 # as the primary value in Accept (with the exception of IE 8, | ||||||
|  |                 # and, well, if you are supporting IE 8, then you have bigger | ||||||
|  |                 # problems to concern yourself with than what default exception | ||||||
|  |                 # renderer is used) | ||||||
|  |                 # Source: | ||||||
|  |                 # https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values | ||||||
|  |  | ||||||
|  |                 if acceptable and acceptable[0].match( | ||||||
|  |                     "text/html", | ||||||
|  |                     allow_type_wildcard=False, | ||||||
|  |                     allow_subtype_wildcard=False, | ||||||
|  |                 ): | ||||||
|  |                     renderer = HTMLRenderer | ||||||
|  |  | ||||||
|  |                 # Second, if there is an Accept header, check if | ||||||
|  |                 # application/json is an option, or if the content-type | ||||||
|  |                 # is application/json | ||||||
|  |                 elif ( | ||||||
|  |                     acceptable | ||||||
|  |                     and acceptable.match( | ||||||
|  |                         "application/json", | ||||||
|  |                         allow_type_wildcard=False, | ||||||
|  |                         allow_subtype_wildcard=False, | ||||||
|  |                     ) | ||||||
|  |                     or content_type == "application/json" | ||||||
|  |                 ): | ||||||
|  |                     renderer = JSONRenderer | ||||||
|  |  | ||||||
|  |                 # Third, if there is no Accept header, assume we want text. | ||||||
|  |                 # The likely use case here is a raw socket. | ||||||
|  |                 elif not acceptable: | ||||||
|  |                     renderer = TextRenderer | ||||||
|  |                 else: | ||||||
|  |                     # Fourth, look to see if there was a JSON body | ||||||
|  |                     # When in this situation, the request is probably coming | ||||||
|  |                     # from curl, an API client like Postman or Insomnia, or a | ||||||
|  |                     # package like requests or httpx | ||||||
|  |                     try: | ||||||
|  |                         # Give them the benefit of the doubt if they did: | ||||||
|  |                         # $ curl localhost:8000 -d '{"foo": "bar"}' | ||||||
|  |                         # And provide them with JSONRenderer | ||||||
|  |                         renderer = JSONRenderer if request.json else base | ||||||
|  |                     except BadRequest: | ||||||
|  |                         renderer = base | ||||||
|  |             else: | ||||||
|  |                 renderer = RENDERERS_BY_CONFIG.get(render_format, renderer) | ||||||
|  |  | ||||||
|  |             # Lastly, if there is an Accept header, make sure | ||||||
|  |             # our choice is okay | ||||||
|  |             if acceptable: | ||||||
|  |                 type_ = CONTENT_TYPE_BY_RENDERERS.get(renderer)  # type: ignore | ||||||
|  |                 if type_ and type_ not in acceptable: | ||||||
|  |                     # If the renderer selected is not in the Accept header | ||||||
|  |                     # look through what is in the Accept header, and select | ||||||
|  |                     # the first option that matches. Otherwise, just drop back | ||||||
|  |                     # to the original default | ||||||
|  |                     for accept in acceptable: | ||||||
|  |                         mtype = f"{accept.type_}/{accept.subtype}" | ||||||
|  |                         maybe = RENDERERS_BY_CONTENT_TYPE.get(mtype) | ||||||
|  |                         if maybe: | ||||||
|  |                             renderer = maybe | ||||||
|  |                             break | ||||||
|  |                     else: | ||||||
|  |                         renderer = base | ||||||
|  |  | ||||||
|     renderer = t.cast(t.Type[BaseRenderer], renderer) |     renderer = t.cast(t.Type[BaseRenderer], renderer) | ||||||
|     return renderer(request, exception, debug).render() |     return renderer(request, exception, debug).render() | ||||||
|  |  | ||||||
|  |  | ||||||
| def guess_mime(req: Request, fallback: str) -> str: |  | ||||||
|     # Attempt to find a suitable MIME format for the response. |  | ||||||
|     # Insertion-ordered map of formats["html"] = "source of that suggestion" |  | ||||||
|     formats = {} |  | ||||||
|     name = "" |  | ||||||
|     # Route error_format (by magic from handler code if auto, the default) |  | ||||||
|     if req.route: |  | ||||||
|         name = req.route.name |  | ||||||
|         f = req.route.extra.error_format |  | ||||||
|         if f in MIME_BY_CONFIG: |  | ||||||
|             formats[f] = name |  | ||||||
|  |  | ||||||
|     if not formats and fallback in MIME_BY_CONFIG: |  | ||||||
|         formats[fallback] = "FALLBACK_ERROR_FORMAT" |  | ||||||
|  |  | ||||||
|     # If still not known, check for the request for clues of JSON |  | ||||||
|     if not formats and fallback == "auto" and req.accept.match(JSON): |  | ||||||
|         if JSON in req.accept:  # Literally, not wildcard |  | ||||||
|             formats["json"] = "request.accept" |  | ||||||
|         elif JSON in req.headers.getone("content-type", ""): |  | ||||||
|             formats["json"] = "content-type" |  | ||||||
|         # DEPRECATION: Remove this block in 24.3 |  | ||||||
|         else: |  | ||||||
|             c = None |  | ||||||
|             try: |  | ||||||
|                 c = req.json |  | ||||||
|             except BadRequest: |  | ||||||
|                 pass |  | ||||||
|             if c: |  | ||||||
|                 formats["json"] = "request.json" |  | ||||||
|                 deprecation( |  | ||||||
|                     "Response type was determined by the JSON content of " |  | ||||||
|                     "the request. This behavior is deprecated and will be " |  | ||||||
|                     "removed in v24.3. Please specify the format either by\n" |  | ||||||
|                     f'  error_format="json" on route {name}, by\n' |  | ||||||
|                     '  FALLBACK_ERROR_FORMAT = "json", or by adding header\n' |  | ||||||
|                     "  accept: application/json to your requests.", |  | ||||||
|                     24.3, |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|     # Any other supported formats |  | ||||||
|     if fallback == "auto": |  | ||||||
|         for k in MIME_BY_CONFIG: |  | ||||||
|             if k not in formats: |  | ||||||
|                 formats[k] = "any" |  | ||||||
|  |  | ||||||
|     mimes = [MIME_BY_CONFIG[k] for k in formats] |  | ||||||
|     m = req.accept.match(*mimes) |  | ||||||
|     if m: |  | ||||||
|         format = CONFIG_BY_MIME[m.mime] |  | ||||||
|         source = formats[format] |  | ||||||
|         logger.debug( |  | ||||||
|             f"The client accepts {m.header}, using '{format}' from {source}" |  | ||||||
|         ) |  | ||||||
|     else: |  | ||||||
|         logger.debug(f"No format found, the client accepts {req.accept!r}") |  | ||||||
|     return m.mime |  | ||||||
|   | |||||||
| @@ -8,10 +8,6 @@ class RequestCancelled(CancelledError): | |||||||
|     quiet = True |     quiet = True | ||||||
|  |  | ||||||
|  |  | ||||||
| class ServerKilled(Exception): |  | ||||||
|     ... |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SanicException(Exception): | class SanicException(Exception): | ||||||
|     message: str = "" |     message: str = "" | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										578
									
								
								sanic/handlers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										578
									
								
								sanic/handlers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,578 @@ | |||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | from functools import partial | ||||||
|  | from inspect import isawaitable | ||||||
|  | from traceback import format_exc | ||||||
|  | from typing import Dict, List, Optional, Tuple, Type | ||||||
|  |  | ||||||
|  | from sanic_routing import Route | ||||||
|  |  | ||||||
|  | from sanic.errorpages import BaseRenderer, TextRenderer, exception_response | ||||||
|  | from sanic.exceptions import ( | ||||||
|  |     HeaderNotFound, | ||||||
|  |     InvalidRangeType, | ||||||
|  |     RangeNotSatisfiable, | ||||||
|  |     SanicException, | ||||||
|  |     ServerError, | ||||||
|  | ) | ||||||
|  | from sanic.http.constants import Stage | ||||||
|  | from sanic.log import deprecation, error_logger, logger | ||||||
|  | from sanic.models.handler_types import RouteHandler | ||||||
|  | from sanic.request import Request | ||||||
|  | from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream, text | ||||||
|  | from sanic.touchup import TouchUpMeta | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RequestHandler: | ||||||
|  |     def __init__(self, func, request_middleware, response_middleware): | ||||||
|  |         self.func = func.func if isinstance(func, RequestHandler) else func | ||||||
|  |         self.request_middleware = request_middleware | ||||||
|  |         self.response_middleware = response_middleware | ||||||
|  |  | ||||||
|  |     def __call__(self, *args, **kwargs): | ||||||
|  |         return self.func(*args, **kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RequestManager(metaclass=TouchUpMeta): | ||||||
|  |     __touchup__ = ( | ||||||
|  |         "cleanup", | ||||||
|  |         "run_request_middleware", | ||||||
|  |         "run_response_middleware", | ||||||
|  |     ) | ||||||
|  |     __slots__ = ( | ||||||
|  |         "handler", | ||||||
|  |         "request_middleware_run", | ||||||
|  |         "request_middleware", | ||||||
|  |         "request", | ||||||
|  |         "response_middleware_run", | ||||||
|  |         "response_middleware", | ||||||
|  |     ) | ||||||
|  |     request: Request | ||||||
|  |  | ||||||
|  |     def __init__(self, request: Request): | ||||||
|  |         self.request_middleware_run = False | ||||||
|  |         self.response_middleware_run = False | ||||||
|  |         self.handler = self._noop | ||||||
|  |         self.set_request(request) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def create(cls, request: Request) -> RequestManager: | ||||||
|  |         return cls(request) | ||||||
|  |  | ||||||
|  |     def set_request(self, request: Request): | ||||||
|  |         request._manager = self | ||||||
|  |         self.request = request | ||||||
|  |         self.request_middleware = request.app.request_middleware | ||||||
|  |         self.response_middleware = request.app.response_middleware | ||||||
|  |  | ||||||
|  |     async def handle(self): | ||||||
|  |         route = self.resolve_route() | ||||||
|  |  | ||||||
|  |         if self.handler is None: | ||||||
|  |             await self.error( | ||||||
|  |                 ServerError( | ||||||
|  |                     ( | ||||||
|  |                         "'None' was returned while requesting a " | ||||||
|  |                         "handler from the router" | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         if ( | ||||||
|  |             self.request.stream | ||||||
|  |             and self.request.stream.request_body | ||||||
|  |             and not route.ctx.ignore_body | ||||||
|  |         ): | ||||||
|  |             await self.receive_body() | ||||||
|  |  | ||||||
|  |         await self.lifecycle( | ||||||
|  |             partial(self.handler, self.request, **self.request.match_info) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     async def lifecycle(self, handler, raise_exception: bool = False): | ||||||
|  |         response: Optional[BaseHTTPResponse] = None | ||||||
|  |         if not self.request_middleware_run and self.request_middleware: | ||||||
|  |             response = await self.run( | ||||||
|  |                 self.run_request_middleware, raise_exception | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         if not response: | ||||||
|  |             # Run response handler | ||||||
|  |             response = await self.run(handler, raise_exception) | ||||||
|  |  | ||||||
|  |         if not self.response_middleware_run and self.response_middleware: | ||||||
|  |             response = await self.run( | ||||||
|  |                 partial(self.run_response_middleware, response), | ||||||
|  |                 raise_exception, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         await self.cleanup(response) | ||||||
|  |  | ||||||
|  |     async def run( | ||||||
|  |         self, operation, raise_exception: bool = False | ||||||
|  |     ) -> Optional[BaseHTTPResponse]: | ||||||
|  |         try: | ||||||
|  |             response = operation() | ||||||
|  |             if isawaitable(response): | ||||||
|  |                 response = await response | ||||||
|  |         except Exception as e: | ||||||
|  |             if raise_exception: | ||||||
|  |                 raise | ||||||
|  |             response = await self.error(e) | ||||||
|  |         return response | ||||||
|  |  | ||||||
|  |     async def error(self, exception: Exception): | ||||||
|  |         error_handler = self.request.app.error_handler | ||||||
|  |         if ( | ||||||
|  |             self.request.stream is not None | ||||||
|  |             and self.request.stream.stage is not Stage.HANDLER | ||||||
|  |         ): | ||||||
|  |             error_logger.exception(exception, exc_info=True) | ||||||
|  |             logger.error( | ||||||
|  |                 "The error response will not be sent to the client for " | ||||||
|  |                 f'the following exception:"{exception}". A previous response ' | ||||||
|  |                 "has at least partially been sent." | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             handler = error_handler._lookup( | ||||||
|  |                 exception, self.request.name if self.request else None | ||||||
|  |             ) | ||||||
|  |             if handler: | ||||||
|  |                 logger.warning( | ||||||
|  |                     "An error occurred while handling the request after at " | ||||||
|  |                     "least some part of the response was sent to the client. " | ||||||
|  |                     "The response from your custom exception handler " | ||||||
|  |                     f"{handler.__name__} will not be sent to the client." | ||||||
|  |                     "Exception handlers should only be used to generate the " | ||||||
|  |                     "exception responses. If you would like to perform any " | ||||||
|  |                     "other action on a raised exception, consider using a " | ||||||
|  |                     "signal handler like " | ||||||
|  |                     '`@app.signal("http.lifecycle.exception")`\n' | ||||||
|  |                     "For further information, please see the docs: " | ||||||
|  |                     "https://sanicframework.org/en/guide/advanced/" | ||||||
|  |                     "signals.html", | ||||||
|  |                 ) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             await self.lifecycle( | ||||||
|  |                 partial(error_handler.response, self.request, exception), True | ||||||
|  |             ) | ||||||
|  |         except Exception as e: | ||||||
|  |             if isinstance(e, SanicException): | ||||||
|  |                 response = error_handler.default(self.request, e) | ||||||
|  |             elif self.request.app.debug: | ||||||
|  |                 response = HTTPResponse( | ||||||
|  |                     ( | ||||||
|  |                         f"Error while handling error: {e}\n" | ||||||
|  |                         f"Stack: {format_exc()}" | ||||||
|  |                     ), | ||||||
|  |                     status=500, | ||||||
|  |                 ) | ||||||
|  |             else: | ||||||
|  |                 error_logger.exception(e) | ||||||
|  |                 response = HTTPResponse( | ||||||
|  |                     "An error occurred while handling an error", status=500 | ||||||
|  |                 ) | ||||||
|  |             return response | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     async def cleanup(self, response: Optional[BaseHTTPResponse]): | ||||||
|  |         if self.request.responded: | ||||||
|  |             if response is not None: | ||||||
|  |                 error_logger.error( | ||||||
|  |                     "The response object returned by the route handler " | ||||||
|  |                     "will not be sent to client. The request has already " | ||||||
|  |                     "been responded to." | ||||||
|  |                 ) | ||||||
|  |             if self.request.stream is not None: | ||||||
|  |                 response = self.request.stream.response | ||||||
|  |         elif response is not None: | ||||||
|  |             self.request.reset_response() | ||||||
|  |             response = await self.request.respond(response)  # type: ignore | ||||||
|  |         elif not hasattr(self.handler, "is_websocket"): | ||||||
|  |             response = self.request.stream.response  # type: ignore | ||||||
|  |  | ||||||
|  |         if isinstance(response, BaseHTTPResponse): | ||||||
|  |             await self.request.app.dispatch( | ||||||
|  |                 "http.lifecycle.response", | ||||||
|  |                 inline=True, | ||||||
|  |                 context={"request": self.request, "response": response}, | ||||||
|  |             ) | ||||||
|  |             await response.send(end_stream=True) | ||||||
|  |         elif isinstance(response, ResponseStream): | ||||||
|  |             await response(self.request)  # type: ignore | ||||||
|  |             await response.eof()  # type: ignore | ||||||
|  |             await self.request.app.dispatch( | ||||||
|  |                 "http.lifecycle.response", | ||||||
|  |                 inline=True, | ||||||
|  |                 context={"request": self.request, "response": response}, | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|  |             if not hasattr(self.handler, "is_websocket"): | ||||||
|  |                 raise ServerError( | ||||||
|  |                     f"Invalid response type {response!r} " | ||||||
|  |                     "(need HTTPResponse)" | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |     async def receive_body(self): | ||||||
|  |         if hasattr(self.handler, "is_stream"): | ||||||
|  |             # Streaming handler: lift the size limit | ||||||
|  |             self.request.stream.request_max_size = float("inf") | ||||||
|  |         else: | ||||||
|  |             # Non-streaming handler: preload body | ||||||
|  |             await self.request.receive_body() | ||||||
|  |  | ||||||
|  |     async def run_request_middleware(self) -> Optional[BaseHTTPResponse]: | ||||||
|  |         self.request._request_middleware_started = True | ||||||
|  |         self.request_middleware_run = True | ||||||
|  |  | ||||||
|  |         for middleware in self.request_middleware: | ||||||
|  |             await self.request.app.dispatch( | ||||||
|  |                 "http.middleware.before", | ||||||
|  |                 inline=True, | ||||||
|  |                 context={"request": self.request, "response": None}, | ||||||
|  |                 condition={"attach_to": "request"}, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             try: | ||||||
|  |                 response = await self.run(partial(middleware, self.request)) | ||||||
|  |             except Exception: | ||||||
|  |                 error_logger.exception( | ||||||
|  |                     "Exception occurred in one of request middleware handlers" | ||||||
|  |                 ) | ||||||
|  |                 raise | ||||||
|  |  | ||||||
|  |             await self.request.app.dispatch( | ||||||
|  |                 "http.middleware.after", | ||||||
|  |                 inline=True, | ||||||
|  |                 context={"request": self.request, "response": None}, | ||||||
|  |                 condition={"attach_to": "request"}, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             if response: | ||||||
|  |                 return response | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     async def run_response_middleware( | ||||||
|  |         self, response: BaseHTTPResponse | ||||||
|  |     ) -> BaseHTTPResponse: | ||||||
|  |         self.response_middleware_run = True | ||||||
|  |         for middleware in self.response_middleware: | ||||||
|  |             await self.request.app.dispatch( | ||||||
|  |                 "http.middleware.before", | ||||||
|  |                 inline=True, | ||||||
|  |                 context={"request": self.request, "response": None}, | ||||||
|  |                 condition={"attach_to": "request"}, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             try: | ||||||
|  |                 resp = await self.run( | ||||||
|  |                     partial(middleware, self.request, response), True | ||||||
|  |                 ) | ||||||
|  |             except Exception as e: | ||||||
|  |                 error_logger.exception( | ||||||
|  |                     "Exception occurred in one of response middleware handlers" | ||||||
|  |                 ) | ||||||
|  |                 await self.error(e) | ||||||
|  |                 resp = None | ||||||
|  |  | ||||||
|  |             await self.request.app.dispatch( | ||||||
|  |                 "http.middleware.after", | ||||||
|  |                 inline=True, | ||||||
|  |                 context={"request": self.request, "response": None}, | ||||||
|  |                 condition={"attach_to": "request"}, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             if resp: | ||||||
|  |                 return resp | ||||||
|  |         return response | ||||||
|  |  | ||||||
|  |     def resolve_route(self) -> Route: | ||||||
|  |         # Fetch handler from router | ||||||
|  |         route, handler, kwargs = self.request.app.router.get( | ||||||
|  |             self.request.path, | ||||||
|  |             self.request.method, | ||||||
|  |             self.request.headers.getone("host", None), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.request._match_info = {**kwargs} | ||||||
|  |         self.request.route = route | ||||||
|  |         self.handler = handler | ||||||
|  |  | ||||||
|  |         if handler and handler.request_middleware: | ||||||
|  |             self.request_middleware = handler.request_middleware | ||||||
|  |  | ||||||
|  |         if handler and handler.response_middleware: | ||||||
|  |             self.response_middleware = handler.response_middleware | ||||||
|  |  | ||||||
|  |         return route | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def _noop(_): | ||||||
|  |         ... | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ErrorHandler: | ||||||
|  |     """ | ||||||
|  |     Provide :class:`sanic.app.Sanic` application with a mechanism to handle | ||||||
|  |     and process any and all uncaught exceptions in a way the application | ||||||
|  |     developer will set fit. | ||||||
|  |  | ||||||
|  |     This error handling framework is built into the core that can be extended | ||||||
|  |     by the developers to perform a wide range of tasks from recording the error | ||||||
|  |     stats to reporting them to an external service that can be used for | ||||||
|  |     realtime alerting system. | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         base: Type[BaseRenderer] = TextRenderer, | ||||||
|  |     ): | ||||||
|  |         self.cached_handlers: Dict[ | ||||||
|  |             Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler] | ||||||
|  |         ] = {} | ||||||
|  |         self.debug = False | ||||||
|  |         self.base = base | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def finalize(cls, *args, **kwargs): | ||||||
|  |         deprecation( | ||||||
|  |             "ErrorHandler.finalize is deprecated and no longer needed. " | ||||||
|  |             "Please remove update your code to remove it. ", | ||||||
|  |             22.12, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def _full_lookup(self, exception, route_name: Optional[str] = None): | ||||||
|  |         return self.lookup(exception, route_name) | ||||||
|  |  | ||||||
|  |     def _add( | ||||||
|  |         self, | ||||||
|  |         key: Tuple[Type[BaseException], Optional[str]], | ||||||
|  |         handler: RouteHandler, | ||||||
|  |     ) -> None: | ||||||
|  |         if key in self.cached_handlers: | ||||||
|  |             exc, name = key | ||||||
|  |             if name is None: | ||||||
|  |                 name = "__ALL_ROUTES__" | ||||||
|  |  | ||||||
|  |             error_logger.warning( | ||||||
|  |                 f"Duplicate exception handler definition on: route={name} " | ||||||
|  |                 f"and exception={exc}" | ||||||
|  |             ) | ||||||
|  |             deprecation( | ||||||
|  |                 "A duplicate exception handler definition was discovered. " | ||||||
|  |                 "This may cause unintended consequences. A warning has been " | ||||||
|  |                 "issued now, but it will not be allowed starting in v23.3.", | ||||||
|  |                 23.3, | ||||||
|  |             ) | ||||||
|  |         self.cached_handlers[key] = handler | ||||||
|  |  | ||||||
|  |     def add(self, exception, handler, route_names: Optional[List[str]] = None): | ||||||
|  |         """ | ||||||
|  |         Add a new exception handler to an already existing handler object. | ||||||
|  |  | ||||||
|  |         :param exception: Type of exception that need to be handled | ||||||
|  |         :param handler: Reference to the method that will handle the exception | ||||||
|  |  | ||||||
|  |         :type exception: :class:`sanic.exceptions.SanicException` or | ||||||
|  |             :class:`Exception` | ||||||
|  |         :type handler: ``function`` | ||||||
|  |  | ||||||
|  |         :return: None | ||||||
|  |         """ | ||||||
|  |         if route_names: | ||||||
|  |             for route in route_names: | ||||||
|  |                 self._add((exception, route), handler) | ||||||
|  |         else: | ||||||
|  |             self._add((exception, None), handler) | ||||||
|  |  | ||||||
|  |     def lookup(self, exception, route_name: Optional[str] = None): | ||||||
|  |         """ | ||||||
|  |         Lookup the existing instance of :class:`ErrorHandler` and fetch the | ||||||
|  |         registered handler for a specific type of exception. | ||||||
|  |  | ||||||
|  |         This method leverages a dict lookup to speedup the retrieval process. | ||||||
|  |  | ||||||
|  |         :param exception: Type of exception | ||||||
|  |  | ||||||
|  |         :type exception: :class:`sanic.exceptions.SanicException` or | ||||||
|  |             :class:`Exception` | ||||||
|  |  | ||||||
|  |         :return: Registered function if found ``None`` otherwise | ||||||
|  |         """ | ||||||
|  |         exception_class = type(exception) | ||||||
|  |  | ||||||
|  |         for name in (route_name, None): | ||||||
|  |             exception_key = (exception_class, name) | ||||||
|  |             handler = self.cached_handlers.get(exception_key) | ||||||
|  |             if handler: | ||||||
|  |                 return handler | ||||||
|  |  | ||||||
|  |         for name in (route_name, None): | ||||||
|  |             for ancestor in type.mro(exception_class): | ||||||
|  |                 exception_key = (ancestor, name) | ||||||
|  |                 if exception_key in self.cached_handlers: | ||||||
|  |                     handler = self.cached_handlers[exception_key] | ||||||
|  |                     self.cached_handlers[ | ||||||
|  |                         (exception_class, route_name) | ||||||
|  |                     ] = handler | ||||||
|  |                     return handler | ||||||
|  |  | ||||||
|  |                 if ancestor is BaseException: | ||||||
|  |                     break | ||||||
|  |         self.cached_handlers[(exception_class, route_name)] = None | ||||||
|  |         handler = None | ||||||
|  |         return handler | ||||||
|  |  | ||||||
|  |     _lookup = _full_lookup | ||||||
|  |  | ||||||
|  |     def response(self, request, exception): | ||||||
|  |         """Fetches and executes an exception handler and returns a response | ||||||
|  |         object | ||||||
|  |  | ||||||
|  |         :param request: Instance of :class:`sanic.request.Request` | ||||||
|  |         :param exception: Exception to handle | ||||||
|  |  | ||||||
|  |         :type request: :class:`sanic.request.Request` | ||||||
|  |         :type exception: :class:`sanic.exceptions.SanicException` or | ||||||
|  |             :class:`Exception` | ||||||
|  |  | ||||||
|  |         :return: Wrap the return value obtained from :func:`default` | ||||||
|  |             or registered handler for that type of exception. | ||||||
|  |         """ | ||||||
|  |         route_name = request.name if request else None | ||||||
|  |         handler = self._lookup(exception, route_name) | ||||||
|  |         response = None | ||||||
|  |         try: | ||||||
|  |             if handler: | ||||||
|  |                 response = handler(request, exception) | ||||||
|  |             if response is None: | ||||||
|  |                 response = self.default(request, exception) | ||||||
|  |         except Exception: | ||||||
|  |             try: | ||||||
|  |                 url = repr(request.url) | ||||||
|  |             except AttributeError:  # no cov | ||||||
|  |                 url = "unknown" | ||||||
|  |             response_message = ( | ||||||
|  |                 "Exception raised in exception handler " '"%s" for uri: %s' | ||||||
|  |             ) | ||||||
|  |             error_logger.exception(response_message, handler.__name__, url) | ||||||
|  |  | ||||||
|  |             if self.debug: | ||||||
|  |                 return text(response_message % (handler.__name__, url), 500) | ||||||
|  |             else: | ||||||
|  |                 return text("An error occurred while handling an error", 500) | ||||||
|  |         return response | ||||||
|  |  | ||||||
|  |     def default(self, request, exception): | ||||||
|  |         """ | ||||||
|  |         Provide a default behavior for the objects of :class:`ErrorHandler`. | ||||||
|  |         If a developer chooses to extent the :class:`ErrorHandler` they can | ||||||
|  |         provide a custom implementation for this method to behave in a way | ||||||
|  |         they see fit. | ||||||
|  |  | ||||||
|  |         :param request: Incoming request | ||||||
|  |         :param exception: Exception object | ||||||
|  |  | ||||||
|  |         :type request: :class:`sanic.request.Request` | ||||||
|  |         :type exception: :class:`sanic.exceptions.SanicException` or | ||||||
|  |             :class:`Exception` | ||||||
|  |         :return: | ||||||
|  |         """ | ||||||
|  |         self.log(request, exception) | ||||||
|  |         fallback = request.app.config.FALLBACK_ERROR_FORMAT | ||||||
|  |         return exception_response( | ||||||
|  |             request, | ||||||
|  |             exception, | ||||||
|  |             debug=self.debug, | ||||||
|  |             base=self.base, | ||||||
|  |             fallback=fallback, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def log(request, exception): | ||||||
|  |         quiet = getattr(exception, "quiet", False) | ||||||
|  |         noisy = getattr(request.app.config, "NOISY_EXCEPTIONS", False) | ||||||
|  |         if quiet is False or noisy is True: | ||||||
|  |             try: | ||||||
|  |                 url = repr(request.url) | ||||||
|  |             except AttributeError:  # no cov | ||||||
|  |                 url = "unknown" | ||||||
|  |  | ||||||
|  |             error_logger.exception( | ||||||
|  |                 "Exception occurred while handling uri: %s", url | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ContentRangeHandler: | ||||||
|  |     """ | ||||||
|  |     A mechanism to parse and process the incoming request headers to | ||||||
|  |     extract the content range information. | ||||||
|  |  | ||||||
|  |     :param request: Incoming api request | ||||||
|  |     :param stats: Stats related to the content | ||||||
|  |  | ||||||
|  |     :type request: :class:`sanic.request.Request` | ||||||
|  |     :type stats: :class:`posix.stat_result` | ||||||
|  |  | ||||||
|  |     :ivar start: Content Range start | ||||||
|  |     :ivar end: Content Range end | ||||||
|  |     :ivar size: Length of the content | ||||||
|  |     :ivar total: Total size identified by the :class:`posix.stat_result` | ||||||
|  |         instance | ||||||
|  |     :ivar ContentRangeHandler.headers: Content range header ``dict`` | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     __slots__ = ("start", "end", "size", "total", "headers") | ||||||
|  |  | ||||||
|  |     def __init__(self, request, stats): | ||||||
|  |         self.total = stats.st_size | ||||||
|  |         _range = request.headers.getone("range", None) | ||||||
|  |         if _range is None: | ||||||
|  |             raise HeaderNotFound("Range Header Not Found") | ||||||
|  |         unit, _, value = tuple(map(str.strip, _range.partition("="))) | ||||||
|  |         if unit != "bytes": | ||||||
|  |             raise InvalidRangeType( | ||||||
|  |                 "%s is not a valid Range Type" % (unit,), self | ||||||
|  |             ) | ||||||
|  |         start_b, _, end_b = tuple(map(str.strip, value.partition("-"))) | ||||||
|  |         try: | ||||||
|  |             self.start = int(start_b) if start_b else None | ||||||
|  |         except ValueError: | ||||||
|  |             raise RangeNotSatisfiable( | ||||||
|  |                 "'%s' is invalid for Content Range" % (start_b,), self | ||||||
|  |             ) | ||||||
|  |         try: | ||||||
|  |             self.end = int(end_b) if end_b else None | ||||||
|  |         except ValueError: | ||||||
|  |             raise RangeNotSatisfiable( | ||||||
|  |                 "'%s' is invalid for Content Range" % (end_b,), self | ||||||
|  |             ) | ||||||
|  |         if self.end is None: | ||||||
|  |             if self.start is None: | ||||||
|  |                 raise RangeNotSatisfiable( | ||||||
|  |                     "Invalid for Content Range parameters", self | ||||||
|  |                 ) | ||||||
|  |             else: | ||||||
|  |                 # this case represents `Content-Range: bytes 5-` | ||||||
|  |                 self.end = self.total - 1 | ||||||
|  |         else: | ||||||
|  |             if self.start is None: | ||||||
|  |                 # this case represents `Content-Range: bytes -5` | ||||||
|  |                 self.start = self.total - self.end | ||||||
|  |                 self.end = self.total - 1 | ||||||
|  |         if self.start >= self.end: | ||||||
|  |             raise RangeNotSatisfiable( | ||||||
|  |                 "Invalid for Content Range parameters", self | ||||||
|  |             ) | ||||||
|  |         self.size = self.end - self.start + 1 | ||||||
|  |         self.headers = { | ||||||
|  |             "Content-Range": "bytes %s-%s/%s" | ||||||
|  |             % (self.start, self.end, self.total) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     def __bool__(self): | ||||||
|  |         return self.size > 0 | ||||||
| @@ -1,10 +0,0 @@ | |||||||
| from .content_range import ContentRangeHandler |  | ||||||
| from .directory import DirectoryHandler |  | ||||||
| from .error import ErrorHandler |  | ||||||
|  |  | ||||||
|  |  | ||||||
| __all__ = ( |  | ||||||
|     "ContentRangeHandler", |  | ||||||
|     "DirectoryHandler", |  | ||||||
|     "ErrorHandler", |  | ||||||
| ) |  | ||||||
| @@ -1,78 +0,0 @@ | |||||||
| from __future__ import annotations |  | ||||||
|  |  | ||||||
| from sanic.exceptions import ( |  | ||||||
|     HeaderNotFound, |  | ||||||
|     InvalidRangeType, |  | ||||||
|     RangeNotSatisfiable, |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ContentRangeHandler: |  | ||||||
|     """ |  | ||||||
|     A mechanism to parse and process the incoming request headers to |  | ||||||
|     extract the content range information. |  | ||||||
|  |  | ||||||
|     :param request: Incoming api request |  | ||||||
|     :param stats: Stats related to the content |  | ||||||
|  |  | ||||||
|     :type request: :class:`sanic.request.Request` |  | ||||||
|     :type stats: :class:`posix.stat_result` |  | ||||||
|  |  | ||||||
|     :ivar start: Content Range start |  | ||||||
|     :ivar end: Content Range end |  | ||||||
|     :ivar size: Length of the content |  | ||||||
|     :ivar total: Total size identified by the :class:`posix.stat_result` |  | ||||||
|         instance |  | ||||||
|     :ivar ContentRangeHandler.headers: Content range header ``dict`` |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     __slots__ = ("start", "end", "size", "total", "headers") |  | ||||||
|  |  | ||||||
|     def __init__(self, request, stats): |  | ||||||
|         self.total = stats.st_size |  | ||||||
|         _range = request.headers.getone("range", None) |  | ||||||
|         if _range is None: |  | ||||||
|             raise HeaderNotFound("Range Header Not Found") |  | ||||||
|         unit, _, value = tuple(map(str.strip, _range.partition("="))) |  | ||||||
|         if unit != "bytes": |  | ||||||
|             raise InvalidRangeType( |  | ||||||
|                 "%s is not a valid Range Type" % (unit,), self |  | ||||||
|             ) |  | ||||||
|         start_b, _, end_b = tuple(map(str.strip, value.partition("-"))) |  | ||||||
|         try: |  | ||||||
|             self.start = int(start_b) if start_b else None |  | ||||||
|         except ValueError: |  | ||||||
|             raise RangeNotSatisfiable( |  | ||||||
|                 "'%s' is invalid for Content Range" % (start_b,), self |  | ||||||
|             ) |  | ||||||
|         try: |  | ||||||
|             self.end = int(end_b) if end_b else None |  | ||||||
|         except ValueError: |  | ||||||
|             raise RangeNotSatisfiable( |  | ||||||
|                 "'%s' is invalid for Content Range" % (end_b,), self |  | ||||||
|             ) |  | ||||||
|         if self.end is None: |  | ||||||
|             if self.start is None: |  | ||||||
|                 raise RangeNotSatisfiable( |  | ||||||
|                     "Invalid for Content Range parameters", self |  | ||||||
|                 ) |  | ||||||
|             else: |  | ||||||
|                 # this case represents `Content-Range: bytes 5-` |  | ||||||
|                 self.end = self.total - 1 |  | ||||||
|         else: |  | ||||||
|             if self.start is None: |  | ||||||
|                 # this case represents `Content-Range: bytes -5` |  | ||||||
|                 self.start = self.total - self.end |  | ||||||
|                 self.end = self.total - 1 |  | ||||||
|         if self.start >= self.end: |  | ||||||
|             raise RangeNotSatisfiable( |  | ||||||
|                 "Invalid for Content Range parameters", self |  | ||||||
|             ) |  | ||||||
|         self.size = self.end - self.start + 1 |  | ||||||
|         self.headers = { |  | ||||||
|             "Content-Range": "bytes %s-%s/%s" |  | ||||||
|             % (self.start, self.end, self.total) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     def __bool__(self): |  | ||||||
|         return self.size > 0 |  | ||||||
| @@ -1,84 +0,0 @@ | |||||||
| from __future__ import annotations |  | ||||||
|  |  | ||||||
| from datetime import datetime |  | ||||||
| from operator import itemgetter |  | ||||||
| from pathlib import Path |  | ||||||
| from stat import S_ISDIR |  | ||||||
| from typing import Dict, Iterable, Optional, Sequence, Union, cast |  | ||||||
|  |  | ||||||
| from sanic.exceptions import NotFound |  | ||||||
| from sanic.pages.directory_page import DirectoryPage, FileInfo |  | ||||||
| from sanic.request import Request |  | ||||||
| from sanic.response import file, html, redirect |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class DirectoryHandler: |  | ||||||
|     def __init__( |  | ||||||
|         self, |  | ||||||
|         uri: str, |  | ||||||
|         directory: Path, |  | ||||||
|         directory_view: bool = False, |  | ||||||
|         index: Optional[Union[str, Sequence[str]]] = None, |  | ||||||
|     ) -> None: |  | ||||||
|         if isinstance(index, str): |  | ||||||
|             index = [index] |  | ||||||
|         elif index is None: |  | ||||||
|             index = [] |  | ||||||
|         self.base = uri.strip("/") |  | ||||||
|         self.directory = directory |  | ||||||
|         self.directory_view = directory_view |  | ||||||
|         self.index = tuple(index) |  | ||||||
|  |  | ||||||
|     async def handle(self, request: Request, path: str): |  | ||||||
|         current = path.strip("/")[len(self.base) :].strip("/")  # noqa: E203 |  | ||||||
|         for file_name in self.index: |  | ||||||
|             index_file = self.directory / current / file_name |  | ||||||
|             if index_file.is_file(): |  | ||||||
|                 return await file(index_file) |  | ||||||
|  |  | ||||||
|         if self.directory_view: |  | ||||||
|             return self._index( |  | ||||||
|                 self.directory / current, path, request.app.debug |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         if self.index: |  | ||||||
|             raise NotFound("File not found") |  | ||||||
|  |  | ||||||
|         raise IsADirectoryError(f"{self.directory.as_posix()} is a directory") |  | ||||||
|  |  | ||||||
|     def _index(self, location: Path, path: str, debug: bool): |  | ||||||
|         # Remove empty path elements, append slash |  | ||||||
|         if "//" in path or not path.endswith("/"): |  | ||||||
|             return redirect( |  | ||||||
|                 "/" + "".join([f"{p}/" for p in path.split("/") if p]) |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         # Render file browser |  | ||||||
|         page = DirectoryPage(self._iter_files(location), path, debug) |  | ||||||
|         return html(page.render()) |  | ||||||
|  |  | ||||||
|     def _prepare_file(self, path: Path) -> Dict[str, Union[int, str]]: |  | ||||||
|         stat = path.stat() |  | ||||||
|         modified = ( |  | ||||||
|             datetime.fromtimestamp(stat.st_mtime) |  | ||||||
|             .isoformat()[:19] |  | ||||||
|             .replace("T", " ") |  | ||||||
|         ) |  | ||||||
|         is_dir = S_ISDIR(stat.st_mode) |  | ||||||
|         icon = "📁" if is_dir else "📄" |  | ||||||
|         file_name = path.name |  | ||||||
|         if is_dir: |  | ||||||
|             file_name += "/" |  | ||||||
|         return { |  | ||||||
|             "priority": is_dir * -1, |  | ||||||
|             "file_name": file_name, |  | ||||||
|             "icon": icon, |  | ||||||
|             "file_access": modified, |  | ||||||
|             "file_size": stat.st_size, |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     def _iter_files(self, location: Path) -> Iterable[FileInfo]: |  | ||||||
|         prepared = [self._prepare_file(f) for f in location.iterdir()] |  | ||||||
|         for item in sorted(prepared, key=itemgetter("priority", "file_name")): |  | ||||||
|             del item["priority"] |  | ||||||
|             yield cast(FileInfo, item) |  | ||||||
| @@ -1,192 +0,0 @@ | |||||||
| from __future__ import annotations |  | ||||||
|  |  | ||||||
| from typing import Dict, List, Optional, Tuple, Type |  | ||||||
|  |  | ||||||
| from sanic.errorpages import BaseRenderer, TextRenderer, exception_response |  | ||||||
| from sanic.log import deprecation, error_logger |  | ||||||
| from sanic.models.handler_types import RouteHandler |  | ||||||
| from sanic.response import text |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ErrorHandler: |  | ||||||
|     """ |  | ||||||
|     Provide :class:`sanic.app.Sanic` application with a mechanism to handle |  | ||||||
|     and process any and all uncaught exceptions in a way the application |  | ||||||
|     developer will set fit. |  | ||||||
|  |  | ||||||
|     This error handling framework is built into the core that can be extended |  | ||||||
|     by the developers to perform a wide range of tasks from recording the error |  | ||||||
|     stats to reporting them to an external service that can be used for |  | ||||||
|     realtime alerting system. |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     def __init__( |  | ||||||
|         self, |  | ||||||
|         base: Type[BaseRenderer] = TextRenderer, |  | ||||||
|     ): |  | ||||||
|         self.cached_handlers: Dict[ |  | ||||||
|             Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler] |  | ||||||
|         ] = {} |  | ||||||
|         self.debug = False |  | ||||||
|         self.base = base |  | ||||||
|  |  | ||||||
|     def _full_lookup(self, exception, route_name: Optional[str] = None): |  | ||||||
|         return self.lookup(exception, route_name) |  | ||||||
|  |  | ||||||
|     def _add( |  | ||||||
|         self, |  | ||||||
|         key: Tuple[Type[BaseException], Optional[str]], |  | ||||||
|         handler: RouteHandler, |  | ||||||
|     ) -> None: |  | ||||||
|         if key in self.cached_handlers: |  | ||||||
|             exc, name = key |  | ||||||
|             if name is None: |  | ||||||
|                 name = "__ALL_ROUTES__" |  | ||||||
|  |  | ||||||
|             error_logger.warning( |  | ||||||
|                 f"Duplicate exception handler definition on: route={name} " |  | ||||||
|                 f"and exception={exc}" |  | ||||||
|             ) |  | ||||||
|             deprecation( |  | ||||||
|                 "A duplicate exception handler definition was discovered. " |  | ||||||
|                 "This may cause unintended consequences. A warning has been " |  | ||||||
|                 "issued now, but it will not be allowed starting in v23.3.", |  | ||||||
|                 23.3, |  | ||||||
|             ) |  | ||||||
|         self.cached_handlers[key] = handler |  | ||||||
|  |  | ||||||
|     def add(self, exception, handler, route_names: Optional[List[str]] = None): |  | ||||||
|         """ |  | ||||||
|         Add a new exception handler to an already existing handler object. |  | ||||||
|  |  | ||||||
|         :param exception: Type of exception that need to be handled |  | ||||||
|         :param handler: Reference to the method that will handle the exception |  | ||||||
|  |  | ||||||
|         :type exception: :class:`sanic.exceptions.SanicException` or |  | ||||||
|             :class:`Exception` |  | ||||||
|         :type handler: ``function`` |  | ||||||
|  |  | ||||||
|         :return: None |  | ||||||
|         """ |  | ||||||
|         if route_names: |  | ||||||
|             for route in route_names: |  | ||||||
|                 self._add((exception, route), handler) |  | ||||||
|         else: |  | ||||||
|             self._add((exception, None), handler) |  | ||||||
|  |  | ||||||
|     def lookup(self, exception, route_name: Optional[str] = None): |  | ||||||
|         """ |  | ||||||
|         Lookup the existing instance of :class:`ErrorHandler` and fetch the |  | ||||||
|         registered handler for a specific type of exception. |  | ||||||
|  |  | ||||||
|         This method leverages a dict lookup to speedup the retrieval process. |  | ||||||
|  |  | ||||||
|         :param exception: Type of exception |  | ||||||
|  |  | ||||||
|         :type exception: :class:`sanic.exceptions.SanicException` or |  | ||||||
|             :class:`Exception` |  | ||||||
|  |  | ||||||
|         :return: Registered function if found ``None`` otherwise |  | ||||||
|         """ |  | ||||||
|         exception_class = type(exception) |  | ||||||
|  |  | ||||||
|         for name in (route_name, None): |  | ||||||
|             exception_key = (exception_class, name) |  | ||||||
|             handler = self.cached_handlers.get(exception_key) |  | ||||||
|             if handler: |  | ||||||
|                 return handler |  | ||||||
|  |  | ||||||
|         for name in (route_name, None): |  | ||||||
|             for ancestor in type.mro(exception_class): |  | ||||||
|                 exception_key = (ancestor, name) |  | ||||||
|                 if exception_key in self.cached_handlers: |  | ||||||
|                     handler = self.cached_handlers[exception_key] |  | ||||||
|                     self.cached_handlers[ |  | ||||||
|                         (exception_class, route_name) |  | ||||||
|                     ] = handler |  | ||||||
|                     return handler |  | ||||||
|  |  | ||||||
|                 if ancestor is BaseException: |  | ||||||
|                     break |  | ||||||
|         self.cached_handlers[(exception_class, route_name)] = None |  | ||||||
|         handler = None |  | ||||||
|         return handler |  | ||||||
|  |  | ||||||
|     _lookup = _full_lookup |  | ||||||
|  |  | ||||||
|     def response(self, request, exception): |  | ||||||
|         """Fetches and executes an exception handler and returns a response |  | ||||||
|         object |  | ||||||
|  |  | ||||||
|         :param request: Instance of :class:`sanic.request.Request` |  | ||||||
|         :param exception: Exception to handle |  | ||||||
|  |  | ||||||
|         :type request: :class:`sanic.request.Request` |  | ||||||
|         :type exception: :class:`sanic.exceptions.SanicException` or |  | ||||||
|             :class:`Exception` |  | ||||||
|  |  | ||||||
|         :return: Wrap the return value obtained from :func:`default` |  | ||||||
|             or registered handler for that type of exception. |  | ||||||
|         """ |  | ||||||
|         route_name = request.name if request else None |  | ||||||
|         handler = self._lookup(exception, route_name) |  | ||||||
|         response = None |  | ||||||
|         try: |  | ||||||
|             if handler: |  | ||||||
|                 response = handler(request, exception) |  | ||||||
|             if response is None: |  | ||||||
|                 response = self.default(request, exception) |  | ||||||
|         except Exception: |  | ||||||
|             try: |  | ||||||
|                 url = repr(request.url) |  | ||||||
|             except AttributeError:  # no cov |  | ||||||
|                 url = "unknown" |  | ||||||
|             response_message = ( |  | ||||||
|                 "Exception raised in exception handler " '"%s" for uri: %s' |  | ||||||
|             ) |  | ||||||
|             error_logger.exception(response_message, handler.__name__, url) |  | ||||||
|  |  | ||||||
|             if self.debug: |  | ||||||
|                 return text(response_message % (handler.__name__, url), 500) |  | ||||||
|             else: |  | ||||||
|                 return text("An error occurred while handling an error", 500) |  | ||||||
|         return response |  | ||||||
|  |  | ||||||
|     def default(self, request, exception): |  | ||||||
|         """ |  | ||||||
|         Provide a default behavior for the objects of :class:`ErrorHandler`. |  | ||||||
|         If a developer chooses to extent the :class:`ErrorHandler` they can |  | ||||||
|         provide a custom implementation for this method to behave in a way |  | ||||||
|         they see fit. |  | ||||||
|  |  | ||||||
|         :param request: Incoming request |  | ||||||
|         :param exception: Exception object |  | ||||||
|  |  | ||||||
|         :type request: :class:`sanic.request.Request` |  | ||||||
|         :type exception: :class:`sanic.exceptions.SanicException` or |  | ||||||
|             :class:`Exception` |  | ||||||
|         :return: |  | ||||||
|         """ |  | ||||||
|         self.log(request, exception) |  | ||||||
|         fallback = request.app.config.FALLBACK_ERROR_FORMAT |  | ||||||
|         return exception_response( |  | ||||||
|             request, |  | ||||||
|             exception, |  | ||||||
|             debug=self.debug, |  | ||||||
|             base=self.base, |  | ||||||
|             fallback=fallback, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def log(request, exception): |  | ||||||
|         quiet = getattr(exception, "quiet", False) |  | ||||||
|         noisy = getattr(request.app.config, "NOISY_EXCEPTIONS", False) |  | ||||||
|         if quiet is False or noisy is True: |  | ||||||
|             try: |  | ||||||
|                 url = repr(request.url) |  | ||||||
|             except AttributeError:  # no cov |  | ||||||
|                 url = "unknown" |  | ||||||
|  |  | ||||||
|             error_logger.exception( |  | ||||||
|                 "Exception occurred while handling uri: %s", url |  | ||||||
|             ) |  | ||||||
							
								
								
									
										378
									
								
								sanic/headers.py
									
									
									
									
									
								
							
							
						
						
									
										378
									
								
								sanic/headers.py
									
									
									
									
									
								
							| @@ -19,6 +19,7 @@ OptionsIterable = Iterable[Tuple[str, str]]  # May contain duplicate keys | |||||||
|  |  | ||||||
| _token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"' | _token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"' | ||||||
| _param = re.compile(rf";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII) | _param = re.compile(rf";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII) | ||||||
|  | _firefox_quote_escape = re.compile(r'\\"(?!; |\s*$)') | ||||||
| _ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}" | _ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}" | ||||||
| _ipv6_re = re.compile(_ipv6) | _ipv6_re = re.compile(_ipv6) | ||||||
| _host_re = re.compile( | _host_re = re.compile( | ||||||
| @@ -32,96 +33,143 @@ _host_re = re.compile( | |||||||
| # For more information, consult ../tests/test_requests.py | # For more information, consult ../tests/test_requests.py | ||||||
|  |  | ||||||
|  |  | ||||||
| class MediaType: | def parse_arg_as_accept(f): | ||||||
|     """A media type, as used in the Accept header.""" |     def func(self, other, *args, **kwargs): | ||||||
|  |         if not isinstance(other, Accept) and other: | ||||||
|  |             other = Accept.parse(other) | ||||||
|  |         return f(self, other, *args, **kwargs) | ||||||
|  |  | ||||||
|  |     return func | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MediaType(str): | ||||||
|  |     def __new__(cls, value: str): | ||||||
|  |         return str.__new__(cls, value) | ||||||
|  |  | ||||||
|  |     def __init__(self, value: str) -> None: | ||||||
|  |         self.value = value | ||||||
|  |         self.is_wildcard = self.check_if_wildcard(value) | ||||||
|  |  | ||||||
|  |     def __eq__(self, other): | ||||||
|  |         if self.is_wildcard: | ||||||
|  |             return True | ||||||
|  |  | ||||||
|  |         if self.match(other): | ||||||
|  |             return True | ||||||
|  |  | ||||||
|  |         other_is_wildcard = ( | ||||||
|  |             other.is_wildcard | ||||||
|  |             if isinstance(other, MediaType) | ||||||
|  |             else self.check_if_wildcard(other) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return other_is_wildcard | ||||||
|  |  | ||||||
|  |     def match(self, other): | ||||||
|  |         other_value = other.value if isinstance(other, MediaType) else other | ||||||
|  |         return self.value == other_value | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def check_if_wildcard(value): | ||||||
|  |         return value == "*" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Accept(str): | ||||||
|  |     def __new__(cls, value: str, *args, **kwargs): | ||||||
|  |         return str.__new__(cls, value) | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         type_: str, |         value: str, | ||||||
|         subtype: str, |         type_: MediaType, | ||||||
|         **params: str, |         subtype: MediaType, | ||||||
|  |         *, | ||||||
|  |         q: str = "1.0", | ||||||
|  |         **kwargs: str, | ||||||
|     ): |     ): | ||||||
|         self.type = type_ |         qvalue = float(q) | ||||||
|         self.subtype = subtype |         if qvalue > 1 or qvalue < 0: | ||||||
|         self.q = float(params.get("q", "1.0")) |             raise InvalidHeader( | ||||||
|         self.params = params |                 f"Accept header qvalue must be between 0 and 1, not: {qvalue}" | ||||||
|         self.mime = f"{type_}/{subtype}" |  | ||||||
|         self.key = ( |  | ||||||
|             -1 * self.q, |  | ||||||
|             -1 * len(self.params), |  | ||||||
|             self.subtype == "*", |  | ||||||
|             self.type == "*", |  | ||||||
|             ) |             ) | ||||||
|  |         self.value = value | ||||||
|  |         self.type_ = type_ | ||||||
|  |         self.subtype = subtype | ||||||
|  |         self.qvalue = qvalue | ||||||
|  |         self.params = kwargs | ||||||
|  |  | ||||||
|     def __repr__(self): |     def _compare(self, other, method): | ||||||
|         return self.mime + "".join(f";{k}={v}" for k, v in self.params.items()) |         try: | ||||||
|  |             return method(self.qvalue, other.qvalue) | ||||||
|     def __eq__(self, other): |         except (AttributeError, TypeError): | ||||||
|         """Check for mime (str or MediaType) identical type/subtype. |  | ||||||
|         Parameters such as q are not considered.""" |  | ||||||
|         if isinstance(other, str): |  | ||||||
|             # Give a friendly reminder if str contains parameters |  | ||||||
|             if ";" in other: |  | ||||||
|                 raise ValueError("Use match() to compare with parameters") |  | ||||||
|             return self.mime == other |  | ||||||
|         if isinstance(other, MediaType): |  | ||||||
|             # Ignore parameters silently with MediaType objects |  | ||||||
|             return self.mime == other.mime |  | ||||||
|             return NotImplemented |             return NotImplemented | ||||||
|  |  | ||||||
|  |     @parse_arg_as_accept | ||||||
|  |     def __lt__(self, other: Union[str, Accept]): | ||||||
|  |         return self._compare(other, lambda s, o: s < o) | ||||||
|  |  | ||||||
|  |     @parse_arg_as_accept | ||||||
|  |     def __le__(self, other: Union[str, Accept]): | ||||||
|  |         return self._compare(other, lambda s, o: s <= o) | ||||||
|  |  | ||||||
|  |     @parse_arg_as_accept | ||||||
|  |     def __eq__(self, other: Union[str, Accept]):  # type: ignore | ||||||
|  |         return self._compare(other, lambda s, o: s == o) | ||||||
|  |  | ||||||
|  |     @parse_arg_as_accept | ||||||
|  |     def __ge__(self, other: Union[str, Accept]): | ||||||
|  |         return self._compare(other, lambda s, o: s >= o) | ||||||
|  |  | ||||||
|  |     @parse_arg_as_accept | ||||||
|  |     def __gt__(self, other: Union[str, Accept]): | ||||||
|  |         return self._compare(other, lambda s, o: s > o) | ||||||
|  |  | ||||||
|  |     @parse_arg_as_accept | ||||||
|  |     def __ne__(self, other: Union[str, Accept]):  # type: ignore | ||||||
|  |         return self._compare(other, lambda s, o: s != o) | ||||||
|  |  | ||||||
|  |     @parse_arg_as_accept | ||||||
|     def match( |     def match( | ||||||
|         self, |         self, | ||||||
|         mime_with_params: Union[str, MediaType], |         other, | ||||||
|     ) -> Optional[MediaType]: |         *, | ||||||
|         """Check if this media type matches the given mime type/subtype. |         allow_type_wildcard: bool = True, | ||||||
|         Wildcards are supported both ways on both type and subtype. |         allow_subtype_wildcard: bool = True, | ||||||
|         If mime contains a semicolon, optionally followed by parameters, |     ) -> bool: | ||||||
|         the parameters of the two media types must match exactly. |         type_match = ( | ||||||
|         Note:  Use the `==` operator instead to check for literal matches |             self.type_ == other.type_ | ||||||
|         without expanding wildcards. |             if allow_type_wildcard | ||||||
|         @param media_type: A type/subtype string to match. |             else ( | ||||||
|         @return `self` if the media types are compatible, else `None` |                 self.type_.match(other.type_) | ||||||
|         """ |                 and not self.type_.is_wildcard | ||||||
|         mt = ( |                 and not other.type_.is_wildcard | ||||||
|             MediaType._parse(mime_with_params) |  | ||||||
|             if isinstance(mime_with_params, str) |  | ||||||
|             else mime_with_params |  | ||||||
|         ) |  | ||||||
|         return ( |  | ||||||
|             self |  | ||||||
|             if ( |  | ||||||
|                 mt |  | ||||||
|                 # All parameters given in the other media type must match |  | ||||||
|                 and all(self.params.get(k) == v for k, v in mt.params.items()) |  | ||||||
|                 # Subtype match |  | ||||||
|                 and ( |  | ||||||
|                     self.subtype == mt.subtype |  | ||||||
|                     or self.subtype == "*" |  | ||||||
|                     or mt.subtype == "*" |  | ||||||
|                 ) |  | ||||||
|                 # Type match |  | ||||||
|                 and ( |  | ||||||
|                     self.type == mt.type or self.type == "*" or mt.type == "*" |  | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|             else None |         subtype_match = ( | ||||||
|  |             self.subtype == other.subtype | ||||||
|  |             if allow_subtype_wildcard | ||||||
|  |             else ( | ||||||
|  |                 self.subtype.match(other.subtype) | ||||||
|  |                 and not self.subtype.is_wildcard | ||||||
|  |                 and not other.subtype.is_wildcard | ||||||
|  |             ) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @property |         return type_match and subtype_match | ||||||
|     def has_wildcard(self) -> bool: |  | ||||||
|         """Return True if this media type has a wildcard in it.""" |  | ||||||
|         return any(part == "*" for part in (self.subtype, self.type)) |  | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _parse(cls, mime_with_params: str) -> Optional[MediaType]: |     def parse(cls, raw: str) -> Accept: | ||||||
|         mtype = mime_with_params.strip() |         invalid = False | ||||||
|         if "/" not in mime_with_params: |         mtype = raw.strip() | ||||||
|             return None |  | ||||||
|  |  | ||||||
|         mime, *raw_params = mtype.split(";") |         try: | ||||||
|         type_, subtype = mime.split("/", 1) |             media, *raw_params = mtype.split(";") | ||||||
|         if not type_ or not subtype: |             type_, subtype = media.split("/") | ||||||
|             raise ValueError(f"Invalid media type: {mtype}") |         except ValueError: | ||||||
|  |             invalid = True | ||||||
|  |  | ||||||
|  |         if invalid or not type_ or not subtype: | ||||||
|  |             raise InvalidHeader(f"Header contains invalid Accept value: {raw}") | ||||||
|  |  | ||||||
|         params = dict( |         params = dict( | ||||||
|             [ |             [ | ||||||
| @@ -130,160 +178,46 @@ class MediaType: | |||||||
|             ] |             ] | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         return cls(type_.lstrip(), subtype.rstrip(), **params) |         return cls(mtype, MediaType(type_), MediaType(subtype), **params) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Matched: | class AcceptContainer(list): | ||||||
|     """A matching result of a MIME string against a header.""" |     def __contains__(self, o: object) -> bool: | ||||||
|  |         return any(item.match(o) for item in self) | ||||||
|  |  | ||||||
|     def __init__(self, mime: str, header: Optional[MediaType]): |     def match( | ||||||
|         self.mime = mime |         self, | ||||||
|         self.header = header |         o: object, | ||||||
|  |         *, | ||||||
|     def __repr__(self): |         allow_type_wildcard: bool = True, | ||||||
|         return f"<{self} matched {self.header}>" if self else "<no match>" |         allow_subtype_wildcard: bool = True, | ||||||
|  |     ) -> bool: | ||||||
|     def __str__(self): |         return any( | ||||||
|         return self.mime |             item.match( | ||||||
|  |                 o, | ||||||
|     def __bool__(self): |                 allow_type_wildcard=allow_type_wildcard, | ||||||
|         return self.header is not None |                 allow_subtype_wildcard=allow_subtype_wildcard, | ||||||
|  |  | ||||||
|     def __eq__(self, other: Any) -> bool: |  | ||||||
|         try: |  | ||||||
|             comp, other_accept = self._compare(other) |  | ||||||
|         except TypeError: |  | ||||||
|             return False |  | ||||||
|  |  | ||||||
|         return bool( |  | ||||||
|             comp |  | ||||||
|             and ( |  | ||||||
|                 (self.header and other_accept.header) |  | ||||||
|                 or (not self.header and not other_accept.header) |  | ||||||
|             ) |             ) | ||||||
|  |             for item in self | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def _compare(self, other) -> Tuple[bool, Matched]: |  | ||||||
|         if isinstance(other, str): |  | ||||||
|             parsed = Matched.parse(other) |  | ||||||
|             if self.mime == other: |  | ||||||
|                 return True, parsed |  | ||||||
|             other = parsed |  | ||||||
|  |  | ||||||
|         if isinstance(other, Matched): |  | ||||||
|             return self.header == other.header, other |  | ||||||
|  |  | ||||||
|         raise TypeError( |  | ||||||
|             "Comparison not supported between unequal " |  | ||||||
|             f"mime types of '{self.mime}' and '{other}'" |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def match(self, other: Union[str, Matched]) -> Optional[Matched]: |  | ||||||
|         accept = Matched.parse(other) if isinstance(other, str) else other |  | ||||||
|         if not self.header or not accept.header: |  | ||||||
|             return None |  | ||||||
|         if self.header.match(accept.header): |  | ||||||
|             return accept |  | ||||||
|         return None |  | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def parse(cls, raw: str) -> Matched: |  | ||||||
|         media_type = MediaType._parse(raw) |  | ||||||
|         return cls(raw, media_type) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AcceptList(list): |  | ||||||
|     """A list of media types, as used in the Accept header. |  | ||||||
|  |  | ||||||
|     The Accept header entries are listed in order of preference, starting |  | ||||||
|     with the most preferred. This class is a list of `MediaType` objects, |  | ||||||
|     that encapsulate also the q value or any other parameters. |  | ||||||
|  |  | ||||||
|     Two separate methods are provided for searching the list: |  | ||||||
|     - 'match' for finding the most preferred match (wildcards supported) |  | ||||||
|     -  operator 'in' for checking explicit matches (wildcards as literals) |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     def match(self, *mimes: str, accept_wildcards=True) -> Matched: |  | ||||||
|         """Find a media type accepted by the client. |  | ||||||
|  |  | ||||||
|         This method can be used to find which of the media types requested by |  | ||||||
|         the client is most preferred against the ones given as arguments. |  | ||||||
|  |  | ||||||
|         The ordering of preference is set by: |  | ||||||
|         1. The order set by RFC 7231, s. 5.3.2, giving a higher priority |  | ||||||
|             to q values and more specific type definitions, |  | ||||||
|         2. The order of the arguments (first is most preferred), and |  | ||||||
|         3. The first matching entry on the Accept header. |  | ||||||
|  |  | ||||||
|         Wildcards are matched both ways. A match is usually found, as the |  | ||||||
|         Accept headers typically include `*/*`, in particular if the header |  | ||||||
|         is missing, is not manually set, or if the client is a browser. |  | ||||||
|  |  | ||||||
|         Note: the returned object behaves as a string of the mime argument |  | ||||||
|         that matched, and is empty/falsy if no match was found. The matched |  | ||||||
|         header entry `MediaType` or `None` is available as the `m` attribute. |  | ||||||
|  |  | ||||||
|         @param mimes: Any MIME types to search for in order of preference. |  | ||||||
|         @param accept_wildcards: Match Accept entries with wildcards in them. |  | ||||||
|         @return A match object with the mime string and the MediaType object. |  | ||||||
|         """ |  | ||||||
|         a = sorted( |  | ||||||
|             (-acc.q, i, j, mime, acc) |  | ||||||
|             for j, acc in enumerate(self) |  | ||||||
|             if accept_wildcards or not acc.has_wildcard |  | ||||||
|             for i, mime in enumerate(mimes) |  | ||||||
|             if acc.match(mime) |  | ||||||
|         ) |  | ||||||
|         return Matched(*(a[0][-2:] if a else ("", None))) |  | ||||||
|  |  | ||||||
|     def __str__(self): |  | ||||||
|         """Format as Accept header value (parsed, not original).""" |  | ||||||
|         return ", ".join(str(m) for m in self) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def parse_accept(accept: Optional[str]) -> AcceptList: |  | ||||||
|     """Parse an Accept header and order the acceptable media types in |  | ||||||
|     according to RFC 7231, s. 5.3.2 |  | ||||||
|     https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 |  | ||||||
|     """ |  | ||||||
|     if not accept: |  | ||||||
|         if accept == "": |  | ||||||
|             return AcceptList()  # Empty header, accept nothing |  | ||||||
|         accept = "*/*"  # No header means that all types are accepted |  | ||||||
|     try: |  | ||||||
|         a = [ |  | ||||||
|             mt |  | ||||||
|             for mt in [MediaType._parse(mtype) for mtype in accept.split(",")] |  | ||||||
|             if mt |  | ||||||
|         ] |  | ||||||
|         if not a: |  | ||||||
|             raise ValueError |  | ||||||
|         return AcceptList(sorted(a, key=lambda x: x.key)) |  | ||||||
|     except ValueError: |  | ||||||
|         raise InvalidHeader(f"Invalid header value in Accept: {accept}") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def parse_content_header(value: str) -> Tuple[str, Options]: | def parse_content_header(value: str) -> Tuple[str, Options]: | ||||||
|     """Parse content-type and content-disposition header values. |     """Parse content-type and content-disposition header values. | ||||||
|  |  | ||||||
|     E.g. `form-data; name=upload; filename="file.txt"` to |     E.g. 'form-data; name=upload; filename=\"file.txt\"' to | ||||||
|     ('form-data', {'name': 'upload', 'filename': 'file.txt'}) |     ('form-data', {'name': 'upload', 'filename': 'file.txt'}) | ||||||
|  |  | ||||||
|     Mostly identical to cgi.parse_header and werkzeug.parse_options_header |     Mostly identical to cgi.parse_header and werkzeug.parse_options_header | ||||||
|     but runs faster and handles special characters better. |     but runs faster and handles special characters better. Unescapes quotes. | ||||||
|  |  | ||||||
|     Unescapes %22 to `"` and %0D%0A to `\n` in field values. |  | ||||||
|     """ |     """ | ||||||
|  |     value = _firefox_quote_escape.sub("%22", value) | ||||||
|     pos = value.find(";") |     pos = value.find(";") | ||||||
|     if pos == -1: |     if pos == -1: | ||||||
|         options: Dict[str, Union[int, str]] = {} |         options: Dict[str, Union[int, str]] = {} | ||||||
|     else: |     else: | ||||||
|         options = { |         options = { | ||||||
|             m.group(1) |             m.group(1).lower(): m.group(2) or m.group(3).replace("%22", '"') | ||||||
|             .lower(): (m.group(2) or m.group(3)) |  | ||||||
|             .replace("%22", '"') |  | ||||||
|             .replace("%0D%0A", "\n") |  | ||||||
|             for m in _param.finditer(value[pos:]) |             for m in _param.finditer(value[pos:]) | ||||||
|         } |         } | ||||||
|         value = value[:pos] |         value = value[:pos] | ||||||
| @@ -434,6 +368,34 @@ def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes: | |||||||
|     return ret |     return ret | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _sort_accept_value(accept: Accept): | ||||||
|  |     return ( | ||||||
|  |         accept.qvalue, | ||||||
|  |         len(accept.params), | ||||||
|  |         accept.subtype != "*", | ||||||
|  |         accept.type_ != "*", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def parse_accept(accept: str) -> AcceptContainer: | ||||||
|  |     """Parse an Accept header and order the acceptable media types in | ||||||
|  |     accorsing to RFC 7231, s. 5.3.2 | ||||||
|  |     https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 | ||||||
|  |     """ | ||||||
|  |     media_types = accept.split(",") | ||||||
|  |     accept_list: List[Accept] = [] | ||||||
|  |  | ||||||
|  |     for mtype in media_types: | ||||||
|  |         if not mtype: | ||||||
|  |             continue | ||||||
|  |  | ||||||
|  |         accept_list.append(Accept.parse(mtype)) | ||||||
|  |  | ||||||
|  |     return AcceptContainer( | ||||||
|  |         sorted(accept_list, key=_sort_accept_value, reverse=True) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def parse_credentials( | def parse_credentials( | ||||||
|     header: Optional[str], |     header: Optional[str], | ||||||
|     prefixes: Union[List, Tuple, Set] = None, |     prefixes: Union[List, Tuple, Set] = None, | ||||||
|   | |||||||
| @@ -16,7 +16,6 @@ from sanic.exceptions import ( | |||||||
|     PayloadTooLarge, |     PayloadTooLarge, | ||||||
|     RequestCancelled, |     RequestCancelled, | ||||||
|     ServerError, |     ServerError, | ||||||
|     ServiceUnavailable, |  | ||||||
| ) | ) | ||||||
| from sanic.headers import format_http1_response | from sanic.headers import format_http1_response | ||||||
| from sanic.helpers import has_message_body | from sanic.helpers import has_message_body | ||||||
| @@ -71,6 +70,7 @@ class Http(Stream, metaclass=TouchUpMeta): | |||||||
|         "request_body", |         "request_body", | ||||||
|         "request_bytes", |         "request_bytes", | ||||||
|         "request_bytes_left", |         "request_bytes_left", | ||||||
|  |         "request_max_size", | ||||||
|         "response", |         "response", | ||||||
|         "response_func", |         "response_func", | ||||||
|         "response_size", |         "response_size", | ||||||
| @@ -124,7 +124,8 @@ class Http(Stream, metaclass=TouchUpMeta): | |||||||
|  |  | ||||||
|                 self.stage = Stage.HANDLER |                 self.stage = Stage.HANDLER | ||||||
|                 self.request.conn_info = self.protocol.conn_info |                 self.request.conn_info = self.protocol.conn_info | ||||||
|                 await self.protocol.request_handler(self.request) |  | ||||||
|  |                 await self.request.manager.handle() | ||||||
|  |  | ||||||
|                 # Handler finished, response should've been sent |                 # Handler finished, response should've been sent | ||||||
|                 if self.stage is Stage.HANDLER and not self.upgrade_websocket: |                 if self.stage is Stage.HANDLER and not self.upgrade_websocket: | ||||||
| @@ -250,6 +251,7 @@ class Http(Stream, metaclass=TouchUpMeta): | |||||||
|             transport=self.protocol.transport, |             transport=self.protocol.transport, | ||||||
|             app=self.protocol.app, |             app=self.protocol.app, | ||||||
|         ) |         ) | ||||||
|  |         self.protocol.request_handler.create(request) | ||||||
|         self.protocol.request_class._current.set(request) |         self.protocol.request_class._current.set(request) | ||||||
|         await self.dispatch( |         await self.dispatch( | ||||||
|             "http.lifecycle.request", |             "http.lifecycle.request", | ||||||
| @@ -423,18 +425,11 @@ class Http(Stream, metaclass=TouchUpMeta): | |||||||
|  |  | ||||||
|         # From request and handler states we can respond, otherwise be silent |         # From request and handler states we can respond, otherwise be silent | ||||||
|         if self.stage is Stage.HANDLER: |         if self.stage is Stage.HANDLER: | ||||||
|             app = self.protocol.app |  | ||||||
|  |  | ||||||
|             if self.request is None: |             if self.request is None: | ||||||
|                 self.create_empty_request() |                 self.create_empty_request() | ||||||
|  |                 self.protocol.request_handler.create(self.request) | ||||||
|  |  | ||||||
|             request_middleware = not isinstance(exception, ServiceUnavailable) |             await self.request.manager.error(exception) | ||||||
|             try: |  | ||||||
|                 await app.handle_exception( |  | ||||||
|                     self.request, exception, request_middleware |  | ||||||
|                 ) |  | ||||||
|             except Exception as e: |  | ||||||
|                 await app.handle_exception(self.request, e, False) |  | ||||||
|  |  | ||||||
|     def create_empty_request(self) -> None: |     def create_empty_request(self) -> None: | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ class Stream: | |||||||
|     request_max_size: Union[int, float] |     request_max_size: Union[int, float] | ||||||
|  |  | ||||||
|     __touchup__: Tuple[str, ...] = tuple() |     __touchup__: Tuple[str, ...] = tuple() | ||||||
|     __slots__ = ("request_max_size",) |     __slots__ = () | ||||||
|  |  | ||||||
|     def respond( |     def respond( | ||||||
|         self, response: BaseHTTPResponse |         self, response: BaseHTTPResponse | ||||||
|   | |||||||
| @@ -24,14 +24,12 @@ def create_context( | |||||||
|     certfile: Optional[str] = None, |     certfile: Optional[str] = None, | ||||||
|     keyfile: Optional[str] = None, |     keyfile: Optional[str] = None, | ||||||
|     password: Optional[str] = None, |     password: Optional[str] = None, | ||||||
|     purpose: ssl.Purpose = ssl.Purpose.CLIENT_AUTH, |  | ||||||
| ) -> ssl.SSLContext: | ) -> ssl.SSLContext: | ||||||
|     """Create a context with secure crypto and HTTP/1.1 in protocols.""" |     """Create a context with secure crypto and HTTP/1.1 in protocols.""" | ||||||
|     context = ssl.create_default_context(purpose=purpose) |     context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) | ||||||
|     context.minimum_version = ssl.TLSVersion.TLSv1_2 |     context.minimum_version = ssl.TLSVersion.TLSv1_2 | ||||||
|     context.set_ciphers(":".join(CIPHERS_TLS12)) |     context.set_ciphers(":".join(CIPHERS_TLS12)) | ||||||
|     context.set_alpn_protocols(["http/1.1"]) |     context.set_alpn_protocols(["http/1.1"]) | ||||||
|     if purpose is ssl.Purpose.CLIENT_AUTH: |  | ||||||
|     context.sni_callback = server_name_callback |     context.sni_callback = server_name_callback | ||||||
|     if certfile and keyfile: |     if certfile and keyfile: | ||||||
|         context.load_cert_chain(certfile, keyfile, password) |         context.load_cert_chain(certfile, keyfile, password) | ||||||
|   | |||||||
| @@ -72,8 +72,7 @@ def get_ssl_context( | |||||||
|             "without passing a TLS certificate. If you are developing " |             "without passing a TLS certificate. If you are developing " | ||||||
|             "locally, please enable DEVELOPMENT mode and Sanic will " |             "locally, please enable DEVELOPMENT mode and Sanic will " | ||||||
|             "generate a localhost TLS certificate. For more information " |             "generate a localhost TLS certificate. For more information " | ||||||
|             "please see: https://sanic.dev/en/guide/deployment/development." |             "please see: ___." | ||||||
|             "html#automatic-tls-certificate." |  | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     creator = CertCreator.select( |     creator = CertCreator.select( | ||||||
| @@ -126,6 +125,7 @@ class CertCreator(ABC): | |||||||
|         local_tls_key, |         local_tls_key, | ||||||
|         local_tls_cert, |         local_tls_cert, | ||||||
|     ) -> CertCreator: |     ) -> CertCreator: | ||||||
|  |  | ||||||
|         creator: Optional[CertCreator] = None |         creator: Optional[CertCreator] = None | ||||||
|  |  | ||||||
|         cert_creator_options: Tuple[ |         cert_creator_options: Tuple[ | ||||||
| @@ -151,8 +151,7 @@ class CertCreator(ABC): | |||||||
|             raise SanicException( |             raise SanicException( | ||||||
|                 "Sanic could not find package to create a TLS certificate. " |                 "Sanic could not find package to create a TLS certificate. " | ||||||
|                 "You must have either mkcert or trustme installed. See " |                 "You must have either mkcert or trustme installed. See " | ||||||
|                 "https://sanic.dev/en/guide/deployment/development.html" |                 "_____ for more details." | ||||||
|                 "#automatic-tls-certificate for more details." |  | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         return creator |         return creator | ||||||
| @@ -204,8 +203,7 @@ class MkcertCreator(CertCreator): | |||||||
|                 "to proceed. Installation instructions can be found here: " |                 "to proceed. Installation instructions can be found here: " | ||||||
|                 "https://github.com/FiloSottile/mkcert.\n" |                 "https://github.com/FiloSottile/mkcert.\n" | ||||||
|                 "Find out more information about your options here: " |                 "Find out more information about your options here: " | ||||||
|                 "https://sanic.dev/en/guide/deployment/development.html#" |                 "_____" | ||||||
|                 "automatic-tls-certificate" |  | ||||||
|             ) from e |             ) from e | ||||||
|  |  | ||||||
|     def generate_cert(self, localhost: str) -> ssl.SSLContext: |     def generate_cert(self, localhost: str) -> ssl.SSLContext: | ||||||
| @@ -262,8 +260,7 @@ class TrustmeCreator(CertCreator): | |||||||
|                 "to proceed. Installation instructions can be found here: " |                 "to proceed. Installation instructions can be found here: " | ||||||
|                 "https://github.com/python-trio/trustme.\n" |                 "https://github.com/python-trio/trustme.\n" | ||||||
|                 "Find out more information about your options here: " |                 "Find out more information about your options here: " | ||||||
|                 "https://sanic.dev/en/guide/deployment/development.html#" |                 "_____" | ||||||
|                 "automatic-tls-certificate" |  | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|     def generate_cert(self, localhost: str) -> ssl.SSLContext: |     def generate_cert(self, localhost: str) -> ssl.SSLContext: | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								sanic/log.py
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								sanic/log.py
									
									
									
									
									
								
							| @@ -2,23 +2,12 @@ import logging | |||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from enum import Enum | from enum import Enum | ||||||
| from typing import TYPE_CHECKING, Any, Dict | from typing import Any, Dict | ||||||
| from warnings import warn | from warnings import warn | ||||||
|  |  | ||||||
| from sanic.compat import is_atty | from sanic.compat import is_atty | ||||||
|  |  | ||||||
|  |  | ||||||
| # Python 3.11 changed the way Enum formatting works for mixed-in types. |  | ||||||
| if sys.version_info < (3, 11, 0): |  | ||||||
|  |  | ||||||
|     class StrEnum(str, Enum): |  | ||||||
|         pass |  | ||||||
|  |  | ||||||
| else: |  | ||||||
|     if not TYPE_CHECKING: |  | ||||||
|         from enum import StrEnum |  | ||||||
|  |  | ||||||
|  |  | ||||||
| LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(  # no cov | LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(  # no cov | ||||||
|     version=1, |     version=1, | ||||||
|     disable_existing_loggers=False, |     disable_existing_loggers=False, | ||||||
| @@ -36,12 +25,6 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(  # no cov | |||||||
|             "propagate": True, |             "propagate": True, | ||||||
|             "qualname": "sanic.access", |             "qualname": "sanic.access", | ||||||
|         }, |         }, | ||||||
|         "sanic.server": { |  | ||||||
|             "level": "INFO", |  | ||||||
|             "handlers": ["console"], |  | ||||||
|             "propagate": True, |  | ||||||
|             "qualname": "sanic.server", |  | ||||||
|         }, |  | ||||||
|     }, |     }, | ||||||
|     handlers={ |     handlers={ | ||||||
|         "console": { |         "console": { | ||||||
| @@ -79,7 +62,7 @@ Defult logging configuration | |||||||
| """ | """ | ||||||
|  |  | ||||||
|  |  | ||||||
| class Colors(StrEnum):  # no cov | class Colors(str, Enum):  # no cov | ||||||
|     END = "\033[0m" |     END = "\033[0m" | ||||||
|     BOLD = "\033[1m" |     BOLD = "\033[1m" | ||||||
|     BLUE = "\033[34m" |     BLUE = "\033[34m" | ||||||
| @@ -118,12 +101,6 @@ Logger used by Sanic for access logging | |||||||
| """ | """ | ||||||
| access_logger.addFilter(_verbosity_filter) | access_logger.addFilter(_verbosity_filter) | ||||||
|  |  | ||||||
| server_logger = logging.getLogger("sanic.server")  # no cov |  | ||||||
| """ |  | ||||||
| Logger used by Sanic for server related messages |  | ||||||
| """ |  | ||||||
| logger.addFilter(_verbosity_filter) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def deprecation(message: str, version: float):  # no cov | def deprecation(message: str, version: float):  # no cov | ||||||
|     version_info = f"[DEPRECATION v{version}] " |     version_info = f"[DEPRECATION v{version}] " | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ class Middleware: | |||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         func: MiddlewareType, |         func: MiddlewareType, | ||||||
|         location: MiddlewareLocation, |         location: MiddlewareLocation = MiddlewareLocation.REQUEST, | ||||||
|         priority: int = 0, |         priority: int = 0, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         self.func = func |         self.func = func | ||||||
| @@ -32,13 +32,11 @@ class Middleware: | |||||||
|     def __call__(self, *args, **kwargs): |     def __call__(self, *args, **kwargs): | ||||||
|         return self.func(*args, **kwargs) |         return self.func(*args, **kwargs) | ||||||
|  |  | ||||||
|     def __hash__(self) -> int: |  | ||||||
|         return hash(self.func) |  | ||||||
|  |  | ||||||
|     def __repr__(self) -> str: |     def __repr__(self) -> str: | ||||||
|  |         name = getattr(self.func, "__name__", str(self.func)) | ||||||
|         return ( |         return ( | ||||||
|             f"{self.__class__.__name__}(" |             f"{self.__class__.__name__}(" | ||||||
|             f"func=<function {self.func.__name__}>, " |             f"func=<function {name}>, " | ||||||
|             f"priority={self.priority}, " |             f"priority={self.priority}, " | ||||||
|             f"location={self.location.name})" |             f"location={self.location.name})" | ||||||
|         ) |         ) | ||||||
| @@ -66,4 +64,3 @@ class Middleware: | |||||||
|     @classmethod |     @classmethod | ||||||
|     def reset_count(cls): |     def reset_count(cls): | ||||||
|         cls._counter = count() |         cls._counter = count() | ||||||
|         cls.count = next(cls._counter) |  | ||||||
|   | |||||||
| @@ -1,35 +0,0 @@ | |||||||
| from typing import Optional |  | ||||||
|  |  | ||||||
| from sanic.base.meta import SanicMeta |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class BaseMixin(metaclass=SanicMeta): |  | ||||||
|     name: str |  | ||||||
|     strict_slashes: Optional[bool] |  | ||||||
|  |  | ||||||
|     def _generate_name(self, *objects) -> str: |  | ||||||
|         name = None |  | ||||||
|  |  | ||||||
|         for obj in objects: |  | ||||||
|             if obj: |  | ||||||
|                 if isinstance(obj, str): |  | ||||||
|                     name = obj |  | ||||||
|                     break |  | ||||||
|  |  | ||||||
|                 try: |  | ||||||
|                     name = obj.name |  | ||||||
|                 except AttributeError: |  | ||||||
|                     try: |  | ||||||
|                         name = obj.__name__ |  | ||||||
|                     except AttributeError: |  | ||||||
|                         continue |  | ||||||
|                 else: |  | ||||||
|                     break |  | ||||||
|  |  | ||||||
|         if not name:  # noqa |  | ||||||
|             raise ValueError("Could not generate a name for handler") |  | ||||||
|  |  | ||||||
|         if not name.startswith(f"{self.name}."): |  | ||||||
|             name = f"{self.name}.{name}" |  | ||||||
|  |  | ||||||
|         return name |  | ||||||
| @@ -4,6 +4,7 @@ from operator import attrgetter | |||||||
| from typing import List | from typing import List | ||||||
|  |  | ||||||
| from sanic.base.meta import SanicMeta | from sanic.base.meta import SanicMeta | ||||||
|  | from sanic.handlers import RequestHandler | ||||||
| from sanic.middleware import Middleware, MiddlewareLocation | from sanic.middleware import Middleware, MiddlewareLocation | ||||||
| from sanic.models.futures import FutureMiddleware | from sanic.models.futures import FutureMiddleware | ||||||
| from sanic.router import Router | from sanic.router import Router | ||||||
| @@ -104,19 +105,23 @@ class MiddlewareMixin(metaclass=SanicMeta): | |||||||
|                 self.named_response_middleware.get(route.name, deque()), |                 self.named_response_middleware.get(route.name, deque()), | ||||||
|                 location=MiddlewareLocation.RESPONSE, |                 location=MiddlewareLocation.RESPONSE, | ||||||
|             ) |             ) | ||||||
|             route.extra.request_middleware = deque( |  | ||||||
|  |             route.handler = RequestHandler( | ||||||
|  |                 route.handler, | ||||||
|  |                 deque( | ||||||
|                     sorted( |                     sorted( | ||||||
|                         request_middleware, |                         request_middleware, | ||||||
|                         key=attrgetter("order"), |                         key=attrgetter("order"), | ||||||
|                         reverse=True, |                         reverse=True, | ||||||
|                     ) |                     ) | ||||||
|             ) |                 ), | ||||||
|             route.extra.response_middleware = deque( |                 deque( | ||||||
|                     sorted( |                     sorted( | ||||||
|                         response_middleware, |                         response_middleware, | ||||||
|                         key=attrgetter("order"), |                         key=attrgetter("order"), | ||||||
|                         reverse=True, |                         reverse=True, | ||||||
|                     )[::-1] |                     )[::-1] | ||||||
|  |                 ), | ||||||
|             ) |             ) | ||||||
|         request_middleware = Middleware.convert( |         request_middleware = Middleware.convert( | ||||||
|             self.request_middleware, |             self.request_middleware, | ||||||
|   | |||||||
| @@ -1,11 +1,15 @@ | |||||||
| from ast import NodeVisitor, Return, parse | from ast import NodeVisitor, Return, parse | ||||||
| from contextlib import suppress | from contextlib import suppress | ||||||
|  | from functools import partial, wraps | ||||||
| from inspect import getsource, signature | from inspect import getsource, signature | ||||||
|  | from mimetypes import guess_type | ||||||
|  | from os import path | ||||||
|  | from pathlib import Path, PurePath | ||||||
| from textwrap import dedent | from textwrap import dedent | ||||||
|  | from time import gmtime, strftime | ||||||
| from typing import ( | from typing import ( | ||||||
|     Any, |     Any, | ||||||
|     Callable, |     Callable, | ||||||
|     Dict, |  | ||||||
|     Iterable, |     Iterable, | ||||||
|     List, |     List, | ||||||
|     Optional, |     Optional, | ||||||
| @@ -14,31 +18,50 @@ from typing import ( | |||||||
|     Union, |     Union, | ||||||
|     cast, |     cast, | ||||||
| ) | ) | ||||||
|  | from urllib.parse import unquote | ||||||
|  |  | ||||||
| from sanic_routing.route import Route | from sanic_routing.route import Route | ||||||
|  |  | ||||||
| from sanic.base.meta import SanicMeta | from sanic.base.meta import SanicMeta | ||||||
| from sanic.constants import HTTP_METHODS | from sanic.compat import stat_async | ||||||
|  | from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS | ||||||
| from sanic.errorpages import RESPONSE_MAPPING | from sanic.errorpages import RESPONSE_MAPPING | ||||||
| from sanic.mixins.base import BaseMixin | from sanic.exceptions import FileNotFound, HeaderNotFound, RangeNotSatisfiable | ||||||
|  | from sanic.handlers import ContentRangeHandler | ||||||
|  | from sanic.log import error_logger | ||||||
| from sanic.models.futures import FutureRoute, FutureStatic | from sanic.models.futures import FutureRoute, FutureStatic | ||||||
| from sanic.models.handler_types import RouteHandler | from sanic.models.handler_types import RouteHandler | ||||||
|  | from sanic.response import HTTPResponse, file, file_stream | ||||||
| from sanic.types import HashableDict | from sanic.types import HashableDict | ||||||
|  |  | ||||||
|  |  | ||||||
| RouteWrapper = Callable[ | RouteWrapper = Callable[ | ||||||
|     [RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]] |     [RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]] | ||||||
| ] | ] | ||||||
|  | RESTRICTED_ROUTE_CONTEXT = ( | ||||||
|  |     "ignore_body", | ||||||
|  |     "stream", | ||||||
|  |     "hosts", | ||||||
|  |     "static", | ||||||
|  |     "error_format", | ||||||
|  |     "websocket", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class RouteMixin(BaseMixin, metaclass=SanicMeta): | class RouteMixin(metaclass=SanicMeta): | ||||||
|  |     name: str | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs) -> None: |     def __init__(self, *args, **kwargs) -> None: | ||||||
|         self._future_routes: Set[FutureRoute] = set() |         self._future_routes: Set[FutureRoute] = set() | ||||||
|         self._future_statics: Set[FutureStatic] = set() |         self._future_statics: Set[FutureStatic] = set() | ||||||
|  |         self.strict_slashes: Optional[bool] = False | ||||||
|  |  | ||||||
|     def _apply_route(self, route: FutureRoute) -> List[Route]: |     def _apply_route(self, route: FutureRoute) -> List[Route]: | ||||||
|         raise NotImplementedError  # noqa |         raise NotImplementedError  # noqa | ||||||
|  |  | ||||||
|  |     def _apply_static(self, static: FutureStatic) -> Route: | ||||||
|  |         raise NotImplementedError  # noqa | ||||||
|  |  | ||||||
|     def route( |     def route( | ||||||
|         self, |         self, | ||||||
|         uri: str, |         uri: str, | ||||||
| @@ -202,7 +225,6 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta): | |||||||
|         stream: bool = False, |         stream: bool = False, | ||||||
|         version_prefix: str = "/v", |         version_prefix: str = "/v", | ||||||
|         error_format: Optional[str] = None, |         error_format: Optional[str] = None, | ||||||
|         unquote: bool = False, |  | ||||||
|         **ctx_kwargs: Any, |         **ctx_kwargs: Any, | ||||||
|     ) -> RouteHandler: |     ) -> RouteHandler: | ||||||
|         """A helper method to register class instance or |         """A helper method to register class instance or | ||||||
| @@ -249,7 +271,6 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta): | |||||||
|             name=name, |             name=name, | ||||||
|             version_prefix=version_prefix, |             version_prefix=version_prefix, | ||||||
|             error_format=error_format, |             error_format=error_format, | ||||||
|             unquote=unquote, |  | ||||||
|             **ctx_kwargs, |             **ctx_kwargs, | ||||||
|         )(handler) |         )(handler) | ||||||
|         return handler |         return handler | ||||||
| @@ -672,6 +693,319 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta): | |||||||
|             **ctx_kwargs, |             **ctx_kwargs, | ||||||
|         )(handler) |         )(handler) | ||||||
|  |  | ||||||
|  |     def static( | ||||||
|  |         self, | ||||||
|  |         uri: str, | ||||||
|  |         file_or_directory: Union[str, bytes, PurePath], | ||||||
|  |         pattern: str = r"/?.+", | ||||||
|  |         use_modified_since: bool = True, | ||||||
|  |         use_content_range: bool = False, | ||||||
|  |         stream_large_files: bool = False, | ||||||
|  |         name: str = "static", | ||||||
|  |         host: Optional[str] = None, | ||||||
|  |         strict_slashes: Optional[bool] = None, | ||||||
|  |         content_type: Optional[bool] = None, | ||||||
|  |         apply: bool = True, | ||||||
|  |         resource_type: Optional[str] = None, | ||||||
|  |     ): | ||||||
|  |         """ | ||||||
|  |         Register a root to serve files from. The input can either be a | ||||||
|  |         file or a directory. This method will enable an easy and simple way | ||||||
|  |         to setup the :class:`Route` necessary to serve the static files. | ||||||
|  |  | ||||||
|  |         :param uri: URL path to be used for serving static content | ||||||
|  |         :param file_or_directory: Path for the Static file/directory with | ||||||
|  |             static files | ||||||
|  |         :param pattern: Regex Pattern identifying the valid static files | ||||||
|  |         :param use_modified_since: If true, send file modified time, and return | ||||||
|  |             not modified if the browser's matches the server's | ||||||
|  |         :param use_content_range: If true, process header for range requests | ||||||
|  |             and sends the file part that is requested | ||||||
|  |         :param stream_large_files: If true, use the | ||||||
|  |             :func:`StreamingHTTPResponse.file_stream` handler rather | ||||||
|  |             than the :func:`HTTPResponse.file` handler to send the file. | ||||||
|  |             If this is an integer, this represents the threshold size to | ||||||
|  |             switch to :func:`StreamingHTTPResponse.file_stream` | ||||||
|  |         :param name: user defined name used for url_for | ||||||
|  |         :param host: Host IP or FQDN for the service to use | ||||||
|  |         :param strict_slashes: Instruct :class:`Sanic` to check if the request | ||||||
|  |             URLs need to terminate with a */* | ||||||
|  |         :param content_type: user defined content type for header | ||||||
|  |         :return: routes registered on the router | ||||||
|  |         :rtype: List[sanic.router.Route] | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         name = self._generate_name(name) | ||||||
|  |  | ||||||
|  |         if strict_slashes is None and self.strict_slashes is not None: | ||||||
|  |             strict_slashes = self.strict_slashes | ||||||
|  |  | ||||||
|  |         if not isinstance(file_or_directory, (str, bytes, PurePath)): | ||||||
|  |             raise ValueError( | ||||||
|  |                 f"Static route must be a valid path, not {file_or_directory}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         static = FutureStatic( | ||||||
|  |             uri, | ||||||
|  |             file_or_directory, | ||||||
|  |             pattern, | ||||||
|  |             use_modified_since, | ||||||
|  |             use_content_range, | ||||||
|  |             stream_large_files, | ||||||
|  |             name, | ||||||
|  |             host, | ||||||
|  |             strict_slashes, | ||||||
|  |             content_type, | ||||||
|  |             resource_type, | ||||||
|  |         ) | ||||||
|  |         self._future_statics.add(static) | ||||||
|  |  | ||||||
|  |         if apply: | ||||||
|  |             self._apply_static(static) | ||||||
|  |  | ||||||
|  |     def _generate_name(self, *objects) -> str: | ||||||
|  |         name = None | ||||||
|  |  | ||||||
|  |         for obj in objects: | ||||||
|  |             if obj: | ||||||
|  |                 if isinstance(obj, str): | ||||||
|  |                     name = obj | ||||||
|  |                     break | ||||||
|  |  | ||||||
|  |                 try: | ||||||
|  |                     name = obj.name | ||||||
|  |                 except AttributeError: | ||||||
|  |                     try: | ||||||
|  |                         name = obj.__name__ | ||||||
|  |                     except AttributeError: | ||||||
|  |                         continue | ||||||
|  |                 else: | ||||||
|  |                     break | ||||||
|  |  | ||||||
|  |         if not name:  # noqa | ||||||
|  |             raise ValueError("Could not generate a name for handler") | ||||||
|  |  | ||||||
|  |         if not name.startswith(f"{self.name}."): | ||||||
|  |             name = f"{self.name}.{name}" | ||||||
|  |  | ||||||
|  |         return name | ||||||
|  |  | ||||||
|  |     async def _static_request_handler( | ||||||
|  |         self, | ||||||
|  |         file_or_directory, | ||||||
|  |         use_modified_since, | ||||||
|  |         use_content_range, | ||||||
|  |         stream_large_files, | ||||||
|  |         request, | ||||||
|  |         content_type=None, | ||||||
|  |         __file_uri__=None, | ||||||
|  |     ): | ||||||
|  |         # Merge served directory and requested file if provided | ||||||
|  |         file_path_raw = Path(unquote(file_or_directory)) | ||||||
|  |         root_path = file_path = file_path_raw.resolve() | ||||||
|  |         not_found = FileNotFound( | ||||||
|  |             "File not found", | ||||||
|  |             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 | ||||||
|  |         try: | ||||||
|  |             headers = {} | ||||||
|  |             # Check if the client has been sent this file before | ||||||
|  |             # and it has not been modified since | ||||||
|  |             stats = None | ||||||
|  |             if use_modified_since: | ||||||
|  |                 stats = await stat_async(file_path) | ||||||
|  |                 modified_since = strftime( | ||||||
|  |                     "%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime) | ||||||
|  |                 ) | ||||||
|  |                 if ( | ||||||
|  |                     request.headers.getone("if-modified-since", None) | ||||||
|  |                     == modified_since | ||||||
|  |                 ): | ||||||
|  |                     return HTTPResponse(status=304) | ||||||
|  |                 headers["Last-Modified"] = modified_since | ||||||
|  |             _range = None | ||||||
|  |             if use_content_range: | ||||||
|  |                 _range = None | ||||||
|  |                 if not stats: | ||||||
|  |                     stats = await stat_async(file_path) | ||||||
|  |                 headers["Accept-Ranges"] = "bytes" | ||||||
|  |                 headers["Content-Length"] = str(stats.st_size) | ||||||
|  |                 if request.method != "HEAD": | ||||||
|  |                     try: | ||||||
|  |                         _range = ContentRangeHandler(request, stats) | ||||||
|  |                     except HeaderNotFound: | ||||||
|  |                         pass | ||||||
|  |                     else: | ||||||
|  |                         del headers["Content-Length"] | ||||||
|  |                         for key, value in _range.headers.items(): | ||||||
|  |                             headers[key] = value | ||||||
|  |  | ||||||
|  |             if "content-type" not in headers: | ||||||
|  |                 content_type = ( | ||||||
|  |                     content_type | ||||||
|  |                     or guess_type(file_path)[0] | ||||||
|  |                     or DEFAULT_HTTP_CONTENT_TYPE | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |                 if "charset=" not in content_type and ( | ||||||
|  |                     content_type.startswith("text/") | ||||||
|  |                     or content_type == "application/javascript" | ||||||
|  |                 ): | ||||||
|  |                     content_type += "; charset=utf-8" | ||||||
|  |  | ||||||
|  |                 headers["Content-Type"] = content_type | ||||||
|  |  | ||||||
|  |             if request.method == "HEAD": | ||||||
|  |                 return HTTPResponse(headers=headers) | ||||||
|  |             else: | ||||||
|  |                 if stream_large_files: | ||||||
|  |                     if type(stream_large_files) == int: | ||||||
|  |                         threshold = stream_large_files | ||||||
|  |                     else: | ||||||
|  |                         threshold = 1024 * 1024 | ||||||
|  |  | ||||||
|  |                     if not stats: | ||||||
|  |                         stats = await stat_async(file_path) | ||||||
|  |                     if stats.st_size >= threshold: | ||||||
|  |                         return await file_stream( | ||||||
|  |                             file_path, headers=headers, _range=_range | ||||||
|  |                         ) | ||||||
|  |                 return await file(file_path, headers=headers, _range=_range) | ||||||
|  |         except RangeNotSatisfiable: | ||||||
|  |             raise | ||||||
|  |         except FileNotFoundError: | ||||||
|  |             raise not_found | ||||||
|  |         except Exception: | ||||||
|  |             error_logger.exception( | ||||||
|  |                 f"Exception in static request handler: " | ||||||
|  |                 f"path={file_or_directory}, " | ||||||
|  |                 f"relative_url={__file_uri__}" | ||||||
|  |             ) | ||||||
|  |             raise | ||||||
|  |  | ||||||
|  |     def _register_static( | ||||||
|  |         self, | ||||||
|  |         static: FutureStatic, | ||||||
|  |     ): | ||||||
|  |         # TODO: Though sanic is not a file server, I feel like we should | ||||||
|  |         # at least make a good effort here.  Modified-since is nice, but | ||||||
|  |         # we could also look into etags, expires, and caching | ||||||
|  |         """ | ||||||
|  |         Register a static directory handler with Sanic by adding a route to the | ||||||
|  |         router and registering a handler. | ||||||
|  |  | ||||||
|  |         :param app: Sanic | ||||||
|  |         :param file_or_directory: File or directory path to serve from | ||||||
|  |         :type file_or_directory: Union[str,bytes,Path] | ||||||
|  |         :param uri: URL to serve from | ||||||
|  |         :type uri: str | ||||||
|  |         :param pattern: regular expression used to match files in the URL | ||||||
|  |         :param use_modified_since: If true, send file modified time, and return | ||||||
|  |                                 not modified if the browser's matches the | ||||||
|  |                                 server's | ||||||
|  |         :param use_content_range: If true, process header for range requests | ||||||
|  |                                 and sends the file part that is requested | ||||||
|  |         :param stream_large_files: If true, use the file_stream() handler | ||||||
|  |                                 rather than the file() handler to send the file | ||||||
|  |                                 If this is an integer, this represents the | ||||||
|  |                                 threshold size to switch to file_stream() | ||||||
|  |         :param name: user defined name used for url_for | ||||||
|  |         :type name: str | ||||||
|  |         :param content_type: user defined content type for header | ||||||
|  |         :return: registered static routes | ||||||
|  |         :rtype: List[sanic.router.Route] | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if isinstance(static.file_or_directory, bytes): | ||||||
|  |             file_or_directory = static.file_or_directory.decode("utf-8") | ||||||
|  |         elif isinstance(static.file_or_directory, PurePath): | ||||||
|  |             file_or_directory = str(static.file_or_directory) | ||||||
|  |         elif not isinstance(static.file_or_directory, str): | ||||||
|  |             raise ValueError("Invalid file path string.") | ||||||
|  |         else: | ||||||
|  |             file_or_directory = static.file_or_directory | ||||||
|  |  | ||||||
|  |         uri = static.uri | ||||||
|  |         name = static.name | ||||||
|  |         # If we're not trying to match a file directly, | ||||||
|  |         # serve from the folder | ||||||
|  |         if not static.resource_type: | ||||||
|  |             if not path.isfile(file_or_directory): | ||||||
|  |                 uri = uri.rstrip("/") | ||||||
|  |                 uri += "/<__file_uri__:path>" | ||||||
|  |         elif static.resource_type == "dir": | ||||||
|  |             if path.isfile(file_or_directory): | ||||||
|  |                 raise TypeError( | ||||||
|  |                     "Resource type improperly identified as directory. " | ||||||
|  |                     f"'{file_or_directory}'" | ||||||
|  |                 ) | ||||||
|  |             uri = uri.rstrip("/") | ||||||
|  |             uri += "/<__file_uri__:path>" | ||||||
|  |         elif static.resource_type == "file" and not path.isfile( | ||||||
|  |             file_or_directory | ||||||
|  |         ): | ||||||
|  |             raise TypeError( | ||||||
|  |                 "Resource type improperly identified as file. " | ||||||
|  |                 f"'{file_or_directory}'" | ||||||
|  |             ) | ||||||
|  |         elif static.resource_type != "file": | ||||||
|  |             raise ValueError( | ||||||
|  |                 "The resource_type should be set to 'file' or 'dir'" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # special prefix for static files | ||||||
|  |         # if not static.name.startswith("_static_"): | ||||||
|  |         #     name = f"_static_{static.name}" | ||||||
|  |  | ||||||
|  |         _handler = wraps(self._static_request_handler)( | ||||||
|  |             partial( | ||||||
|  |                 self._static_request_handler, | ||||||
|  |                 file_or_directory, | ||||||
|  |                 static.use_modified_since, | ||||||
|  |                 static.use_content_range, | ||||||
|  |                 static.stream_large_files, | ||||||
|  |                 content_type=static.content_type, | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         route, _ = self.route(  # type: ignore | ||||||
|  |             uri=uri, | ||||||
|  |             methods=["GET", "HEAD"], | ||||||
|  |             name=name, | ||||||
|  |             host=static.host, | ||||||
|  |             strict_slashes=static.strict_slashes, | ||||||
|  |             static=True, | ||||||
|  |         )(_handler) | ||||||
|  |  | ||||||
|  |         return route | ||||||
|  |  | ||||||
|     def _determine_error_format(self, handler) -> str: |     def _determine_error_format(self, handler) -> str: | ||||||
|         with suppress(OSError, TypeError): |         with suppress(OSError, TypeError): | ||||||
|             src = dedent(getsource(handler)) |             src = dedent(getsource(handler)) | ||||||
| @@ -707,12 +1041,24 @@ class RouteMixin(BaseMixin, metaclass=SanicMeta): | |||||||
|  |  | ||||||
|         return types |         return types | ||||||
|  |  | ||||||
|     def _build_route_context(self, raw: Dict[str, Any]) -> HashableDict: |     def _build_route_context(self, raw): | ||||||
|         ctx_kwargs = { |         ctx_kwargs = { | ||||||
|             key.replace("ctx_", ""): raw.pop(key) |             key.replace("ctx_", ""): raw.pop(key) | ||||||
|             for key in {**raw}.keys() |             for key in {**raw}.keys() | ||||||
|             if key.startswith("ctx_") |             if key.startswith("ctx_") | ||||||
|         } |         } | ||||||
|  |         restricted = [ | ||||||
|  |             key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT | ||||||
|  |         ] | ||||||
|  |         if restricted: | ||||||
|  |             restricted_arguments = ", ".join(restricted) | ||||||
|  |             raise AttributeError( | ||||||
|  |                 "Cannot use restricted route context: " | ||||||
|  |                 f"{restricted_arguments}. This limitation is only in place " | ||||||
|  |                 "until v22.9 when the restricted names will no longer be in" | ||||||
|  |                 "conflict. See https://github.com/sanic-org/sanic/issues/2303 " | ||||||
|  |                 "for more information." | ||||||
|  |             ) | ||||||
|         if raw: |         if raw: | ||||||
|             unexpected_arguments = ", ".join(raw.keys()) |             unexpected_arguments = ", ".join(raw.keys()) | ||||||
|             raise TypeError( |             raise TypeError( | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ class SignalMixin(metaclass=SanicMeta): | |||||||
|         event: Union[str, Enum], |         event: Union[str, Enum], | ||||||
|         *, |         *, | ||||||
|         apply: bool = True, |         apply: bool = True, | ||||||
|         condition: Optional[Dict[str, Any]] = None, |         condition: Dict[str, Any] = None, | ||||||
|         exclusive: bool = True, |         exclusive: bool = True, | ||||||
|     ) -> Callable[[SignalHandler], SignalHandler]: |     ) -> Callable[[SignalHandler], SignalHandler]: | ||||||
|         """ |         """ | ||||||
| @@ -64,7 +64,7 @@ class SignalMixin(metaclass=SanicMeta): | |||||||
|         self, |         self, | ||||||
|         handler: Optional[Callable[..., Any]], |         handler: Optional[Callable[..., Any]], | ||||||
|         event: str, |         event: str, | ||||||
|         condition: Optional[Dict[str, Any]] = None, |         condition: Dict[str, Any] = None, | ||||||
|         exclusive: bool = True, |         exclusive: bool = True, | ||||||
|     ): |     ): | ||||||
|         if not handler: |         if not handler: | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ from importlib import import_module | |||||||
| from multiprocessing import Manager, Pipe, get_context | from multiprocessing import Manager, Pipe, get_context | ||||||
| from multiprocessing.context import BaseContext | from multiprocessing.context import BaseContext | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from socket import SHUT_RDWR, socket | from socket import socket | ||||||
| from ssl import SSLContext | from ssl import SSLContext | ||||||
| from typing import ( | from typing import ( | ||||||
|     TYPE_CHECKING, |     TYPE_CHECKING, | ||||||
| @@ -27,7 +27,6 @@ from typing import ( | |||||||
|     Callable, |     Callable, | ||||||
|     Dict, |     Dict, | ||||||
|     List, |     List, | ||||||
|     Mapping, |  | ||||||
|     Optional, |     Optional, | ||||||
|     Set, |     Set, | ||||||
|     Tuple, |     Tuple, | ||||||
| @@ -36,14 +35,12 @@ from typing import ( | |||||||
|     cast, |     cast, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| from sanic.application.ext import setup_ext |  | ||||||
| from sanic.application.logo import get_logo | from sanic.application.logo import get_logo | ||||||
| from sanic.application.motd import MOTD | from sanic.application.motd import MOTD | ||||||
| from sanic.application.state import ApplicationServerInfo, Mode, ServerStage | from sanic.application.state import ApplicationServerInfo, Mode, ServerStage | ||||||
| from sanic.base.meta import SanicMeta | from sanic.base.meta import SanicMeta | ||||||
| from sanic.compat import OS_IS_WINDOWS, StartMethod, is_atty | from sanic.compat import OS_IS_WINDOWS, is_atty | ||||||
| from sanic.exceptions import ServerKilled | from sanic.helpers import _default | ||||||
| from sanic.helpers import Default, _default |  | ||||||
| from sanic.http.constants import HTTP | from sanic.http.constants import HTTP | ||||||
| from sanic.http.tls import get_ssl_context, process_to_context | from sanic.http.tls import get_ssl_context, process_to_context | ||||||
| from sanic.http.tls.context import SanicSSLContext | from sanic.http.tls.context import SanicSSLContext | ||||||
| @@ -59,6 +56,7 @@ from sanic.server.protocols.http_protocol import HttpProtocol | |||||||
| from sanic.server.protocols.websocket_protocol import WebSocketProtocol | from sanic.server.protocols.websocket_protocol import WebSocketProtocol | ||||||
| from sanic.server.runners import serve, serve_multiple, serve_single | from sanic.server.runners import serve, serve_multiple, serve_single | ||||||
| from sanic.server.socket import configure_socket, remove_unix_socket | from sanic.server.socket import configure_socket, remove_unix_socket | ||||||
|  | from sanic.worker.inspector import Inspector | ||||||
| from sanic.worker.loader import AppLoader | from sanic.worker.loader import AppLoader | ||||||
| from sanic.worker.manager import WorkerManager | from sanic.worker.manager import WorkerManager | ||||||
| from sanic.worker.multiplexer import WorkerMultiplexer | from sanic.worker.multiplexer import WorkerMultiplexer | ||||||
| @@ -88,13 +86,11 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|     state: ApplicationState |     state: ApplicationState | ||||||
|     websocket_enabled: bool |     websocket_enabled: bool | ||||||
|     multiplexer: WorkerMultiplexer |     multiplexer: WorkerMultiplexer | ||||||
|     start_method: StartMethod = _default |  | ||||||
|  |  | ||||||
|     def setup_loop(self): |     def setup_loop(self): | ||||||
|         if not self.asgi: |         if not self.asgi: | ||||||
|             if self.config.USE_UVLOOP is True or ( |             if self.config.USE_UVLOOP is True or ( | ||||||
|                 isinstance(self.config.USE_UVLOOP, Default) |                 self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS | ||||||
|                 and not OS_IS_WINDOWS |  | ||||||
|             ): |             ): | ||||||
|                 try_use_uvloop() |                 try_use_uvloop() | ||||||
|             elif OS_IS_WINDOWS: |             elif OS_IS_WINDOWS: | ||||||
| @@ -126,7 +122,7 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|         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, | ||||||
|         loop: Optional[AbstractEventLoop] = None, |         loop: AbstractEventLoop = None, | ||||||
|         reload_dir: Optional[Union[List[str], str]] = None, |         reload_dir: Optional[Union[List[str], str]] = None, | ||||||
|         noisy_exceptions: Optional[bool] = None, |         noisy_exceptions: Optional[bool] = None, | ||||||
|         motd: bool = True, |         motd: bool = True, | ||||||
| @@ -225,7 +221,7 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|         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, | ||||||
|         loop: Optional[AbstractEventLoop] = None, |         loop: AbstractEventLoop = None, | ||||||
|         reload_dir: Optional[Union[List[str], str]] = None, |         reload_dir: Optional[Union[List[str], str]] = None, | ||||||
|         noisy_exceptions: Optional[bool] = None, |         noisy_exceptions: Optional[bool] = None, | ||||||
|         motd: bool = True, |         motd: bool = True, | ||||||
| @@ -355,12 +351,12 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|         debug: bool = False, |         debug: bool = False, | ||||||
|         ssl: Union[None, SSLContext, dict, str, list, tuple] = None, |         ssl: Union[None, SSLContext, dict, str, list, tuple] = None, | ||||||
|         sock: Optional[socket] = None, |         sock: Optional[socket] = None, | ||||||
|         protocol: Optional[Type[Protocol]] = None, |         protocol: Type[Protocol] = None, | ||||||
|         backlog: int = 100, |         backlog: int = 100, | ||||||
|         access_log: Optional[bool] = None, |         access_log: Optional[bool] = None, | ||||||
|         unix: Optional[str] = None, |         unix: Optional[str] = None, | ||||||
|         return_asyncio_server: bool = False, |         return_asyncio_server: bool = False, | ||||||
|         asyncio_server_kwargs: Optional[Dict[str, Any]] = None, |         asyncio_server_kwargs: Dict[str, Any] = None, | ||||||
|         noisy_exceptions: Optional[bool] = None, |         noisy_exceptions: Optional[bool] = None, | ||||||
|     ) -> Optional[AsyncioServer]: |     ) -> Optional[AsyncioServer]: | ||||||
|         """ |         """ | ||||||
| @@ -434,7 +430,7 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|             run_async=return_asyncio_server, |             run_async=return_asyncio_server, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         if not isinstance(self.config.USE_UVLOOP, Default): |         if self.config.USE_UVLOOP is not _default: | ||||||
|             error_logger.warning( |             error_logger.warning( | ||||||
|                 "You are trying to change the uvloop configuration, but " |                 "You are trying to change the uvloop configuration, but " | ||||||
|                 "this is only effective when using the run(...) method. " |                 "this is only effective when using the run(...) method. " | ||||||
| @@ -481,7 +477,7 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|         sock: Optional[socket] = None, |         sock: Optional[socket] = None, | ||||||
|         unix: Optional[str] = None, |         unix: Optional[str] = None, | ||||||
|         workers: int = 1, |         workers: int = 1, | ||||||
|         loop: Optional[AbstractEventLoop] = None, |         loop: AbstractEventLoop = None, | ||||||
|         protocol: Type[Protocol] = HttpProtocol, |         protocol: Type[Protocol] = HttpProtocol, | ||||||
|         backlog: int = 100, |         backlog: int = 100, | ||||||
|         register_sys_signals: bool = True, |         register_sys_signals: bool = True, | ||||||
| @@ -562,6 +558,7 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|  |  | ||||||
|     def motd( |     def motd( | ||||||
|         self, |         self, | ||||||
|  |         serve_location: str = "", | ||||||
|         server_settings: Optional[Dict[str, Any]] = None, |         server_settings: Optional[Dict[str, Any]] = None, | ||||||
|     ): |     ): | ||||||
|         if ( |         if ( | ||||||
| @@ -571,6 +568,13 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|             or os.environ.get("SANIC_SERVER_RUNNING") |             or os.environ.get("SANIC_SERVER_RUNNING") | ||||||
|         ): |         ): | ||||||
|             return |             return | ||||||
|  |         if serve_location: | ||||||
|  |             deprecation( | ||||||
|  |                 "Specifying a serve_location in the MOTD is deprecated and " | ||||||
|  |                 "will be removed.", | ||||||
|  |                 22.9, | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|             serve_location = self.get_server_location(server_settings) |             serve_location = self.get_server_location(server_settings) | ||||||
|         if self.config.MOTD: |         if self.config.MOTD: | ||||||
|             logo = get_logo(coffee=self.state.coffee) |             logo = get_logo(coffee=self.state.coffee) | ||||||
| @@ -692,18 +696,13 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|     def should_auto_reload(cls) -> bool: |     def should_auto_reload(cls) -> bool: | ||||||
|         return any(app.state.auto_reload for app in cls._app_registry.values()) |         return any(app.state.auto_reload for app in cls._app_registry.values()) | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def _get_startup_method(cls) -> str: |  | ||||||
|         return ( |  | ||||||
|             cls.start_method |  | ||||||
|             if not isinstance(cls.start_method, Default) |  | ||||||
|             else "spawn" |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _get_context(cls) -> BaseContext: |     def _get_context(cls) -> BaseContext: | ||||||
|         method = cls._get_startup_method() |         method = ( | ||||||
|         logger.debug("Creating multiprocessing context using '%s'", method) |             "spawn" | ||||||
|  |             if "linux" not in sys.platform or cls.should_auto_reload() | ||||||
|  |             else "fork" | ||||||
|  |         ) | ||||||
|         return get_context(method) |         return get_context(method) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
| @@ -741,18 +740,15 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|         except IndexError: |         except IndexError: | ||||||
|             raise RuntimeError( |             raise RuntimeError( | ||||||
|                 f"No server information found for {primary.name}. Perhaps you " |                 f"No server information found for {primary.name}. Perhaps you " | ||||||
|                 "need to run app.prepare(...)?" |                 "need to run app.prepare(...)?\n" | ||||||
|  |                 "See ____ for more information." | ||||||
|             ) from None |             ) from None | ||||||
|  |  | ||||||
|         socks = [] |         socks = [] | ||||||
|         sync_manager = Manager() |         sync_manager = Manager() | ||||||
|         setup_ext(primary) |  | ||||||
|         exit_code = 0 |  | ||||||
|         try: |         try: | ||||||
|             primary_server_info.settings.pop("main_start", None) |             main_start = primary_server_info.settings.pop("main_start", None) | ||||||
|             primary_server_info.settings.pop("main_stop", None) |             main_stop = primary_server_info.settings.pop("main_stop", None) | ||||||
|             main_start = primary.listeners.get("main_process_start") |  | ||||||
|             main_stop = primary.listeners.get("main_process_stop") |  | ||||||
|             app = primary_server_info.settings.pop("app") |             app = primary_server_info.settings.pop("app") | ||||||
|             app.setup_loop() |             app.setup_loop() | ||||||
|             loop = new_event_loop() |             loop = new_event_loop() | ||||||
| @@ -769,7 +765,7 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|             ] |             ] | ||||||
|             primary_server_info.settings["run_multiple"] = True |             primary_server_info.settings["run_multiple"] = True | ||||||
|             monitor_sub, monitor_pub = Pipe(True) |             monitor_sub, monitor_pub = Pipe(True) | ||||||
|             worker_state: Mapping[str, Any] = sync_manager.dict() |             worker_state: Dict[str, Any] = sync_manager.dict() | ||||||
|             kwargs: Dict[str, Any] = { |             kwargs: Dict[str, Any] = { | ||||||
|                 **primary_server_info.settings, |                 **primary_server_info.settings, | ||||||
|                 "monitor_publisher": monitor_pub, |                 "monitor_publisher": monitor_pub, | ||||||
| @@ -825,7 +821,7 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|                 reload_dirs: Set[Path] = primary.state.reload_dirs.union( |                 reload_dirs: Set[Path] = primary.state.reload_dirs.union( | ||||||
|                     *(app.state.reload_dirs for app in apps) |                     *(app.state.reload_dirs for app in apps) | ||||||
|                 ) |                 ) | ||||||
|                 reloader = Reloader(monitor_pub, 0, reload_dirs, app_loader) |                 reloader = Reloader(monitor_pub, 1.0, reload_dirs, app_loader) | ||||||
|                 manager.manage("Reloader", reloader, {}, transient=False) |                 manager.manage("Reloader", reloader, {}, transient=False) | ||||||
|  |  | ||||||
|             inspector = None |             inspector = None | ||||||
| @@ -841,15 +837,12 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|                     "packages": [sanic_version, *packages], |                     "packages": [sanic_version, *packages], | ||||||
|                     "extra": extra, |                     "extra": extra, | ||||||
|                 } |                 } | ||||||
|                 inspector = primary.inspector_class( |                 inspector = Inspector( | ||||||
|                     monitor_pub, |                     monitor_pub, | ||||||
|                     app_info, |                     app_info, | ||||||
|                     worker_state, |                     worker_state, | ||||||
|                     primary.config.INSPECTOR_HOST, |                     primary.config.INSPECTOR_HOST, | ||||||
|                     primary.config.INSPECTOR_PORT, |                     primary.config.INSPECTOR_PORT, | ||||||
|                     primary.config.INSPECTOR_API_KEY, |  | ||||||
|                     primary.config.INSPECTOR_TLS_KEY, |  | ||||||
|                     primary.config.INSPECTOR_TLS_CERT, |  | ||||||
|                 ) |                 ) | ||||||
|                 manager.manage("Inspector", inspector, {}, transient=False) |                 manager.manage("Inspector", inspector, {}, transient=False) | ||||||
|  |  | ||||||
| @@ -860,8 +853,6 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|             trigger_events(ready, loop, primary) |             trigger_events(ready, loop, primary) | ||||||
|  |  | ||||||
|             manager.run() |             manager.run() | ||||||
|         except ServerKilled: |  | ||||||
|             exit_code = 1 |  | ||||||
|         except BaseException: |         except BaseException: | ||||||
|             kwargs = primary_server_info.settings |             kwargs = primary_server_info.settings | ||||||
|             error_logger.exception( |             error_logger.exception( | ||||||
| @@ -877,10 +868,6 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|  |  | ||||||
|             sync_manager.shutdown() |             sync_manager.shutdown() | ||||||
|             for sock in socks: |             for sock in socks: | ||||||
|                 try: |  | ||||||
|                     sock.shutdown(SHUT_RDWR) |  | ||||||
|                 except OSError: |  | ||||||
|                     ... |  | ||||||
|                 sock.close() |                 sock.close() | ||||||
|             socks = [] |             socks = [] | ||||||
|             trigger_events(main_stop, loop, primary) |             trigger_events(main_stop, loop, primary) | ||||||
| @@ -890,8 +877,6 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|             unix = kwargs.get("unix") |             unix = kwargs.get("unix") | ||||||
|             if unix: |             if unix: | ||||||
|                 remove_unix_socket(unix) |                 remove_unix_socket(unix) | ||||||
|         if exit_code: |  | ||||||
|             os._exit(exit_code) |  | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def serve_single(cls, primary: Optional[Sanic] = None) -> None: |     def serve_single(cls, primary: Optional[Sanic] = None) -> None: | ||||||
| @@ -1112,6 +1097,7 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|         app: StartupMixin, |         app: StartupMixin, | ||||||
|         server_info: ApplicationServerInfo, |         server_info: ApplicationServerInfo, | ||||||
|     ) -> None:  # no cov |     ) -> None:  # no cov | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             # We should never get to this point without a server |             # We should never get to this point without a server | ||||||
|             # This is primarily to keep mypy happy |             # This is primarily to keep mypy happy | ||||||
|   | |||||||
| @@ -1,348 +0,0 @@ | |||||||
| from email.utils import formatdate |  | ||||||
| from functools import partial, wraps |  | ||||||
| from mimetypes import guess_type |  | ||||||
| from os import PathLike, path |  | ||||||
| from pathlib import Path, PurePath |  | ||||||
| from typing import Optional, Sequence, Set, Union, cast |  | ||||||
| from urllib.parse import unquote |  | ||||||
|  |  | ||||||
| from sanic_routing.route import Route |  | ||||||
|  |  | ||||||
| from sanic.base.meta import SanicMeta |  | ||||||
| from sanic.compat import stat_async |  | ||||||
| from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE |  | ||||||
| from sanic.exceptions import FileNotFound, HeaderNotFound, RangeNotSatisfiable |  | ||||||
| from sanic.handlers import ContentRangeHandler |  | ||||||
| from sanic.handlers.directory import DirectoryHandler |  | ||||||
| from sanic.log import deprecation, error_logger |  | ||||||
| from sanic.mixins.base import BaseMixin |  | ||||||
| from sanic.models.futures import FutureStatic |  | ||||||
| from sanic.request import Request |  | ||||||
| from sanic.response import HTTPResponse, file, file_stream, validate_file |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class StaticMixin(BaseMixin, metaclass=SanicMeta): |  | ||||||
|     def __init__(self, *args, **kwargs) -> None: |  | ||||||
|         self._future_statics: Set[FutureStatic] = set() |  | ||||||
|  |  | ||||||
|     def _apply_static(self, static: FutureStatic) -> Route: |  | ||||||
|         raise NotImplementedError  # noqa |  | ||||||
|  |  | ||||||
|     def static( |  | ||||||
|         self, |  | ||||||
|         uri: str, |  | ||||||
|         file_or_directory: Union[PathLike, str, bytes], |  | ||||||
|         pattern: str = r"/?.+", |  | ||||||
|         use_modified_since: bool = True, |  | ||||||
|         use_content_range: bool = False, |  | ||||||
|         stream_large_files: Union[bool, int] = False, |  | ||||||
|         name: str = "static", |  | ||||||
|         host: Optional[str] = None, |  | ||||||
|         strict_slashes: Optional[bool] = None, |  | ||||||
|         content_type: Optional[str] = None, |  | ||||||
|         apply: bool = True, |  | ||||||
|         resource_type: Optional[str] = None, |  | ||||||
|         index: Optional[Union[str, Sequence[str]]] = None, |  | ||||||
|         directory_view: bool = False, |  | ||||||
|         directory_handler: Optional[DirectoryHandler] = None, |  | ||||||
|     ): |  | ||||||
|         """ |  | ||||||
|         Register a root to serve files from. The input can either be a |  | ||||||
|         file or a directory. This method will enable an easy and simple way |  | ||||||
|         to setup the :class:`Route` necessary to serve the static files. |  | ||||||
|  |  | ||||||
|         :param uri: URL path to be used for serving static content |  | ||||||
|         :param file_or_directory: Path for the Static file/directory with |  | ||||||
|             static files |  | ||||||
|         :param pattern: Regex Pattern identifying the valid static files |  | ||||||
|         :param use_modified_since: If true, send file modified time, and return |  | ||||||
|             not modified if the browser's matches the server's |  | ||||||
|         :param use_content_range: If true, process header for range requests |  | ||||||
|             and sends the file part that is requested |  | ||||||
|         :param stream_large_files: If true, use the |  | ||||||
|             :func:`StreamingHTTPResponse.file_stream` handler rather |  | ||||||
|             than the :func:`HTTPResponse.file` handler to send the file. |  | ||||||
|             If this is an integer, this represents the threshold size to |  | ||||||
|             switch to :func:`StreamingHTTPResponse.file_stream` |  | ||||||
|         :param name: user defined name used for url_for |  | ||||||
|         :param host: Host IP or FQDN for the service to use |  | ||||||
|         :param strict_slashes: Instruct :class:`Sanic` to check if the request |  | ||||||
|             URLs need to terminate with a */* |  | ||||||
|         :param content_type: user defined content type for header |  | ||||||
|         :param apply: If true, will register the route immediately |  | ||||||
|         :param resource_type: Explicitly declare a resource to be a " |  | ||||||
|             file" or a "dir" |  | ||||||
|         :param index: When exposing against a directory, index is the name that |  | ||||||
|             will be served as the default file. When multiple files names are |  | ||||||
|             passed, then they will be tried in order. |  | ||||||
|         :param directory_view: Whether to fallback to showing the directory |  | ||||||
|             viewer when exposing a directory |  | ||||||
|         :param directory_handler: An instance of :class:`DirectoryHandler` |  | ||||||
|             that can be used for explicitly controlling and subclassing the |  | ||||||
|             behavior of the default directory handler |  | ||||||
|         :return: routes registered on the router |  | ||||||
|         :rtype: List[sanic.router.Route] |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         name = self._generate_name(name) |  | ||||||
|  |  | ||||||
|         if strict_slashes is None and self.strict_slashes is not None: |  | ||||||
|             strict_slashes = self.strict_slashes |  | ||||||
|  |  | ||||||
|         if not isinstance(file_or_directory, (str, bytes, PurePath)): |  | ||||||
|             raise ValueError( |  | ||||||
|                 f"Static route must be a valid path, not {file_or_directory}" |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         if isinstance(file_or_directory, bytes): |  | ||||||
|             deprecation( |  | ||||||
|                 "Serving a static directory with a bytes string is " |  | ||||||
|                 "deprecated and will be removed in v22.9.", |  | ||||||
|                 22.9, |  | ||||||
|             ) |  | ||||||
|             file_or_directory = cast(str, file_or_directory.decode()) |  | ||||||
|         file_or_directory = Path(file_or_directory) |  | ||||||
|  |  | ||||||
|         if directory_handler and (directory_view or index): |  | ||||||
|             raise ValueError( |  | ||||||
|                 "When explicitly setting directory_handler, you cannot " |  | ||||||
|                 "set either directory_view or index. Instead, pass " |  | ||||||
|                 "these arguments to your DirectoryHandler instance." |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         if not directory_handler: |  | ||||||
|             directory_handler = DirectoryHandler( |  | ||||||
|                 uri=uri, |  | ||||||
|                 directory=file_or_directory, |  | ||||||
|                 directory_view=directory_view, |  | ||||||
|                 index=index, |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         static = FutureStatic( |  | ||||||
|             uri, |  | ||||||
|             file_or_directory, |  | ||||||
|             pattern, |  | ||||||
|             use_modified_since, |  | ||||||
|             use_content_range, |  | ||||||
|             stream_large_files, |  | ||||||
|             name, |  | ||||||
|             host, |  | ||||||
|             strict_slashes, |  | ||||||
|             content_type, |  | ||||||
|             resource_type, |  | ||||||
|             directory_handler, |  | ||||||
|         ) |  | ||||||
|         self._future_statics.add(static) |  | ||||||
|  |  | ||||||
|         if apply: |  | ||||||
|             self._apply_static(static) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class StaticHandleMixin(metaclass=SanicMeta): |  | ||||||
|     def _apply_static(self, static: FutureStatic) -> Route: |  | ||||||
|         return self._register_static(static) |  | ||||||
|  |  | ||||||
|     def _register_static( |  | ||||||
|         self, |  | ||||||
|         static: FutureStatic, |  | ||||||
|     ): |  | ||||||
|         # TODO: Though sanic is not a file server, I feel like we should |  | ||||||
|         # at least make a good effort here.  Modified-since is nice, but |  | ||||||
|         # we could also look into etags, expires, and caching |  | ||||||
|         """ |  | ||||||
|         Register a static directory handler with Sanic by adding a route to the |  | ||||||
|         router and registering a handler. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         if isinstance(static.file_or_directory, bytes): |  | ||||||
|             file_or_directory = static.file_or_directory.decode("utf-8") |  | ||||||
|         elif isinstance(static.file_or_directory, PurePath): |  | ||||||
|             file_or_directory = str(static.file_or_directory) |  | ||||||
|         elif not isinstance(static.file_or_directory, str): |  | ||||||
|             raise ValueError("Invalid file path string.") |  | ||||||
|         else: |  | ||||||
|             file_or_directory = static.file_or_directory |  | ||||||
|  |  | ||||||
|         uri = static.uri |  | ||||||
|         name = static.name |  | ||||||
|         # If we're not trying to match a file directly, |  | ||||||
|         # serve from the folder |  | ||||||
|         if not static.resource_type: |  | ||||||
|             if not path.isfile(file_or_directory): |  | ||||||
|                 uri = uri.rstrip("/") |  | ||||||
|                 uri += "/<__file_uri__:path>" |  | ||||||
|         elif static.resource_type == "dir": |  | ||||||
|             if path.isfile(file_or_directory): |  | ||||||
|                 raise TypeError( |  | ||||||
|                     "Resource type improperly identified as directory. " |  | ||||||
|                     f"'{file_or_directory}'" |  | ||||||
|                 ) |  | ||||||
|             uri = uri.rstrip("/") |  | ||||||
|             uri += "/<__file_uri__:path>" |  | ||||||
|         elif static.resource_type == "file" and not path.isfile( |  | ||||||
|             file_or_directory |  | ||||||
|         ): |  | ||||||
|             raise TypeError( |  | ||||||
|                 "Resource type improperly identified as file. " |  | ||||||
|                 f"'{file_or_directory}'" |  | ||||||
|             ) |  | ||||||
|         elif static.resource_type != "file": |  | ||||||
|             raise ValueError( |  | ||||||
|                 "The resource_type should be set to 'file' or 'dir'" |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         # special prefix for static files |  | ||||||
|         # if not static.name.startswith("_static_"): |  | ||||||
|         #     name = f"_static_{static.name}" |  | ||||||
|  |  | ||||||
|         _handler = wraps(self._static_request_handler)( |  | ||||||
|             partial( |  | ||||||
|                 self._static_request_handler, |  | ||||||
|                 file_or_directory=file_or_directory, |  | ||||||
|                 use_modified_since=static.use_modified_since, |  | ||||||
|                 use_content_range=static.use_content_range, |  | ||||||
|                 stream_large_files=static.stream_large_files, |  | ||||||
|                 content_type=static.content_type, |  | ||||||
|                 directory_handler=static.directory_handler, |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         route, _ = self.route(  # type: ignore |  | ||||||
|             uri=uri, |  | ||||||
|             methods=["GET", "HEAD"], |  | ||||||
|             name=name, |  | ||||||
|             host=static.host, |  | ||||||
|             strict_slashes=static.strict_slashes, |  | ||||||
|             static=True, |  | ||||||
|         )(_handler) |  | ||||||
|  |  | ||||||
|         return route |  | ||||||
|  |  | ||||||
|     async def _static_request_handler( |  | ||||||
|         self, |  | ||||||
|         request: Request, |  | ||||||
|         *, |  | ||||||
|         file_or_directory: PathLike, |  | ||||||
|         use_modified_since: bool, |  | ||||||
|         use_content_range: bool, |  | ||||||
|         stream_large_files: Union[bool, int], |  | ||||||
|         directory_handler: DirectoryHandler, |  | ||||||
|         content_type: Optional[str] = None, |  | ||||||
|         __file_uri__: Optional[str] = None, |  | ||||||
|     ): |  | ||||||
|         not_found = FileNotFound( |  | ||||||
|             "File not found", |  | ||||||
|             path=file_or_directory, |  | ||||||
|             relative_url=__file_uri__, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         # Merge served directory and requested file if provided |  | ||||||
|         file_path = await self._get_file_path( |  | ||||||
|             file_or_directory, __file_uri__, not_found |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         try: |  | ||||||
|             headers = {} |  | ||||||
|             # Check if the client has been sent this file before |  | ||||||
|             # and it has not been modified since |  | ||||||
|             stats = None |  | ||||||
|             if use_modified_since: |  | ||||||
|                 stats = await stat_async(file_path) |  | ||||||
|                 modified_since = stats.st_mtime |  | ||||||
|                 response = await validate_file(request.headers, modified_since) |  | ||||||
|                 if response: |  | ||||||
|                     return response |  | ||||||
|                 headers["Last-Modified"] = formatdate( |  | ||||||
|                     modified_since, usegmt=True |  | ||||||
|                 ) |  | ||||||
|             _range = None |  | ||||||
|             if use_content_range: |  | ||||||
|                 _range = None |  | ||||||
|                 if not stats: |  | ||||||
|                     stats = await stat_async(file_path) |  | ||||||
|                 headers["Accept-Ranges"] = "bytes" |  | ||||||
|                 headers["Content-Length"] = str(stats.st_size) |  | ||||||
|                 if request.method != "HEAD": |  | ||||||
|                     try: |  | ||||||
|                         _range = ContentRangeHandler(request, stats) |  | ||||||
|                     except HeaderNotFound: |  | ||||||
|                         pass |  | ||||||
|                     else: |  | ||||||
|                         del headers["Content-Length"] |  | ||||||
|                         headers.update(_range.headers) |  | ||||||
|  |  | ||||||
|             if "content-type" not in headers: |  | ||||||
|                 content_type = ( |  | ||||||
|                     content_type |  | ||||||
|                     or guess_type(file_path)[0] |  | ||||||
|                     or DEFAULT_HTTP_CONTENT_TYPE |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|                 if "charset=" not in content_type and ( |  | ||||||
|                     content_type.startswith("text/") |  | ||||||
|                     or content_type == "application/javascript" |  | ||||||
|                 ): |  | ||||||
|                     content_type += "; charset=utf-8" |  | ||||||
|  |  | ||||||
|                 headers["Content-Type"] = content_type |  | ||||||
|  |  | ||||||
|             if request.method == "HEAD": |  | ||||||
|                 return HTTPResponse(headers=headers) |  | ||||||
|             else: |  | ||||||
|                 if stream_large_files: |  | ||||||
|                     if isinstance(stream_large_files, bool): |  | ||||||
|                         threshold = 1024 * 1024 |  | ||||||
|                     else: |  | ||||||
|                         threshold = stream_large_files |  | ||||||
|  |  | ||||||
|                     if not stats: |  | ||||||
|                         stats = await stat_async(file_path) |  | ||||||
|                     if stats.st_size >= threshold: |  | ||||||
|                         return await file_stream( |  | ||||||
|                             file_path, headers=headers, _range=_range |  | ||||||
|                         ) |  | ||||||
|                 return await file(file_path, headers=headers, _range=_range) |  | ||||||
|         except (IsADirectoryError, PermissionError): |  | ||||||
|             return await directory_handler.handle(request, request.path) |  | ||||||
|         except RangeNotSatisfiable: |  | ||||||
|             raise |  | ||||||
|         except FileNotFoundError: |  | ||||||
|             raise not_found |  | ||||||
|         except Exception: |  | ||||||
|             error_logger.exception( |  | ||||||
|                 "Exception in static request handler: " |  | ||||||
|                 f"path={file_or_directory}, " |  | ||||||
|                 f"relative_url={__file_uri__}" |  | ||||||
|             ) |  | ||||||
|             raise |  | ||||||
|  |  | ||||||
|     async def _get_file_path(self, file_or_directory, __file_uri__, not_found): |  | ||||||
|         file_path_raw = Path(unquote(file_or_directory)) |  | ||||||
|         root_path = file_path = file_path_raw.resolve() |  | ||||||
|  |  | ||||||
|         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 |  | ||||||
|         return file_path |  | ||||||
| @@ -1,7 +1,6 @@ | |||||||
| from pathlib import Path | from pathlib import PurePath | ||||||
| from typing import Dict, Iterable, List, NamedTuple, Optional, Union | from typing import Dict, Iterable, List, NamedTuple, Optional, Union | ||||||
|  |  | ||||||
| from sanic.handlers.directory import DirectoryHandler |  | ||||||
| from sanic.models.handler_types import ( | from sanic.models.handler_types import ( | ||||||
|     ErrorMiddlewareType, |     ErrorMiddlewareType, | ||||||
|     ListenerType, |     ListenerType, | ||||||
| @@ -47,17 +46,16 @@ class FutureException(NamedTuple): | |||||||
|  |  | ||||||
| class FutureStatic(NamedTuple): | class FutureStatic(NamedTuple): | ||||||
|     uri: str |     uri: str | ||||||
|     file_or_directory: Path |     file_or_directory: Union[str, bytes, PurePath] | ||||||
|     pattern: str |     pattern: str | ||||||
|     use_modified_since: bool |     use_modified_since: bool | ||||||
|     use_content_range: bool |     use_content_range: bool | ||||||
|     stream_large_files: Union[bool, int] |     stream_large_files: bool | ||||||
|     name: str |     name: str | ||||||
|     host: Optional[str] |     host: Optional[str] | ||||||
|     strict_slashes: Optional[bool] |     strict_slashes: Optional[bool] | ||||||
|     content_type: Optional[str] |     content_type: Optional[bool] | ||||||
|     resource_type: Optional[str] |     resource_type: Optional[str] | ||||||
|     directory_handler: DirectoryHandler |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class FutureSignal(NamedTuple): | class FutureSignal(NamedTuple): | ||||||
|   | |||||||
| @@ -1,70 +0,0 @@ | |||||||
| from abc import ABC, abstractmethod |  | ||||||
|  |  | ||||||
| from html5tagger import HTML, Builder, Document |  | ||||||
|  |  | ||||||
| from sanic import __version__ as VERSION |  | ||||||
| from sanic.application.logo import SVG_LOGO_SIMPLE |  | ||||||
| from sanic.pages.css import CSS |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class BasePage(ABC, metaclass=CSS):  # no cov |  | ||||||
|     TITLE = "Sanic" |  | ||||||
|     HEADING = None |  | ||||||
|     CSS: str |  | ||||||
|     doc: Builder |  | ||||||
|  |  | ||||||
|     def __init__(self, debug: bool = True) -> None: |  | ||||||
|         self.debug = debug |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def style(self) -> str: |  | ||||||
|         return self.CSS |  | ||||||
|  |  | ||||||
|     def render(self) -> str: |  | ||||||
|         self.doc = Document(self.TITLE, lang="en", id="sanic") |  | ||||||
|         self._head() |  | ||||||
|         self._body() |  | ||||||
|         self._foot() |  | ||||||
|         return str(self.doc) |  | ||||||
|  |  | ||||||
|     def _head(self) -> None: |  | ||||||
|         self.doc.style(HTML(self.style)) |  | ||||||
|         with self.doc.header: |  | ||||||
|             self.doc.div(self.HEADING or self.TITLE) |  | ||||||
|  |  | ||||||
|     def _foot(self) -> None: |  | ||||||
|         with self.doc.footer: |  | ||||||
|             self.doc.div("powered by") |  | ||||||
|             with self.doc.div: |  | ||||||
|                 self._sanic_logo() |  | ||||||
|             if self.debug: |  | ||||||
|                 self.doc.div(f"Version {VERSION}") |  | ||||||
|                 with self.doc.div: |  | ||||||
|                     for idx, (title, href) in enumerate( |  | ||||||
|                         ( |  | ||||||
|                             ("Docs", "https://sanic.dev"), |  | ||||||
|                             ("Help", "https://sanic.dev/en/help.html"), |  | ||||||
|                             ("GitHub", "https://github.com/sanic-org/sanic"), |  | ||||||
|                         ) |  | ||||||
|                     ): |  | ||||||
|                         if idx > 0: |  | ||||||
|                             self.doc(" | ") |  | ||||||
|                         self.doc.a( |  | ||||||
|                             title, |  | ||||||
|                             href=href, |  | ||||||
|                             target="_blank", |  | ||||||
|                             referrerpolicy="no-referrer", |  | ||||||
|                         ) |  | ||||||
|                 self.doc.div("DEBUG mode") |  | ||||||
|  |  | ||||||
|     @abstractmethod |  | ||||||
|     def _body(self) -> None: |  | ||||||
|         ... |  | ||||||
|  |  | ||||||
|     def _sanic_logo(self) -> None: |  | ||||||
|         self.doc.a( |  | ||||||
|             HTML(SVG_LOGO_SIMPLE), |  | ||||||
|             href="https://sanic.dev", |  | ||||||
|             target="_blank", |  | ||||||
|             referrerpolicy="no-referrer", |  | ||||||
|         ) |  | ||||||
| @@ -1,35 +0,0 @@ | |||||||
| from abc import ABCMeta |  | ||||||
| from pathlib import Path |  | ||||||
| from typing import Optional |  | ||||||
|  |  | ||||||
|  |  | ||||||
| CURRENT_DIR = Path(__file__).parent |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def _extract_style(maybe_style: Optional[str], name: str) -> str: |  | ||||||
|     if maybe_style is not None: |  | ||||||
|         maybe_path = Path(maybe_style) |  | ||||||
|         if maybe_path.exists(): |  | ||||||
|             return maybe_path.read_text(encoding="UTF-8") |  | ||||||
|         return maybe_style |  | ||||||
|     maybe_path = CURRENT_DIR / "styles" / f"{name}.css" |  | ||||||
|     if maybe_path.exists(): |  | ||||||
|         return maybe_path.read_text(encoding="UTF-8") |  | ||||||
|     return "" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class CSS(ABCMeta): |  | ||||||
|     """Cascade stylesheets, i.e. combine all ancestor styles""" |  | ||||||
|  |  | ||||||
|     def __new__(cls, name, bases, attrs): |  | ||||||
|         Page = super().__new__(cls, name, bases, attrs) |  | ||||||
|         # Use a locally defined STYLE or the one from styles directory |  | ||||||
|         Page.STYLE = _extract_style(attrs.get("STYLE_FILE"), name) |  | ||||||
|         Page.STYLE += attrs.get("STYLE_APPEND", "") |  | ||||||
|         # Combine with all ancestor styles |  | ||||||
|         Page.CSS = "".join( |  | ||||||
|             Class.STYLE |  | ||||||
|             for Class in reversed(Page.__mro__) |  | ||||||
|             if type(Class) is CSS |  | ||||||
|         ) |  | ||||||
|         return Page |  | ||||||
| @@ -1,66 +0,0 @@ | |||||||
| import sys |  | ||||||
|  |  | ||||||
| from typing import Dict, Iterable |  | ||||||
|  |  | ||||||
| from html5tagger import E |  | ||||||
|  |  | ||||||
| from .base import BasePage |  | ||||||
|  |  | ||||||
|  |  | ||||||
| if sys.version_info < (3, 8):  # no cov |  | ||||||
|     FileInfo = Dict |  | ||||||
|  |  | ||||||
| else: |  | ||||||
|     from typing import TypedDict |  | ||||||
|  |  | ||||||
|     class FileInfo(TypedDict): |  | ||||||
|         icon: str |  | ||||||
|         file_name: str |  | ||||||
|         file_access: str |  | ||||||
|         file_size: str |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class DirectoryPage(BasePage):  # no cov |  | ||||||
|     TITLE = "Directory Viewer" |  | ||||||
|  |  | ||||||
|     def __init__( |  | ||||||
|         self, files: Iterable[FileInfo], url: str, debug: bool |  | ||||||
|     ) -> None: |  | ||||||
|         super().__init__(debug) |  | ||||||
|         self.files = files |  | ||||||
|         self.url = url |  | ||||||
|  |  | ||||||
|     def _body(self) -> None: |  | ||||||
|         with self.doc.main: |  | ||||||
|             self._headline() |  | ||||||
|             files = list(self.files) |  | ||||||
|             if files: |  | ||||||
|                 self._file_table(files) |  | ||||||
|             else: |  | ||||||
|                 self.doc.p("The folder is empty.") |  | ||||||
|  |  | ||||||
|     def _headline(self): |  | ||||||
|         """Implement a heading with the current path, combined with |  | ||||||
|         breadcrumb links""" |  | ||||||
|         with self.doc.h1(id="breadcrumbs"): |  | ||||||
|             p = self.url.split("/")[:-1] |  | ||||||
|  |  | ||||||
|             for i, part in enumerate(p): |  | ||||||
|                 path = "/".join(p[: i + 1]) + "/" |  | ||||||
|                 with self.doc.a(href=path): |  | ||||||
|                     self.doc.span(part, class_="dir").span("/", class_="sep") |  | ||||||
|  |  | ||||||
|     def _file_table(self, files: Iterable[FileInfo]): |  | ||||||
|         with self.doc.table(class_="autoindex container"): |  | ||||||
|             for f in files: |  | ||||||
|                 self._file_row(**f) |  | ||||||
|  |  | ||||||
|     def _file_row( |  | ||||||
|         self, |  | ||||||
|         icon: str, |  | ||||||
|         file_name: str, |  | ||||||
|         file_access: str, |  | ||||||
|         file_size: str, |  | ||||||
|     ): |  | ||||||
|         first = E.span(icon, class_="icon").a(file_name, href=file_name) |  | ||||||
|         self.doc.tr.td(first).td(file_size).td(file_access) |  | ||||||
| @@ -1,109 +0,0 @@ | |||||||
| from typing import Any, Mapping |  | ||||||
|  |  | ||||||
| import tracerite.html |  | ||||||
|  |  | ||||||
| from html5tagger import E |  | ||||||
| from tracerite import html_traceback, inspector |  | ||||||
|  |  | ||||||
| from sanic.request import Request |  | ||||||
|  |  | ||||||
| from .base import BasePage |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # Avoid showing the request in the traceback variable inspectors |  | ||||||
| inspector.blacklist_types += (Request,) |  | ||||||
|  |  | ||||||
| ENDUSER_TEXT = """\ |  | ||||||
| We're sorry, but it looks like something went wrong. Please try refreshing \ |  | ||||||
| the page or navigating back to the homepage. If the issue persists, our \ |  | ||||||
| technical team is working to resolve it as soon as possible. We apologize \ |  | ||||||
| for the inconvenience and appreciate your patience.\ |  | ||||||
| """ |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ErrorPage(BasePage): |  | ||||||
|     STYLE_APPEND = tracerite.html.style |  | ||||||
|  |  | ||||||
|     def __init__( |  | ||||||
|         self, |  | ||||||
|         debug: bool, |  | ||||||
|         title: str, |  | ||||||
|         text: str, |  | ||||||
|         request: Request, |  | ||||||
|         exc: Exception, |  | ||||||
|     ) -> None: |  | ||||||
|         super().__init__(debug) |  | ||||||
|         name = request.app.name.replace("_", " ").strip() |  | ||||||
|         if name.islower(): |  | ||||||
|             name = name.title() |  | ||||||
|         self.TITLE = f"Application {name} cannot handle your request" |  | ||||||
|         self.HEADING = E("Application ").strong(name)( |  | ||||||
|             " cannot handle your request" |  | ||||||
|         ) |  | ||||||
|         self.title = title |  | ||||||
|         self.text = text |  | ||||||
|         self.request = request |  | ||||||
|         self.exc = exc |  | ||||||
|         self.details_open = not getattr(exc, "quiet", False) |  | ||||||
|  |  | ||||||
|     def _head(self) -> None: |  | ||||||
|         self.doc._script(tracerite.html.javascript) |  | ||||||
|         super()._head() |  | ||||||
|  |  | ||||||
|     def _body(self) -> None: |  | ||||||
|         debug = self.request.app.debug |  | ||||||
|         route_name = self.request.name or "[route not found]" |  | ||||||
|         with self.doc.main: |  | ||||||
|             self.doc.h1(f"⚠️ {self.title}").p(self.text) |  | ||||||
|             # Show context details if available on the exception |  | ||||||
|             context = getattr(self.exc, "context", None) |  | ||||||
|             if context: |  | ||||||
|                 self._key_value_table( |  | ||||||
|                     "Issue context", "exception-context", context |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|             if not debug: |  | ||||||
|                 with self.doc.div(id="enduser"): |  | ||||||
|                     self.doc.p(ENDUSER_TEXT).p.a("Front Page", href="/") |  | ||||||
|                 return |  | ||||||
|             # Show additional details in debug mode, |  | ||||||
|             # open by default for 500 errors |  | ||||||
|             with self.doc.details(open=self.details_open, class_="smalltext"): |  | ||||||
|                 # Show extra details if available on the exception |  | ||||||
|                 extra = getattr(self.exc, "extra", None) |  | ||||||
|                 if extra: |  | ||||||
|                     self._key_value_table( |  | ||||||
|                         "Issue extra data", "exception-extra", extra |  | ||||||
|                     ) |  | ||||||
|  |  | ||||||
|                 self.doc.summary( |  | ||||||
|                     "Details for developers (Sanic debug mode only)" |  | ||||||
|                 ) |  | ||||||
|                 if self.exc: |  | ||||||
|                     with self.doc.div(class_="exception-wrapper"): |  | ||||||
|                         self.doc.h2(f"Exception in {route_name}:") |  | ||||||
|                         self.doc( |  | ||||||
|                             html_traceback(self.exc, include_js_css=False) |  | ||||||
|                         ) |  | ||||||
|  |  | ||||||
|                 self._key_value_table( |  | ||||||
|                     f"{self.request.method} {self.request.path}", |  | ||||||
|                     "request-headers", |  | ||||||
|                     self.request.headers, |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|     def _key_value_table( |  | ||||||
|         self, title: str, table_id: str, data: Mapping[str, Any] |  | ||||||
|     ) -> None: |  | ||||||
|         with self.doc.div(class_="key-value-display"): |  | ||||||
|             self.doc.h2(title) |  | ||||||
|             with self.doc.dl(id=table_id, class_="key-value-table smalltext"): |  | ||||||
|                 for key, value in data.items(): |  | ||||||
|                     # Reading values may cause a new exception, so suppress it |  | ||||||
|                     try: |  | ||||||
|                         value = str(value) |  | ||||||
|                     except Exception: |  | ||||||
|                         value = E.em("Unable to display value") |  | ||||||
|                     self.doc.dt.span(key, class_="nobr key").span(": ").dd( |  | ||||||
|                         value |  | ||||||
|                     ) |  | ||||||
| @@ -1,146 +0,0 @@ | |||||||
| /** BasePage **/ |  | ||||||
|  |  | ||||||
| :root { |  | ||||||
|     --sanic: #ff0d68; |  | ||||||
|     --sanic-yellow: #FFE900; |  | ||||||
|     --sanic-background: #efeced; |  | ||||||
|     --sanic-text: #121010; |  | ||||||
|     --sanic-text-lighter: #756169; |  | ||||||
|     --sanic-link: #ff0d68; |  | ||||||
|     --sanic-block-background: #f7f4f6; |  | ||||||
|     --sanic-block-text: #000; |  | ||||||
|     --sanic-block-alt-text: #6b6468; |  | ||||||
|     --sanic-header-background: #272325; |  | ||||||
|     --sanic-header-border: #fff; |  | ||||||
|     --sanic-header-text: #fff; |  | ||||||
|     --sanic-highlight-background: var(--sanic-yellow); |  | ||||||
|     --sanic-highlight-text: var(--sanic-text); |  | ||||||
|     --sanic-tab-background: #f7f4f6; |  | ||||||
|     --sanic-tab-shadow: #f7f6f6; |  | ||||||
|     --sanic-tab-text: #222021; |  | ||||||
|     --sanic-tracerite-var: var(--sanic-text); |  | ||||||
|     --sanic-tracerite-val: #ff0d68; |  | ||||||
|     --sanic-tracerite-type: #6d6a6b; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @media (prefers-color-scheme: dark) { |  | ||||||
|     :root { |  | ||||||
|         --sanic-text: #f7f4f6; |  | ||||||
|         --sanic-background: #121010; |  | ||||||
|         --sanic-block-background: #0f0d0e; |  | ||||||
|         --sanic-block-text: #f7f4f6; |  | ||||||
|         --sanic-header-background: #030203; |  | ||||||
|         --sanic-header-border: #000; |  | ||||||
|         --sanic-highlight-text: var(--sanic-background); |  | ||||||
|         --sanic-tab-background: #292728; |  | ||||||
|         --sanic-tab-shadow: #0f0d0e; |  | ||||||
|         --sanic-tab-text: #aea7ab; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| html { |  | ||||||
|     font: 16px sans-serif; |  | ||||||
|     background: var(--sanic-background); |  | ||||||
|     color: var(--sanic-text); |  | ||||||
|     scrollbar-gutter: stable; |  | ||||||
|     overflow: hidden auto; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| body { |  | ||||||
|     margin: 0; |  | ||||||
|     font-size: 1.25rem; |  | ||||||
|     line-height: 125%; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| body>* { |  | ||||||
|     padding: 1rem 2vw; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media (max-width: 1000px) { |  | ||||||
|     body>* { |  | ||||||
|         padding: 0.5rem 1.5vw; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     html { |  | ||||||
|         /* Scale everything by rem of 6px-16px by viewport width */ |  | ||||||
|         font-size: calc(6px + 10 * 100vw / 1000); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| main { |  | ||||||
|     /* Make sure the footer is closer to bottom */ |  | ||||||
|     min-height: 70vh; |  | ||||||
|     /* Generous padding for readability */ |  | ||||||
|     padding: 1rem 2.5rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .smalltext { |  | ||||||
|     font-size: 1.0rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .container { |  | ||||||
|     min-width: 600px; |  | ||||||
|     max-width: 1600px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| header { |  | ||||||
|     background: var(--sanic-header-background); |  | ||||||
|     color: var(--sanic-header-text); |  | ||||||
|     border-bottom: 1px solid var(--sanic-header-border); |  | ||||||
|     text-align: center; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| footer { |  | ||||||
|     text-align: center; |  | ||||||
|     display: flex; |  | ||||||
|     flex-direction: column; |  | ||||||
|     font-size: 0.8rem; |  | ||||||
|     margin: 2rem; |  | ||||||
|     line-height: 1.5em; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| h1 { |  | ||||||
|     text-align: left; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| a { |  | ||||||
|     text-decoration: none; |  | ||||||
|     color: var(--sanic-link); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| a:hover, |  | ||||||
| a:focus { |  | ||||||
|     text-decoration: underline; |  | ||||||
|     outline: none; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| span.icon { |  | ||||||
|     margin-right: 1rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #logo-simple { |  | ||||||
|     height: 1.75rem; |  | ||||||
|     padding: 0 0.25rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @media (prefers-color-scheme: dark) { |  | ||||||
|     #logo-simple path:last-child { |  | ||||||
|         fill: #e1e1e1; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #sanic pre, |  | ||||||
| #sanic code { |  | ||||||
|     font-family: "Fira Code", |  | ||||||
|         "Source Code Pro", |  | ||||||
|         Menlo, |  | ||||||
|         Meslo, |  | ||||||
|         Monaco, |  | ||||||
|         Consolas, |  | ||||||
|         Lucida Console, |  | ||||||
|         monospace; |  | ||||||
|     font-size: 0.8rem; |  | ||||||
| } |  | ||||||
| @@ -1,63 +0,0 @@ | |||||||
| /** DirectoryPage **/ |  | ||||||
| #breadcrumbs>a:hover { |  | ||||||
|     text-decoration: none; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #breadcrumbs>a .dir { |  | ||||||
|     padding: 0 0.25em; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #breadcrumbs>a:first-child:hover::before, |  | ||||||
| #breadcrumbs>a .dir:hover { |  | ||||||
|     text-decoration: underline; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #breadcrumbs>a:first-child::before { |  | ||||||
|     content: "🏠"; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #breadcrumbs>a:last-child { |  | ||||||
|     color: #ff0d68; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| main a { |  | ||||||
|     color: inherit; |  | ||||||
|     font-weight: bold; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| table.autoindex { |  | ||||||
|     width: 100%; |  | ||||||
|     font-family: monospace; |  | ||||||
|     font-size: 1.25rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| table.autoindex tr { |  | ||||||
|     display: flex; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| table.autoindex tr:hover { |  | ||||||
|     background-color: #ddd; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| table.autoindex td { |  | ||||||
|     margin: 0 0.5rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| table.autoindex td:first-child { |  | ||||||
|     flex: 1; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| table.autoindex td:nth-child(2) { |  | ||||||
|     text-align: right; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| table.autoindex td:last-child { |  | ||||||
|     text-align: right; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @media (prefers-color-scheme: dark) { |  | ||||||
|     table.autoindex tr:hover { |  | ||||||
|         background-color: #222; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,108 +0,0 @@ | |||||||
| /** ErrorPage **/ |  | ||||||
| #enduser { |  | ||||||
|     max-width: 30em; |  | ||||||
|     margin: 5em auto 5em auto; |  | ||||||
|     text-align: justify; |  | ||||||
|     /*text-justify: both;*/ |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #enduser a { |  | ||||||
|     color: var(--sanic-blue); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #enduser p:last-child { |  | ||||||
|     text-align: right; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| summary { |  | ||||||
|     margin-top: 3em; |  | ||||||
|     color: var(--sanic-text-lighter); |  | ||||||
|     cursor: pointer; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .tracerite { |  | ||||||
|     --tracerite-var: var(--sanic-tracerite-var); |  | ||||||
|     --tracerite-val: var(--sanic-tracerite-val); |  | ||||||
|     --tracerite-type: var(--sanic-tracerite-type); |  | ||||||
|     --tracerite-exception: var(--sanic); |  | ||||||
|     --tracerite-highlight: var(--sanic-yellow); |  | ||||||
|     --tracerite-tab: var(--sanic-tab-background); |  | ||||||
|     --tracerite-tab-text: var(--sanic-tab-text); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .tracerite>h3 { |  | ||||||
|     margin: 0.5rem 0 !important; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #sanic .tracerite .traceback-labels button { |  | ||||||
|     font-size: 0.8rem; |  | ||||||
|     line-height: 120%; |  | ||||||
|     background: var(--tracerite-tab); |  | ||||||
|     color: var(--tracerite-tab-text); |  | ||||||
|     transition: 0.3s; |  | ||||||
|     cursor: pointer; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .tracerite .traceback-labels { |  | ||||||
|     padding-top: 5px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .tracerite .traceback-labels button:hover { |  | ||||||
|     filter: contrast(150%) brightness(120%) drop-shadow(0 -0 2px var(--sanic-tab-shadow)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #sanic .tracerite .tracerite-tooltip::before { |  | ||||||
|     bottom: 1.75em; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #sanic .tracerite .traceback-details mark span { |  | ||||||
|     background: var(--sanic-highlight-background); |  | ||||||
|     color: var(--sanic-highlight-text); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| header { |  | ||||||
|     background: var(--sanic-header-background); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| h2 { |  | ||||||
|     font-size: 1.3rem; |  | ||||||
|     color: var(--sanic-text); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .key-value-display, |  | ||||||
| .exception-wrapper { |  | ||||||
|     padding: 0.5rem; |  | ||||||
|     margin-top: 1rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .key-value-display { |  | ||||||
|     background-color: var(--sanic-block-background); |  | ||||||
|     color: var(--sanic-block-text); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .key-value-display h2 { |  | ||||||
|     margin-bottom: 0.2em; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| dl.key-value-table { |  | ||||||
|     width: 100%; |  | ||||||
|     margin: 0; |  | ||||||
|     display: grid; |  | ||||||
|     grid-template-columns: 1fr 5fr; |  | ||||||
|     grid-gap: .3em; |  | ||||||
|     white-space: pre-wrap; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| dl.key-value-table * { |  | ||||||
|     margin: 0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| dl.key-value-table dt { |  | ||||||
|     color: var(--sanic-block-alt-text); |  | ||||||
|     word-break: break-word; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| dl.key-value-table dd { |  | ||||||
|     /* Better breaking for cookies header and such */ |  | ||||||
|     word-break: break-all; |  | ||||||
| } |  | ||||||
							
								
								
									
										1116
									
								
								sanic/request.py
									
									
									
									
									
								
							
							
						
						
									
										1116
									
								
								sanic/request.py
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -2,20 +2,212 @@ from __future__ import annotations | |||||||
| 
 | 
 | ||||||
| from datetime import datetime, timezone | from datetime import datetime, timezone | ||||||
| from email.utils import formatdate, parsedate_to_datetime | from email.utils import formatdate, parsedate_to_datetime | ||||||
|  | from functools import partial | ||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| from os import path | from os import path | ||||||
| from pathlib import PurePath | from pathlib import PurePath | ||||||
| from time import time | from time import time | ||||||
| from typing import Any, AnyStr, Callable, Dict, Optional, Union | from typing import ( | ||||||
|  |     TYPE_CHECKING, | ||||||
|  |     Any, | ||||||
|  |     AnyStr, | ||||||
|  |     Callable, | ||||||
|  |     Coroutine, | ||||||
|  |     Dict, | ||||||
|  |     Iterator, | ||||||
|  |     Optional, | ||||||
|  |     Tuple, | ||||||
|  |     TypeVar, | ||||||
|  |     Union, | ||||||
|  | ) | ||||||
| from urllib.parse import quote_plus | from urllib.parse import quote_plus | ||||||
| 
 | 
 | ||||||
| from sanic.compat import Header, open_async, stat_async | from sanic.compat import Header, open_async, stat_async | ||||||
| from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE | from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE | ||||||
| from sanic.helpers import Default, _default | from sanic.cookies import CookieJar | ||||||
|  | from sanic.exceptions import SanicException, ServerError | ||||||
|  | from sanic.helpers import ( | ||||||
|  |     Default, | ||||||
|  |     _default, | ||||||
|  |     has_message_body, | ||||||
|  |     remove_entity_headers, | ||||||
|  | ) | ||||||
|  | from sanic.http import Http | ||||||
| from sanic.log import logger | from sanic.log import logger | ||||||
| from sanic.models.protocol_types import HTMLProtocol, Range | from sanic.models.protocol_types import HTMLProtocol, Range | ||||||
| 
 | 
 | ||||||
| from .types import HTTPResponse, JSONResponse, ResponseStream | 
 | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from sanic.asgi import ASGIApp | ||||||
|  |     from sanic.http.http3 import HTTPReceiver | ||||||
|  |     from sanic.request import Request | ||||||
|  | else: | ||||||
|  |     Request = TypeVar("Request") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     from ujson import dumps as json_dumps | ||||||
|  | except ImportError: | ||||||
|  |     # This is done in order to ensure that the JSON response is | ||||||
|  |     # kept consistent across both ujson and inbuilt json usage. | ||||||
|  |     from json import dumps | ||||||
|  | 
 | ||||||
|  |     json_dumps = partial(dumps, separators=(",", ":")) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BaseHTTPResponse: | ||||||
|  |     """ | ||||||
|  |     The base class for all HTTP Responses | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     __slots__ = ( | ||||||
|  |         "asgi", | ||||||
|  |         "body", | ||||||
|  |         "content_type", | ||||||
|  |         "stream", | ||||||
|  |         "status", | ||||||
|  |         "headers", | ||||||
|  |         "_cookies", | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     _dumps = json_dumps | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         self.asgi: bool = False | ||||||
|  |         self.body: Optional[bytes] = None | ||||||
|  |         self.content_type: Optional[str] = None | ||||||
|  |         self.stream: Optional[Union[Http, ASGIApp, HTTPReceiver]] = None | ||||||
|  |         self.status: int = None | ||||||
|  |         self.headers = Header({}) | ||||||
|  |         self._cookies: Optional[CookieJar] = None | ||||||
|  | 
 | ||||||
|  |     def __repr__(self): | ||||||
|  |         class_name = self.__class__.__name__ | ||||||
|  |         return f"<{class_name}: {self.status} {self.content_type}>" | ||||||
|  | 
 | ||||||
|  |     def _encode_body(self, data: Optional[AnyStr]): | ||||||
|  |         if data is None: | ||||||
|  |             return b"" | ||||||
|  |         return ( | ||||||
|  |             data.encode() if hasattr(data, "encode") else data  # type: ignore | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def cookies(self) -> CookieJar: | ||||||
|  |         """ | ||||||
|  |         The response cookies. Cookies should be set and written as follows: | ||||||
|  | 
 | ||||||
|  |         .. code-block:: python | ||||||
|  | 
 | ||||||
|  |                 response.cookies["test"] = "It worked!" | ||||||
|  |                 response.cookies["test"]["domain"] = ".yummy-yummy-cookie.com" | ||||||
|  |                 response.cookies["test"]["httponly"] = True | ||||||
|  | 
 | ||||||
|  |         `See user guide re: cookies | ||||||
|  |         <https://sanicframework.org/guide/basics/cookies.html>`__ | ||||||
|  | 
 | ||||||
|  |         :return: the cookie jar | ||||||
|  |         :rtype: CookieJar | ||||||
|  |         """ | ||||||
|  |         if self._cookies is None: | ||||||
|  |             self._cookies = CookieJar(self.headers) | ||||||
|  |         return self._cookies | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def processed_headers(self) -> Iterator[Tuple[bytes, bytes]]: | ||||||
|  |         """ | ||||||
|  |         Obtain a list of header tuples encoded in bytes for sending. | ||||||
|  | 
 | ||||||
|  |         Add and remove headers based on status and content_type. | ||||||
|  | 
 | ||||||
|  |         :return: response headers | ||||||
|  |         :rtype: Tuple[Tuple[bytes, bytes], ...] | ||||||
|  |         """ | ||||||
|  |         # TODO: Make a blacklist set of header names and then filter with that | ||||||
|  |         if self.status in (304, 412):  # Not Modified, Precondition Failed | ||||||
|  |             self.headers = remove_entity_headers(self.headers) | ||||||
|  |         if has_message_body(self.status): | ||||||
|  |             self.headers.setdefault("content-type", self.content_type) | ||||||
|  |         # Encode headers into bytes | ||||||
|  |         return ( | ||||||
|  |             (name.encode("ascii"), f"{value}".encode(errors="surrogateescape")) | ||||||
|  |             for name, value in self.headers.items() | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     async def send( | ||||||
|  |         self, | ||||||
|  |         data: Optional[AnyStr] = None, | ||||||
|  |         end_stream: Optional[bool] = None, | ||||||
|  |     ) -> None: | ||||||
|  |         """ | ||||||
|  |         Send any pending response headers and the given data as body. | ||||||
|  | 
 | ||||||
|  |         :param data: str or bytes to be written | ||||||
|  |         :param end_stream: whether to close the stream after this block | ||||||
|  |         """ | ||||||
|  |         if data is None and end_stream is None: | ||||||
|  |             end_stream = True | ||||||
|  |         if self.stream is None: | ||||||
|  |             raise SanicException( | ||||||
|  |                 "No stream is connected to the response object instance." | ||||||
|  |             ) | ||||||
|  |         if self.stream.send is None: | ||||||
|  |             if end_stream and not data: | ||||||
|  |                 return | ||||||
|  |             raise ServerError( | ||||||
|  |                 "Response stream was ended, no more response data is " | ||||||
|  |                 "allowed to be sent." | ||||||
|  |             ) | ||||||
|  |         data = ( | ||||||
|  |             data.encode()  # type: ignore | ||||||
|  |             if hasattr(data, "encode") | ||||||
|  |             else data or b"" | ||||||
|  |         ) | ||||||
|  |         await self.stream.send( | ||||||
|  |             data,  # type: ignore | ||||||
|  |             end_stream=end_stream or False, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class HTTPResponse(BaseHTTPResponse): | ||||||
|  |     """ | ||||||
|  |     HTTP response to be sent back to the client. | ||||||
|  | 
 | ||||||
|  |     :param body: the body content to be returned | ||||||
|  |     :type body: Optional[bytes] | ||||||
|  |     :param status: HTTP response number. **Default=200** | ||||||
|  |     :type status: int | ||||||
|  |     :param headers: headers to be returned | ||||||
|  |     :type headers: Optional; | ||||||
|  |     :param content_type: content type to be returned (as a header) | ||||||
|  |     :type content_type: Optional[str] | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     __slots__ = () | ||||||
|  | 
 | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         body: Optional[AnyStr] = None, | ||||||
|  |         status: int = 200, | ||||||
|  |         headers: Optional[Union[Header, Dict[str, str]]] = None, | ||||||
|  |         content_type: Optional[str] = None, | ||||||
|  |     ): | ||||||
|  |         super().__init__() | ||||||
|  | 
 | ||||||
|  |         self.content_type: Optional[str] = content_type | ||||||
|  |         self.body = self._encode_body(body) | ||||||
|  |         self.status = status | ||||||
|  |         self.headers = Header(headers or {}) | ||||||
|  |         self._cookies = None | ||||||
|  | 
 | ||||||
|  |     async def eof(self): | ||||||
|  |         await self.send("", True) | ||||||
|  | 
 | ||||||
|  |     async def __aenter__(self): | ||||||
|  |         return self.send | ||||||
|  | 
 | ||||||
|  |     async def __aexit__(self, *_): | ||||||
|  |         await self.eof() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def empty( | def empty( | ||||||
| @@ -37,7 +229,7 @@ def json( | |||||||
|     content_type: str = "application/json", |     content_type: str = "application/json", | ||||||
|     dumps: Optional[Callable[..., str]] = None, |     dumps: Optional[Callable[..., str]] = None, | ||||||
|     **kwargs: Any, |     **kwargs: Any, | ||||||
| ) -> JSONResponse: | ) -> HTTPResponse: | ||||||
|     """ |     """ | ||||||
|     Returns response object with body in json format. |     Returns response object with body in json format. | ||||||
| 
 | 
 | ||||||
| @@ -46,14 +238,13 @@ def json( | |||||||
|     :param headers: Custom Headers. |     :param headers: Custom Headers. | ||||||
|     :param kwargs: Remaining arguments that are passed to the json encoder. |     :param kwargs: Remaining arguments that are passed to the json encoder. | ||||||
|     """ |     """ | ||||||
| 
 |     if not dumps: | ||||||
|     return JSONResponse( |         dumps = BaseHTTPResponse._dumps | ||||||
|         body, |     return HTTPResponse( | ||||||
|         status=status, |         dumps(body, **kwargs), | ||||||
|         headers=headers, |         headers=headers, | ||||||
|  |         status=status, | ||||||
|         content_type=content_type, |         content_type=content_type, | ||||||
|         dumps=dumps, |  | ||||||
|         **kwargs, |  | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @@ -274,6 +465,80 @@ def redirect( | |||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class ResponseStream: | ||||||
|  |     """ | ||||||
|  |     ResponseStream is a compat layer to bridge the gap after the deprecation | ||||||
|  |     of StreamingHTTPResponse. It will be removed when: | ||||||
|  |     - file_stream is moved to new style streaming | ||||||
|  |     - file and file_stream are combined into a single API | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     __slots__ = ( | ||||||
|  |         "_cookies", | ||||||
|  |         "content_type", | ||||||
|  |         "headers", | ||||||
|  |         "request", | ||||||
|  |         "response", | ||||||
|  |         "status", | ||||||
|  |         "streaming_fn", | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         streaming_fn: Callable[ | ||||||
|  |             [Union[BaseHTTPResponse, ResponseStream]], | ||||||
|  |             Coroutine[Any, Any, None], | ||||||
|  |         ], | ||||||
|  |         status: int = 200, | ||||||
|  |         headers: Optional[Union[Header, Dict[str, str]]] = None, | ||||||
|  |         content_type: Optional[str] = None, | ||||||
|  |     ): | ||||||
|  |         self.streaming_fn = streaming_fn | ||||||
|  |         self.status = status | ||||||
|  |         self.headers = headers or Header() | ||||||
|  |         self.content_type = content_type | ||||||
|  |         self.request: Optional[Request] = None | ||||||
|  |         self._cookies: Optional[CookieJar] = None | ||||||
|  | 
 | ||||||
|  |     async def write(self, message: str): | ||||||
|  |         await self.response.send(message) | ||||||
|  | 
 | ||||||
|  |     async def stream(self) -> HTTPResponse: | ||||||
|  |         if not self.request: | ||||||
|  |             raise ServerError("Attempted response to unknown request") | ||||||
|  |         self.response = await self.request.respond( | ||||||
|  |             headers=self.headers, | ||||||
|  |             status=self.status, | ||||||
|  |             content_type=self.content_type, | ||||||
|  |         ) | ||||||
|  |         await self.streaming_fn(self) | ||||||
|  |         return self.response | ||||||
|  | 
 | ||||||
|  |     async def eof(self) -> None: | ||||||
|  |         await self.response.eof() | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def cookies(self) -> CookieJar: | ||||||
|  |         if self._cookies is None: | ||||||
|  |             self._cookies = CookieJar(self.headers) | ||||||
|  |         return self._cookies | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def processed_headers(self): | ||||||
|  |         return self.response.processed_headers | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def body(self): | ||||||
|  |         return self.response.body | ||||||
|  | 
 | ||||||
|  |     def __call__(self, request: Request) -> ResponseStream: | ||||||
|  |         self.request = request | ||||||
|  |         return self | ||||||
|  | 
 | ||||||
|  |     def __await__(self): | ||||||
|  |         return self.stream().__await__() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| async def file_stream( | async def file_stream( | ||||||
|     location: Union[str, PurePath], |     location: Union[str, PurePath], | ||||||
|     status: int = 200, |     status: int = 200, | ||||||
| @@ -1,36 +0,0 @@ | |||||||
| from .convenience import ( |  | ||||||
|     empty, |  | ||||||
|     file, |  | ||||||
|     file_stream, |  | ||||||
|     html, |  | ||||||
|     json, |  | ||||||
|     raw, |  | ||||||
|     redirect, |  | ||||||
|     text, |  | ||||||
|     validate_file, |  | ||||||
| ) |  | ||||||
| from .types import ( |  | ||||||
|     BaseHTTPResponse, |  | ||||||
|     HTTPResponse, |  | ||||||
|     JSONResponse, |  | ||||||
|     ResponseStream, |  | ||||||
|     json_dumps, |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| __all__ = ( |  | ||||||
|     "BaseHTTPResponse", |  | ||||||
|     "HTTPResponse", |  | ||||||
|     "JSONResponse", |  | ||||||
|     "ResponseStream", |  | ||||||
|     "empty", |  | ||||||
|     "json", |  | ||||||
|     "text", |  | ||||||
|     "raw", |  | ||||||
|     "html", |  | ||||||
|     "validate_file", |  | ||||||
|     "file", |  | ||||||
|     "redirect", |  | ||||||
|     "file_stream", |  | ||||||
|     "json_dumps", |  | ||||||
| ) |  | ||||||
| @@ -1,453 +0,0 @@ | |||||||
| from __future__ import annotations |  | ||||||
|  |  | ||||||
| from functools import partial |  | ||||||
| from typing import ( |  | ||||||
|     TYPE_CHECKING, |  | ||||||
|     Any, |  | ||||||
|     AnyStr, |  | ||||||
|     Callable, |  | ||||||
|     Coroutine, |  | ||||||
|     Dict, |  | ||||||
|     Iterator, |  | ||||||
|     Optional, |  | ||||||
|     Tuple, |  | ||||||
|     TypeVar, |  | ||||||
|     Union, |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| from sanic.compat import Header |  | ||||||
| from sanic.cookies import CookieJar |  | ||||||
| from sanic.exceptions import SanicException, ServerError |  | ||||||
| from sanic.helpers import ( |  | ||||||
|     Default, |  | ||||||
|     _default, |  | ||||||
|     has_message_body, |  | ||||||
|     remove_entity_headers, |  | ||||||
| ) |  | ||||||
| from sanic.http import Http |  | ||||||
|  |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING: |  | ||||||
|     from sanic.asgi import ASGIApp |  | ||||||
|     from sanic.http.http3 import HTTPReceiver |  | ||||||
|     from sanic.request import Request |  | ||||||
| else: |  | ||||||
|     Request = TypeVar("Request") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| try: |  | ||||||
|     from ujson import dumps as json_dumps |  | ||||||
| except ImportError: |  | ||||||
|     # This is done in order to ensure that the JSON response is |  | ||||||
|     # kept consistent across both ujson and inbuilt json usage. |  | ||||||
|     from json import dumps |  | ||||||
|  |  | ||||||
|     json_dumps = partial(dumps, separators=(",", ":")) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class BaseHTTPResponse: |  | ||||||
|     """ |  | ||||||
|     The base class for all HTTP Responses |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     __slots__ = ( |  | ||||||
|         "asgi", |  | ||||||
|         "body", |  | ||||||
|         "content_type", |  | ||||||
|         "stream", |  | ||||||
|         "status", |  | ||||||
|         "headers", |  | ||||||
|         "_cookies", |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     _dumps = json_dumps |  | ||||||
|  |  | ||||||
|     def __init__(self): |  | ||||||
|         self.asgi: bool = False |  | ||||||
|         self.body: Optional[bytes] = None |  | ||||||
|         self.content_type: Optional[str] = None |  | ||||||
|         self.stream: Optional[Union[Http, ASGIApp, HTTPReceiver]] = None |  | ||||||
|         self.status: int = None |  | ||||||
|         self.headers = Header({}) |  | ||||||
|         self._cookies: Optional[CookieJar] = None |  | ||||||
|  |  | ||||||
|     def __repr__(self): |  | ||||||
|         class_name = self.__class__.__name__ |  | ||||||
|         return f"<{class_name}: {self.status} {self.content_type}>" |  | ||||||
|  |  | ||||||
|     def _encode_body(self, data: Optional[AnyStr]): |  | ||||||
|         if data is None: |  | ||||||
|             return b"" |  | ||||||
|         return ( |  | ||||||
|             data.encode() if hasattr(data, "encode") else data  # type: ignore |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def cookies(self) -> CookieJar: |  | ||||||
|         """ |  | ||||||
|         The response cookies. Cookies should be set and written as follows: |  | ||||||
|  |  | ||||||
|         .. code-block:: python |  | ||||||
|  |  | ||||||
|                 response.cookies["test"] = "It worked!" |  | ||||||
|                 response.cookies["test"]["domain"] = ".yummy-yummy-cookie.com" |  | ||||||
|                 response.cookies["test"]["httponly"] = True |  | ||||||
|  |  | ||||||
|         `See user guide re: cookies |  | ||||||
|         <https://sanic.dev/en/guide/basics/cookies.html>` |  | ||||||
|  |  | ||||||
|         :return: the cookie jar |  | ||||||
|         :rtype: CookieJar |  | ||||||
|         """ |  | ||||||
|         if self._cookies is None: |  | ||||||
|             self._cookies = CookieJar(self.headers) |  | ||||||
|         return self._cookies |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def processed_headers(self) -> Iterator[Tuple[bytes, bytes]]: |  | ||||||
|         """ |  | ||||||
|         Obtain a list of header tuples encoded in bytes for sending. |  | ||||||
|  |  | ||||||
|         Add and remove headers based on status and content_type. |  | ||||||
|  |  | ||||||
|         :return: response headers |  | ||||||
|         :rtype: Tuple[Tuple[bytes, bytes], ...] |  | ||||||
|         """ |  | ||||||
|         # TODO: Make a blacklist set of header names and then filter with that |  | ||||||
|         if self.status in (304, 412):  # Not Modified, Precondition Failed |  | ||||||
|             self.headers = remove_entity_headers(self.headers) |  | ||||||
|         if has_message_body(self.status): |  | ||||||
|             self.headers.setdefault("content-type", self.content_type) |  | ||||||
|         # Encode headers into bytes |  | ||||||
|         return ( |  | ||||||
|             (name.encode("ascii"), f"{value}".encode(errors="surrogateescape")) |  | ||||||
|             for name, value in self.headers.items() |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     async def send( |  | ||||||
|         self, |  | ||||||
|         data: Optional[AnyStr] = None, |  | ||||||
|         end_stream: Optional[bool] = None, |  | ||||||
|     ) -> None: |  | ||||||
|         """ |  | ||||||
|         Send any pending response headers and the given data as body. |  | ||||||
|  |  | ||||||
|         :param data: str or bytes to be written |  | ||||||
|         :param end_stream: whether to close the stream after this block |  | ||||||
|         """ |  | ||||||
|         if data is None and end_stream is None: |  | ||||||
|             end_stream = True |  | ||||||
|         if self.stream is None: |  | ||||||
|             raise SanicException( |  | ||||||
|                 "No stream is connected to the response object instance." |  | ||||||
|             ) |  | ||||||
|         if self.stream.send is None: |  | ||||||
|             if end_stream and not data: |  | ||||||
|                 return |  | ||||||
|             raise ServerError( |  | ||||||
|                 "Response stream was ended, no more response data is " |  | ||||||
|                 "allowed to be sent." |  | ||||||
|             ) |  | ||||||
|         data = ( |  | ||||||
|             data.encode()  # type: ignore |  | ||||||
|             if hasattr(data, "encode") |  | ||||||
|             else data or b"" |  | ||||||
|         ) |  | ||||||
|         await self.stream.send( |  | ||||||
|             data,  # type: ignore |  | ||||||
|             end_stream=end_stream or False, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class HTTPResponse(BaseHTTPResponse): |  | ||||||
|     """ |  | ||||||
|     HTTP response to be sent back to the client. |  | ||||||
|  |  | ||||||
|     :param body: the body content to be returned |  | ||||||
|     :type body: Optional[bytes] |  | ||||||
|     :param status: HTTP response number. **Default=200** |  | ||||||
|     :type status: int |  | ||||||
|     :param headers: headers to be returned |  | ||||||
|     :type headers: Optional; |  | ||||||
|     :param content_type: content type to be returned (as a header) |  | ||||||
|     :type content_type: Optional[str] |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     __slots__ = () |  | ||||||
|  |  | ||||||
|     def __init__( |  | ||||||
|         self, |  | ||||||
|         body: Optional[Any] = None, |  | ||||||
|         status: int = 200, |  | ||||||
|         headers: Optional[Union[Header, Dict[str, str]]] = None, |  | ||||||
|         content_type: Optional[str] = None, |  | ||||||
|     ): |  | ||||||
|         super().__init__() |  | ||||||
|  |  | ||||||
|         self.content_type: Optional[str] = content_type |  | ||||||
|         self.body = self._encode_body(body) |  | ||||||
|         self.status = status |  | ||||||
|         self.headers = Header(headers or {}) |  | ||||||
|         self._cookies = None |  | ||||||
|  |  | ||||||
|     async def eof(self): |  | ||||||
|         await self.send("", True) |  | ||||||
|  |  | ||||||
|     async def __aenter__(self): |  | ||||||
|         return self.send |  | ||||||
|  |  | ||||||
|     async def __aexit__(self, *_): |  | ||||||
|         await self.eof() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class JSONResponse(HTTPResponse): |  | ||||||
|     """ |  | ||||||
|     HTTP response to be sent back to the client, when the response |  | ||||||
|     is of json type. Offers several utilities to manipulate common |  | ||||||
|     json data types. |  | ||||||
|  |  | ||||||
|     :param body: the body content to be returned |  | ||||||
|     :type body: Optional[Any] |  | ||||||
|     :param status: HTTP response number. **Default=200** |  | ||||||
|     :type status: int |  | ||||||
|     :param headers: headers to be returned |  | ||||||
|     :type headers: Optional |  | ||||||
|     :param content_type: content type to be returned (as a header) |  | ||||||
|     :type content_type: Optional[str] |  | ||||||
|     :param dumps: json.dumps function to use |  | ||||||
|     :type dumps: Optional[Callable] |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     __slots__ = ( |  | ||||||
|         "_body", |  | ||||||
|         "_body_manually_set", |  | ||||||
|         "_initialized", |  | ||||||
|         "_raw_body", |  | ||||||
|         "_use_dumps", |  | ||||||
|         "_use_dumps_kwargs", |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     def __init__( |  | ||||||
|         self, |  | ||||||
|         body: Optional[Any] = None, |  | ||||||
|         status: int = 200, |  | ||||||
|         headers: Optional[Union[Header, Dict[str, str]]] = None, |  | ||||||
|         content_type: Optional[str] = None, |  | ||||||
|         dumps: Optional[Callable[..., str]] = None, |  | ||||||
|         **kwargs: Any, |  | ||||||
|     ): |  | ||||||
|         self._initialized = False |  | ||||||
|         self._body_manually_set = False |  | ||||||
|  |  | ||||||
|         self._use_dumps = dumps or BaseHTTPResponse._dumps |  | ||||||
|         self._use_dumps_kwargs = kwargs |  | ||||||
|  |  | ||||||
|         self._raw_body = body |  | ||||||
|  |  | ||||||
|         super().__init__( |  | ||||||
|             self._encode_body(self._use_dumps(body, **self._use_dumps_kwargs)), |  | ||||||
|             headers=headers, |  | ||||||
|             status=status, |  | ||||||
|             content_type=content_type, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         self._initialized = True |  | ||||||
|  |  | ||||||
|     def _check_body_not_manually_set(self): |  | ||||||
|         if self._body_manually_set: |  | ||||||
|             raise SanicException( |  | ||||||
|                 "Cannot use raw_body after body has been manually set." |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def raw_body(self) -> Optional[Any]: |  | ||||||
|         """Returns the raw body, as long as body has not been manually |  | ||||||
|         set previously. |  | ||||||
|  |  | ||||||
|         NOTE: This object should not be mutated, as it will not be |  | ||||||
|         reflected in the response body. If you need to mutate the |  | ||||||
|         response body, consider using one of the provided methods in |  | ||||||
|         this class or alternatively call set_body() with the mutated |  | ||||||
|         object afterwards or set the raw_body property to it. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         self._check_body_not_manually_set() |  | ||||||
|         return self._raw_body |  | ||||||
|  |  | ||||||
|     @raw_body.setter |  | ||||||
|     def raw_body(self, value: Any): |  | ||||||
|         self._body_manually_set = False |  | ||||||
|         self._body = self._encode_body( |  | ||||||
|             self._use_dumps(value, **self._use_dumps_kwargs) |  | ||||||
|         ) |  | ||||||
|         self._raw_body = value |  | ||||||
|  |  | ||||||
|     @property  # type: ignore |  | ||||||
|     def body(self) -> Optional[bytes]:  # type: ignore |  | ||||||
|         return self._body |  | ||||||
|  |  | ||||||
|     @body.setter |  | ||||||
|     def body(self, value: Optional[bytes]): |  | ||||||
|         self._body = value |  | ||||||
|         if not self._initialized: |  | ||||||
|             return |  | ||||||
|         self._body_manually_set = True |  | ||||||
|  |  | ||||||
|     def set_body( |  | ||||||
|         self, |  | ||||||
|         body: Any, |  | ||||||
|         dumps: Optional[Callable[..., str]] = None, |  | ||||||
|         **dumps_kwargs: Any, |  | ||||||
|     ) -> None: |  | ||||||
|         """Sets a new response body using the given dumps function |  | ||||||
|         and kwargs, or falling back to the defaults given when |  | ||||||
|         creating the object if none are specified. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         self._body_manually_set = False |  | ||||||
|         self._raw_body = body |  | ||||||
|  |  | ||||||
|         use_dumps = dumps or self._use_dumps |  | ||||||
|         use_dumps_kwargs = dumps_kwargs if dumps else self._use_dumps_kwargs |  | ||||||
|  |  | ||||||
|         self._body = self._encode_body(use_dumps(body, **use_dumps_kwargs)) |  | ||||||
|  |  | ||||||
|     def append(self, value: Any) -> None: |  | ||||||
|         """Appends a value to the response raw_body, ensuring that |  | ||||||
|         body is kept up to date. This can only be used if raw_body |  | ||||||
|         is a list. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         self._check_body_not_manually_set() |  | ||||||
|  |  | ||||||
|         if not isinstance(self._raw_body, list): |  | ||||||
|             raise SanicException("Cannot append to a non-list object.") |  | ||||||
|  |  | ||||||
|         self._raw_body.append(value) |  | ||||||
|         self.raw_body = self._raw_body |  | ||||||
|  |  | ||||||
|     def extend(self, value: Any) -> None: |  | ||||||
|         """Extends the response's raw_body with the given values, ensuring |  | ||||||
|         that body is kept up to date. This can only be used if raw_body is |  | ||||||
|         a list. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         self._check_body_not_manually_set() |  | ||||||
|  |  | ||||||
|         if not isinstance(self._raw_body, list): |  | ||||||
|             raise SanicException("Cannot extend a non-list object.") |  | ||||||
|  |  | ||||||
|         self._raw_body.extend(value) |  | ||||||
|         self.raw_body = self._raw_body |  | ||||||
|  |  | ||||||
|     def update(self, *args, **kwargs) -> None: |  | ||||||
|         """Updates the response's raw_body with the given values, ensuring |  | ||||||
|         that body is kept up to date. This can only be used if raw_body is |  | ||||||
|         a dict. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         self._check_body_not_manually_set() |  | ||||||
|  |  | ||||||
|         if not isinstance(self._raw_body, dict): |  | ||||||
|             raise SanicException("Cannot update a non-dict object.") |  | ||||||
|  |  | ||||||
|         self._raw_body.update(*args, **kwargs) |  | ||||||
|         self.raw_body = self._raw_body |  | ||||||
|  |  | ||||||
|     def pop(self, key: Any, default: Any = _default) -> Any: |  | ||||||
|         """Pops a key from the response's raw_body, ensuring that body is |  | ||||||
|         kept up to date. This can only be used if raw_body is a dict or a |  | ||||||
|         list. |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         self._check_body_not_manually_set() |  | ||||||
|  |  | ||||||
|         if not isinstance(self._raw_body, (list, dict)): |  | ||||||
|             raise SanicException( |  | ||||||
|                 "Cannot pop from a non-list and non-dict object." |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         if isinstance(default, Default): |  | ||||||
|             value = self._raw_body.pop(key) |  | ||||||
|         elif isinstance(self._raw_body, list): |  | ||||||
|             raise TypeError("pop doesn't accept a default argument for lists") |  | ||||||
|         else: |  | ||||||
|             value = self._raw_body.pop(key, default) |  | ||||||
|  |  | ||||||
|         self.raw_body = self._raw_body |  | ||||||
|  |  | ||||||
|         return value |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ResponseStream: |  | ||||||
|     """ |  | ||||||
|     ResponseStream is a compat layer to bridge the gap after the deprecation |  | ||||||
|     of StreamingHTTPResponse. It will be removed when: |  | ||||||
|     - file_stream is moved to new style streaming |  | ||||||
|     - file and file_stream are combined into a single API |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     __slots__ = ( |  | ||||||
|         "_cookies", |  | ||||||
|         "content_type", |  | ||||||
|         "headers", |  | ||||||
|         "request", |  | ||||||
|         "response", |  | ||||||
|         "status", |  | ||||||
|         "streaming_fn", |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     def __init__( |  | ||||||
|         self, |  | ||||||
|         streaming_fn: Callable[ |  | ||||||
|             [Union[BaseHTTPResponse, ResponseStream]], |  | ||||||
|             Coroutine[Any, Any, None], |  | ||||||
|         ], |  | ||||||
|         status: int = 200, |  | ||||||
|         headers: Optional[Union[Header, Dict[str, str]]] = None, |  | ||||||
|         content_type: Optional[str] = None, |  | ||||||
|     ): |  | ||||||
|         self.streaming_fn = streaming_fn |  | ||||||
|         self.status = status |  | ||||||
|         self.headers = headers or Header() |  | ||||||
|         self.content_type = content_type |  | ||||||
|         self.request: Optional[Request] = None |  | ||||||
|         self._cookies: Optional[CookieJar] = None |  | ||||||
|  |  | ||||||
|     async def write(self, message: str): |  | ||||||
|         await self.response.send(message) |  | ||||||
|  |  | ||||||
|     async def stream(self) -> HTTPResponse: |  | ||||||
|         if not self.request: |  | ||||||
|             raise ServerError("Attempted response to unknown request") |  | ||||||
|         self.response = await self.request.respond( |  | ||||||
|             headers=self.headers, |  | ||||||
|             status=self.status, |  | ||||||
|             content_type=self.content_type, |  | ||||||
|         ) |  | ||||||
|         await self.streaming_fn(self) |  | ||||||
|         return self.response |  | ||||||
|  |  | ||||||
|     async def eof(self) -> None: |  | ||||||
|         await self.response.eof() |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def cookies(self) -> CookieJar: |  | ||||||
|         if self._cookies is None: |  | ||||||
|             self._cookies = CookieJar(self.headers) |  | ||||||
|         return self._cookies |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def processed_headers(self): |  | ||||||
|         return self.response.processed_headers |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def body(self): |  | ||||||
|         return self.response.body |  | ||||||
|  |  | ||||||
|     def __call__(self, request: Request) -> ResponseStream: |  | ||||||
|         self.request = request |  | ||||||
|         return self |  | ||||||
|  |  | ||||||
|     def __await__(self): |  | ||||||
|         return self.stream().__await__() |  | ||||||
| @@ -13,6 +13,7 @@ from sanic_routing.route import Route | |||||||
| from sanic.constants import HTTP_METHODS | from sanic.constants import HTTP_METHODS | ||||||
| from sanic.errorpages import check_error_format | from sanic.errorpages import check_error_format | ||||||
| from sanic.exceptions import MethodNotAllowed, NotFound, SanicException | from sanic.exceptions import MethodNotAllowed, NotFound, SanicException | ||||||
|  | from sanic.handlers import RequestHandler | ||||||
| from sanic.models.handler_types import RouteHandler | from sanic.models.handler_types import RouteHandler | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -31,26 +32,28 @@ class Router(BaseRouter): | |||||||
|  |  | ||||||
|     def _get( |     def _get( | ||||||
|         self, path: str, method: str, host: Optional[str] |         self, path: str, method: str, host: Optional[str] | ||||||
|     ) -> Tuple[Route, RouteHandler, Dict[str, Any]]: |     ) -> Tuple[Route, RequestHandler, Dict[str, Any]]: | ||||||
|         try: |         try: | ||||||
|             return self.resolve( |             # We know this will always be RequestHandler, so we can ignore | ||||||
|  |             # typing issue here | ||||||
|  |             return self.resolve(  # type: ignore | ||||||
|                 path=path, |                 path=path, | ||||||
|                 method=method, |                 method=method, | ||||||
|                 extra={"host": host} if host else None, |                 extra={"host": host} if host else None, | ||||||
|             ) |             ) | ||||||
|         except RoutingNotFound as e: |         except RoutingNotFound as e: | ||||||
|             raise NotFound(f"Requested URL {e.path} not found") from None |             raise NotFound("Requested URL {} not found".format(e.path)) | ||||||
|         except NoMethod as e: |         except NoMethod as e: | ||||||
|             raise MethodNotAllowed( |             raise MethodNotAllowed( | ||||||
|                 f"Method {method} not allowed for URL {path}", |                 "Method {} not allowed for URL {}".format(method, path), | ||||||
|                 method=method, |                 method=method, | ||||||
|                 allowed_methods=e.allowed_methods, |                 allowed_methods=e.allowed_methods, | ||||||
|             ) from None |             ) | ||||||
|  |  | ||||||
|     @lru_cache(maxsize=ROUTER_CACHE_SIZE) |     @lru_cache(maxsize=ROUTER_CACHE_SIZE) | ||||||
|     def get(  # type: ignore |     def get(  # type: ignore | ||||||
|         self, path: str, method: str, host: Optional[str] |         self, path: str, method: str, host: Optional[str] | ||||||
|     ) -> Tuple[Route, RouteHandler, Dict[str, Any]]: |     ) -> Tuple[Route, RequestHandler, Dict[str, Any]]: | ||||||
|         """ |         """ | ||||||
|         Retrieve a `Route` object containing the details about how to handle |         Retrieve a `Route` object containing the details about how to handle | ||||||
|         a response for a given request |         a response for a given request | ||||||
| @@ -59,9 +62,8 @@ class Router(BaseRouter): | |||||||
|         :type request: Request |         :type request: Request | ||||||
|         :return: details needed for handling the request and returning the |         :return: details needed for handling the request and returning the | ||||||
|             correct response |             correct response | ||||||
|         :rtype: Tuple[ Route, RouteHandler, Dict[str, Any]] |         :rtype: Tuple[ Route, RequestHandler, Dict[str, Any]] | ||||||
|         """ |         """ | ||||||
|         __tracebackhide__ = True |  | ||||||
|         return self._get(path, method, host) |         return self._get(path, method, host) | ||||||
|  |  | ||||||
|     def add(  # type: ignore |     def add(  # type: ignore | ||||||
| @@ -115,7 +117,7 @@ class Router(BaseRouter): | |||||||
|  |  | ||||||
|         params = dict( |         params = dict( | ||||||
|             path=uri, |             path=uri, | ||||||
|             handler=handler, |             handler=RequestHandler(handler, [], []), | ||||||
|             methods=frozenset(map(str, methods)) if methods else None, |             methods=frozenset(map(str, methods)) if methods else None, | ||||||
|             name=name, |             name=name, | ||||||
|             strict=strict_slashes, |             strict=strict_slashes, | ||||||
| @@ -134,14 +136,14 @@ class Router(BaseRouter): | |||||||
|                 params.update({"requirements": {"host": host}}) |                 params.update({"requirements": {"host": host}}) | ||||||
|  |  | ||||||
|             route = super().add(**params)  # type: ignore |             route = super().add(**params)  # type: ignore | ||||||
|             route.extra.ignore_body = ignore_body |             route.ctx.ignore_body = ignore_body | ||||||
|             route.extra.stream = stream |             route.ctx.stream = stream | ||||||
|             route.extra.hosts = hosts |             route.ctx.hosts = hosts | ||||||
|             route.extra.static = static |             route.ctx.static = static | ||||||
|             route.extra.error_format = error_format |             route.ctx.error_format = error_format | ||||||
|  |  | ||||||
|             if error_format: |             if error_format: | ||||||
|                 check_error_format(route.extra.error_format) |                 check_error_format(route.ctx.error_format) | ||||||
|  |  | ||||||
|             routes.append(route) |             routes.append(route) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -94,6 +94,7 @@ def watchdog(sleep_interval, reload_dirs): | |||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         while True: |         while True: | ||||||
|  |  | ||||||
|             changed = set() |             changed = set() | ||||||
|             for filename in itertools.chain( |             for filename in itertools.chain( | ||||||
|                 _iter_module_files(), |                 _iter_module_files(), | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
|  | from distutils.util import strtobool | ||||||
| from os import getenv | from os import getenv | ||||||
|  |  | ||||||
| from sanic.compat import OS_IS_WINDOWS | from sanic.compat import OS_IS_WINDOWS | ||||||
| from sanic.log import error_logger | from sanic.log import error_logger | ||||||
| from sanic.utils import str_to_bool |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def try_use_uvloop() -> None: | def try_use_uvloop() -> None: | ||||||
| @@ -35,7 +35,7 @@ def try_use_uvloop() -> None: | |||||||
|         ) |         ) | ||||||
|         return |         return | ||||||
|  |  | ||||||
|     uvloop_install_removed = str_to_bool(getenv("SANIC_NO_UVLOOP", "no")) |     uvloop_install_removed = strtobool(getenv("SANIC_NO_UVLOOP", "no")) | ||||||
|     if uvloop_install_removed: |     if uvloop_install_removed: | ||||||
|         error_logger.info( |         error_logger.info( | ||||||
|             "You are requesting to run Sanic using uvloop, but the " |             "You are requesting to run Sanic using uvloop, but the " | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ from __future__ import annotations | |||||||
|  |  | ||||||
| from typing import TYPE_CHECKING, Optional | from typing import TYPE_CHECKING, Optional | ||||||
|  |  | ||||||
|  | from sanic.handlers import RequestManager | ||||||
| from sanic.http.constants import HTTP | from sanic.http.constants import HTTP | ||||||
| from sanic.http.http3 import Http3 | from sanic.http.http3 import Http3 | ||||||
| from sanic.touchup.meta import TouchUpMeta | from sanic.touchup.meta import TouchUpMeta | ||||||
| @@ -57,7 +58,7 @@ class HttpProtocolMixin: | |||||||
|     def _setup(self): |     def _setup(self): | ||||||
|         self.request: Optional[Request] = None |         self.request: Optional[Request] = None | ||||||
|         self.access_log = self.app.config.ACCESS_LOG |         self.access_log = self.app.config.ACCESS_LOG | ||||||
|         self.request_handler = self.app.handle_request |         self.request_handler = RequestManager | ||||||
|         self.error_handler = self.app.error_handler |         self.error_handler = self.app.error_handler | ||||||
|         self.request_timeout = self.app.config.REQUEST_TIMEOUT |         self.request_timeout = self.app.config.REQUEST_TIMEOUT | ||||||
|         self.response_timeout = self.app.config.RESPONSE_TIMEOUT |         self.response_timeout = self.app.config.RESPONSE_TIMEOUT | ||||||
|   | |||||||
| @@ -1,13 +1,7 @@ | |||||||
| from typing import TYPE_CHECKING, Optional, Sequence, cast | from typing import TYPE_CHECKING, Optional, Sequence, cast | ||||||
|  |  | ||||||
|  | from websockets.connection import CLOSED, CLOSING, OPEN | ||||||
| try:  # websockets < 11.0 | from websockets.server import ServerConnection | ||||||
|     from websockets.connection import State |  | ||||||
|     from websockets.server import ServerConnection as ServerProtocol |  | ||||||
| except ImportError:  # websockets >= 11.0 |  | ||||||
|     from websockets.protocol import State  # type: ignore |  | ||||||
|     from websockets.server import ServerProtocol  # type: ignore |  | ||||||
|  |  | ||||||
| from websockets.typing import Subprotocol | from websockets.typing import Subprotocol | ||||||
|  |  | ||||||
| from sanic.exceptions import ServerError | from sanic.exceptions import ServerError | ||||||
| @@ -21,11 +15,6 @@ if TYPE_CHECKING: | |||||||
|     from websockets import http11 |     from websockets import http11 | ||||||
|  |  | ||||||
|  |  | ||||||
| OPEN = State.OPEN |  | ||||||
| CLOSING = State.CLOSING |  | ||||||
| CLOSED = State.CLOSED |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class WebSocketProtocol(HttpProtocol): | class WebSocketProtocol(HttpProtocol): | ||||||
|     __slots__ = ( |     __slots__ = ( | ||||||
|         "websocket", |         "websocket", | ||||||
| @@ -85,7 +74,7 @@ class WebSocketProtocol(HttpProtocol): | |||||||
|         # Called by Sanic Server when shutting down |         # Called by Sanic Server when shutting down | ||||||
|         # If we've upgraded to websocket, shut it down |         # If we've upgraded to websocket, shut it down | ||||||
|         if self.websocket is not None: |         if self.websocket is not None: | ||||||
|             if self.websocket.ws_proto.state in (CLOSING, CLOSED): |             if self.websocket.connection.state in (CLOSING, CLOSED): | ||||||
|                 return True |                 return True | ||||||
|             elif self.websocket.loop is not None: |             elif self.websocket.loop is not None: | ||||||
|                 self.websocket.loop.create_task(self.websocket.close(1001)) |                 self.websocket.loop.create_task(self.websocket.close(1001)) | ||||||
| @@ -101,7 +90,7 @@ class WebSocketProtocol(HttpProtocol): | |||||||
|         try: |         try: | ||||||
|             if subprotocols is not None: |             if subprotocols is not None: | ||||||
|                 # subprotocols can be a set or frozenset, |                 # subprotocols can be a set or frozenset, | ||||||
|                 # but ServerProtocol needs a list |                 # but ServerConnection needs a list | ||||||
|                 subprotocols = cast( |                 subprotocols = cast( | ||||||
|                     Optional[Sequence[Subprotocol]], |                     Optional[Sequence[Subprotocol]], | ||||||
|                     list( |                     list( | ||||||
| @@ -111,13 +100,13 @@ class WebSocketProtocol(HttpProtocol): | |||||||
|                         ] |                         ] | ||||||
|                     ), |                     ), | ||||||
|                 ) |                 ) | ||||||
|             ws_proto = ServerProtocol( |             ws_conn = ServerConnection( | ||||||
|                 max_size=self.websocket_max_size, |                 max_size=self.websocket_max_size, | ||||||
|                 subprotocols=subprotocols, |                 subprotocols=subprotocols, | ||||||
|                 state=OPEN, |                 state=OPEN, | ||||||
|                 logger=logger, |                 logger=logger, | ||||||
|             ) |             ) | ||||||
|             resp: "http11.Response" = ws_proto.accept(request) |             resp: "http11.Response" = ws_conn.accept(request) | ||||||
|         except Exception: |         except Exception: | ||||||
|             msg = ( |             msg = ( | ||||||
|                 "Failed to open a WebSocket connection.\n" |                 "Failed to open a WebSocket connection.\n" | ||||||
| @@ -140,7 +129,7 @@ class WebSocketProtocol(HttpProtocol): | |||||||
|         else: |         else: | ||||||
|             raise ServerError(resp.body, resp.status_code) |             raise ServerError(resp.body, resp.status_code) | ||||||
|         self.websocket = WebsocketImplProtocol( |         self.websocket = WebsocketImplProtocol( | ||||||
|             ws_proto, |             ws_conn, | ||||||
|             ping_interval=self.websocket_ping_interval, |             ping_interval=self.websocket_ping_interval, | ||||||
|             ping_timeout=self.websocket_ping_timeout, |             ping_timeout=self.websocket_ping_timeout, | ||||||
|             close_timeout=self.websocket_timeout, |             close_timeout=self.websocket_timeout, | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ from signal import signal as signal_func | |||||||
| from sanic.application.ext import setup_ext | from sanic.application.ext import setup_ext | ||||||
| from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows | from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows | ||||||
| from sanic.http.http3 import SessionTicketStore, get_config | from sanic.http.http3 import SessionTicketStore, get_config | ||||||
| from sanic.log import error_logger, server_logger | from sanic.log import error_logger, logger | ||||||
| from sanic.models.server_types import Signal | from sanic.models.server_types import Signal | ||||||
| from sanic.server.async_server import AsyncioServer | from sanic.server.async_server import AsyncioServer | ||||||
| from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol | from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol | ||||||
| @@ -149,12 +149,12 @@ def _setup_system_signals( | |||||||
| def _run_server_forever(loop, before_stop, after_stop, cleanup, unix): | def _run_server_forever(loop, before_stop, after_stop, cleanup, unix): | ||||||
|     pid = os.getpid() |     pid = os.getpid() | ||||||
|     try: |     try: | ||||||
|         server_logger.info("Starting worker [%s]", pid) |         logger.info("Starting worker [%s]", pid) | ||||||
|         loop.run_forever() |         loop.run_forever() | ||||||
|     except KeyboardInterrupt: |     except KeyboardInterrupt: | ||||||
|         pass |         pass | ||||||
|     finally: |     finally: | ||||||
|         server_logger.info("Stopping worker [%s]", pid) |         logger.info("Stopping worker [%s]", pid) | ||||||
|  |  | ||||||
|         loop.run_until_complete(before_stop()) |         loop.run_until_complete(before_stop()) | ||||||
|  |  | ||||||
| @@ -200,7 +200,7 @@ def _serve_http_1( | |||||||
|     asyncio_server_kwargs = ( |     asyncio_server_kwargs = ( | ||||||
|         asyncio_server_kwargs if asyncio_server_kwargs else {} |         asyncio_server_kwargs if asyncio_server_kwargs else {} | ||||||
|     ) |     ) | ||||||
|     if OS_IS_WINDOWS and sock: |     if OS_IS_WINDOWS: | ||||||
|         pid = os.getpid() |         pid = os.getpid() | ||||||
|         sock = sock.share(pid) |         sock = sock.share(pid) | ||||||
|         sock = socket.fromshare(sock) |         sock = socket.fromshare(sock) | ||||||
| @@ -229,7 +229,6 @@ def _serve_http_1( | |||||||
|  |  | ||||||
|     loop.run_until_complete(app._startup()) |     loop.run_until_complete(app._startup()) | ||||||
|     loop.run_until_complete(app._server_event("init", "before")) |     loop.run_until_complete(app._server_event("init", "before")) | ||||||
|     app.ack() |  | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|         http_server = loop.run_until_complete(server_coroutine) |         http_server = loop.run_until_complete(server_coroutine) | ||||||
| @@ -307,7 +306,6 @@ def _serve_http_3( | |||||||
|     server = AsyncioServer(app, loop, coro, []) |     server = AsyncioServer(app, loop, coro, []) | ||||||
|     loop.run_until_complete(server.startup()) |     loop.run_until_complete(server.startup()) | ||||||
|     loop.run_until_complete(server.before_start()) |     loop.run_until_complete(server.before_start()) | ||||||
|     app.ack() |  | ||||||
|     loop.run_until_complete(server) |     loop.run_until_complete(server) | ||||||
|     _setup_system_signals(app, run_multiple, register_sys_signals, loop) |     _setup_system_signals(app, run_multiple, register_sys_signals, loop) | ||||||
|     loop.run_until_complete(server.after_start()) |     loop.run_until_complete(server.after_start()) | ||||||
| @@ -374,9 +372,7 @@ def serve_multiple(server_settings, workers): | |||||||
|     processes = [] |     processes = [] | ||||||
|  |  | ||||||
|     def sig_handler(signal, frame): |     def sig_handler(signal, frame): | ||||||
|         server_logger.info( |         logger.info("Received signal %s. Shutting down.", Signals(signal).name) | ||||||
|             "Received signal %s. Shutting down.", Signals(signal).name |  | ||||||
|         ) |  | ||||||
|         for process in processes: |         for process in processes: | ||||||
|             os.kill(process.pid, SIGTERM) |             os.kill(process.pid, SIGTERM) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -113,16 +113,13 @@ def configure_socket( | |||||||
|                 backlog=backlog, |                 backlog=backlog, | ||||||
|             ) |             ) | ||||||
|         except OSError as e:  # no cov |         except OSError as e:  # no cov | ||||||
|             error = ServerError( |             raise ServerError( | ||||||
|                 f"Sanic server could not start: {e}.\n\n" |                 f"Sanic server could not start: {e}.\n" | ||||||
|                 "This may have happened if you are running Sanic in the " |                 "This may have happened if you are running Sanic in the " | ||||||
|                 "global scope and not inside of a " |                 "global scope and not inside of a " | ||||||
|                 '`if __name__ == "__main__"` block.\n\nSee more information: ' |                 '`if __name__ == "__main__"` block. See more information: ' | ||||||
|                 "https://sanic.dev/en/guide/deployment/manager.html#" |                 "____." | ||||||
|                 "how-sanic-server-starts-processes\n" |             ) from e | ||||||
|             ) |  | ||||||
|             error.quiet = True |  | ||||||
|             raise error |  | ||||||
|         sock.set_inheritable(True) |         sock.set_inheritable(True) | ||||||
|         server_settings["sock"] = sock |         server_settings["sock"] = sock | ||||||
|         server_settings["host"] = None |         server_settings["host"] = None | ||||||
|   | |||||||
| @@ -9,10 +9,8 @@ from typing import ( | |||||||
|     Union, |     Union, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| from sanic.exceptions import InvalidUsage |  | ||||||
|  |  | ||||||
|  | ASIMessage = MutableMapping[str, Any] | ||||||
| ASGIMessage = MutableMapping[str, Any] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class WebSocketConnection: | class WebSocketConnection: | ||||||
| @@ -27,8 +25,8 @@ class WebSocketConnection: | |||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         send: Callable[[ASGIMessage], Awaitable[None]], |         send: Callable[[ASIMessage], Awaitable[None]], | ||||||
|         receive: Callable[[], Awaitable[ASGIMessage]], |         receive: Callable[[], Awaitable[ASIMessage]], | ||||||
|         subprotocols: Optional[List[str]] = None, |         subprotocols: Optional[List[str]] = None, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         self._send = send |         self._send = send | ||||||
| @@ -45,17 +43,11 @@ class WebSocketConnection: | |||||||
|  |  | ||||||
|         await self._send(message) |         await self._send(message) | ||||||
|  |  | ||||||
|     async def recv(self, *args, **kwargs) -> Optional[Union[str, bytes]]: |     async def recv(self, *args, **kwargs) -> Optional[str]: | ||||||
|         message = await self._receive() |         message = await self._receive() | ||||||
|  |  | ||||||
|         if message["type"] == "websocket.receive": |         if message["type"] == "websocket.receive": | ||||||
|             try: |  | ||||||
|             return message["text"] |             return message["text"] | ||||||
|             except KeyError: |  | ||||||
|                 try: |  | ||||||
|                     return message["bytes"] |  | ||||||
|                 except KeyError: |  | ||||||
|                     raise InvalidUsage("Bad ASGI message received") |  | ||||||
|         elif message["type"] == "websocket.disconnect": |         elif message["type"] == "websocket.disconnect": | ||||||
|             pass |             pass | ||||||
|  |  | ||||||
|   | |||||||
| @@ -52,6 +52,7 @@ class WebsocketFrameAssembler: | |||||||
|         paused: bool |         paused: bool | ||||||
|  |  | ||||||
|     def __init__(self, protocol) -> None: |     def __init__(self, protocol) -> None: | ||||||
|  |  | ||||||
|         self.protocol = protocol |         self.protocol = protocol | ||||||
|  |  | ||||||
|         self.read_mutex = asyncio.Lock() |         self.read_mutex = asyncio.Lock() | ||||||
|   | |||||||
| @@ -12,37 +12,21 @@ from typing import ( | |||||||
|     Union, |     Union, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| from websockets.exceptions import ( | from websockets.connection import CLOSED, CLOSING, OPEN, Event | ||||||
|     ConnectionClosed, | from websockets.exceptions import ConnectionClosed, ConnectionClosedError | ||||||
|     ConnectionClosedError, |  | ||||||
|     ConnectionClosedOK, |  | ||||||
| ) |  | ||||||
| from websockets.frames import Frame, Opcode | from websockets.frames import Frame, Opcode | ||||||
|  | from websockets.server import ServerConnection | ||||||
|  |  | ||||||
| try:  # websockets < 11.0 |  | ||||||
|     from websockets.connection import Event, State |  | ||||||
|     from websockets.server import ServerConnection as ServerProtocol |  | ||||||
| except ImportError:  # websockets >= 11.0 |  | ||||||
|     from websockets.protocol import Event, State  # type: ignore |  | ||||||
|     from websockets.server import ServerProtocol  # type: ignore |  | ||||||
|  |  | ||||||
| from websockets.typing import Data | from websockets.typing import Data | ||||||
|  |  | ||||||
| from sanic.log import deprecation, error_logger, logger | from sanic.log import error_logger, logger | ||||||
| from sanic.server.protocols.base_protocol import SanicProtocol | from sanic.server.protocols.base_protocol import SanicProtocol | ||||||
|  |  | ||||||
| from ...exceptions import ServerError, WebsocketClosed | from ...exceptions import ServerError, WebsocketClosed | ||||||
| from .frame import WebsocketFrameAssembler | from .frame import WebsocketFrameAssembler | ||||||
|  |  | ||||||
|  |  | ||||||
| OPEN = State.OPEN |  | ||||||
| CLOSING = State.CLOSING |  | ||||||
| CLOSED = State.CLOSED |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class WebsocketImplProtocol: | class WebsocketImplProtocol: | ||||||
|     ws_proto: ServerProtocol |     connection: ServerConnection | ||||||
|     io_proto: Optional[SanicProtocol] |     io_proto: Optional[SanicProtocol] | ||||||
|     loop: Optional[asyncio.AbstractEventLoop] |     loop: Optional[asyncio.AbstractEventLoop] | ||||||
|     max_queue: int |     max_queue: int | ||||||
| @@ -68,14 +52,14 @@ class WebsocketImplProtocol: | |||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         ws_proto, |         connection, | ||||||
|         max_queue=None, |         max_queue=None, | ||||||
|         ping_interval: Optional[float] = 20, |         ping_interval: Optional[float] = 20, | ||||||
|         ping_timeout: Optional[float] = 20, |         ping_timeout: Optional[float] = 20, | ||||||
|         close_timeout: float = 10, |         close_timeout: float = 10, | ||||||
|         loop=None, |         loop=None, | ||||||
|     ): |     ): | ||||||
|         self.ws_proto = ws_proto |         self.connection = connection | ||||||
|         self.io_proto = None |         self.io_proto = None | ||||||
|         self.loop = None |         self.loop = None | ||||||
|         self.max_queue = max_queue |         self.max_queue = max_queue | ||||||
| @@ -97,16 +81,7 @@ class WebsocketImplProtocol: | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def subprotocol(self): |     def subprotocol(self): | ||||||
|         return self.ws_proto.subprotocol |         return self.connection.subprotocol | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def connection(self): |  | ||||||
|         deprecation( |  | ||||||
|             "The connection property has been deprecated and will be removed. " |  | ||||||
|             "Please use the ws_proto property instead going forward.", |  | ||||||
|             22.6, |  | ||||||
|         ) |  | ||||||
|         return self.ws_proto |  | ||||||
|  |  | ||||||
|     def pause_frames(self): |     def pause_frames(self): | ||||||
|         if not self.can_pause: |         if not self.can_pause: | ||||||
| @@ -320,15 +295,15 @@ class WebsocketImplProtocol: | |||||||
|             # Not draining the write buffer is acceptable in this context. |             # Not draining the write buffer is acceptable in this context. | ||||||
|  |  | ||||||
|             # clear the send buffer |             # clear the send buffer | ||||||
|             _ = self.ws_proto.data_to_send() |             _ = self.connection.data_to_send() | ||||||
|             # If we're not already CLOSED or CLOSING, then send the close. |             # If we're not already CLOSED or CLOSING, then send the close. | ||||||
|             if self.ws_proto.state is OPEN: |             if self.connection.state is OPEN: | ||||||
|                 if code in (1000, 1001): |                 if code in (1000, 1001): | ||||||
|                     self.ws_proto.send_close(code, reason) |                     self.connection.send_close(code, reason) | ||||||
|                 else: |                 else: | ||||||
|                     self.ws_proto.fail(code, reason) |                     self.connection.fail(code, reason) | ||||||
|                 try: |                 try: | ||||||
|                     data_to_send = self.ws_proto.data_to_send() |                     data_to_send = self.connection.data_to_send() | ||||||
|                     while ( |                     while ( | ||||||
|                         len(data_to_send) |                         len(data_to_send) | ||||||
|                         and self.io_proto |                         and self.io_proto | ||||||
| @@ -342,7 +317,7 @@ class WebsocketImplProtocol: | |||||||
|                     ... |                     ... | ||||||
|         if code == 1006: |         if code == 1006: | ||||||
|             # Special case: 1006 consider the transport already closed |             # Special case: 1006 consider the transport already closed | ||||||
|             self.ws_proto.state = CLOSED |             self.connection.state = CLOSED | ||||||
|         if self.data_finished_fut and not self.data_finished_fut.done(): |         if self.data_finished_fut and not self.data_finished_fut.done(): | ||||||
|             # We have a graceful auto-closer. Use it to close the connection. |             # We have a graceful auto-closer. Use it to close the connection. | ||||||
|             self.data_finished_fut.cancel() |             self.data_finished_fut.cancel() | ||||||
| @@ -363,10 +338,10 @@ class WebsocketImplProtocol: | |||||||
|         # In Python Version 3.7: pause_reading is idempotent |         # In Python Version 3.7: pause_reading is idempotent | ||||||
|         # i.e. it can be called when the transport is already paused or closed. |         # i.e. it can be called when the transport is already paused or closed. | ||||||
|         self.io_proto.transport.pause_reading() |         self.io_proto.transport.pause_reading() | ||||||
|         if self.ws_proto.state == OPEN: |         if self.connection.state == OPEN: | ||||||
|             data_to_send = self.ws_proto.data_to_send() |             data_to_send = self.connection.data_to_send() | ||||||
|             self.ws_proto.send_close(code, reason) |             self.connection.send_close(code, reason) | ||||||
|             data_to_send.extend(self.ws_proto.data_to_send()) |             data_to_send.extend(self.connection.data_to_send()) | ||||||
|             try: |             try: | ||||||
|                 while ( |                 while ( | ||||||
|                     len(data_to_send) |                     len(data_to_send) | ||||||
| @@ -475,7 +450,7 @@ class WebsocketImplProtocol: | |||||||
|         Raise ConnectionClosed in pending keepalive pings. |         Raise ConnectionClosed in pending keepalive pings. | ||||||
|         They'll never receive a pong once the connection is closed. |         They'll never receive a pong once the connection is closed. | ||||||
|         """ |         """ | ||||||
|         if self.ws_proto.state is not CLOSED: |         if self.connection.state is not CLOSED: | ||||||
|             raise ServerError( |             raise ServerError( | ||||||
|                 "Webscoket about_pings should only be called " |                 "Webscoket about_pings should only be called " | ||||||
|                 "after connection state is changed to CLOSED" |                 "after connection state is changed to CLOSED" | ||||||
| @@ -504,9 +479,9 @@ class WebsocketImplProtocol: | |||||||
|             self.fail_connection(code, reason) |             self.fail_connection(code, reason) | ||||||
|             return |             return | ||||||
|         async with self.conn_mutex: |         async with self.conn_mutex: | ||||||
|             if self.ws_proto.state is OPEN: |             if self.connection.state is OPEN: | ||||||
|                 self.ws_proto.send_close(code, reason) |                 self.connection.send_close(code, reason) | ||||||
|                 data_to_send = self.ws_proto.data_to_send() |                 data_to_send = self.connection.data_to_send() | ||||||
|                 await self.send_data(data_to_send) |                 await self.send_data(data_to_send) | ||||||
|  |  | ||||||
|     async def recv(self, timeout: Optional[float] = None) -> Optional[Data]: |     async def recv(self, timeout: Optional[float] = None) -> Optional[Data]: | ||||||
| @@ -536,7 +511,7 @@ class WebsocketImplProtocol: | |||||||
|                 "already waiting for the next message" |                 "already waiting for the next message" | ||||||
|             ) |             ) | ||||||
|         await self.recv_lock.acquire() |         await self.recv_lock.acquire() | ||||||
|         if self.ws_proto.state is CLOSED: |         if self.connection.state is CLOSED: | ||||||
|             self.recv_lock.release() |             self.recv_lock.release() | ||||||
|             raise WebsocketClosed( |             raise WebsocketClosed( | ||||||
|                 "Cannot receive from websocket interface after it is closed." |                 "Cannot receive from websocket interface after it is closed." | ||||||
| @@ -587,7 +562,7 @@ class WebsocketImplProtocol: | |||||||
|                 "for the next message" |                 "for the next message" | ||||||
|             ) |             ) | ||||||
|         await self.recv_lock.acquire() |         await self.recv_lock.acquire() | ||||||
|         if self.ws_proto.state is CLOSED: |         if self.connection.state is CLOSED: | ||||||
|             self.recv_lock.release() |             self.recv_lock.release() | ||||||
|             raise WebsocketClosed( |             raise WebsocketClosed( | ||||||
|                 "Cannot receive from websocket interface after it is closed." |                 "Cannot receive from websocket interface after it is closed." | ||||||
| @@ -646,7 +621,7 @@ class WebsocketImplProtocol: | |||||||
|                 "is already waiting for the next message" |                 "is already waiting for the next message" | ||||||
|             ) |             ) | ||||||
|         await self.recv_lock.acquire() |         await self.recv_lock.acquire() | ||||||
|         if self.ws_proto.state is CLOSED: |         if self.connection.state is CLOSED: | ||||||
|             self.recv_lock.release() |             self.recv_lock.release() | ||||||
|             raise WebsocketClosed( |             raise WebsocketClosed( | ||||||
|                 "Cannot receive from websocket interface after it is closed." |                 "Cannot receive from websocket interface after it is closed." | ||||||
| @@ -686,7 +661,8 @@ class WebsocketImplProtocol: | |||||||
|         :raises TypeError: for unsupported inputs |         :raises TypeError: for unsupported inputs | ||||||
|         """ |         """ | ||||||
|         async with self.conn_mutex: |         async with self.conn_mutex: | ||||||
|             if self.ws_proto.state in (CLOSED, CLOSING): |  | ||||||
|  |             if self.connection.state in (CLOSED, CLOSING): | ||||||
|                 raise WebsocketClosed( |                 raise WebsocketClosed( | ||||||
|                     "Cannot write to websocket interface after it is closed." |                     "Cannot write to websocket interface after it is closed." | ||||||
|                 ) |                 ) | ||||||
| @@ -699,12 +675,12 @@ class WebsocketImplProtocol: | |||||||
|             # strings and bytes-like objects are iterable. |             # strings and bytes-like objects are iterable. | ||||||
|  |  | ||||||
|             if isinstance(message, str): |             if isinstance(message, str): | ||||||
|                 self.ws_proto.send_text(message.encode("utf-8")) |                 self.connection.send_text(message.encode("utf-8")) | ||||||
|                 await self.send_data(self.ws_proto.data_to_send()) |                 await self.send_data(self.connection.data_to_send()) | ||||||
|  |  | ||||||
|             elif isinstance(message, (bytes, bytearray, memoryview)): |             elif isinstance(message, (bytes, bytearray, memoryview)): | ||||||
|                 self.ws_proto.send_binary(message) |                 self.connection.send_binary(message) | ||||||
|                 await self.send_data(self.ws_proto.data_to_send()) |                 await self.send_data(self.connection.data_to_send()) | ||||||
|  |  | ||||||
|             elif isinstance(message, Mapping): |             elif isinstance(message, Mapping): | ||||||
|                 # Catch a common mistake -- passing a dict to send(). |                 # Catch a common mistake -- passing a dict to send(). | ||||||
| @@ -733,7 +709,7 @@ class WebsocketImplProtocol: | |||||||
|         (which will be encoded to UTF-8) or a bytes-like object. |         (which will be encoded to UTF-8) or a bytes-like object. | ||||||
|         """ |         """ | ||||||
|         async with self.conn_mutex: |         async with self.conn_mutex: | ||||||
|             if self.ws_proto.state in (CLOSED, CLOSING): |             if self.connection.state in (CLOSED, CLOSING): | ||||||
|                 raise WebsocketClosed( |                 raise WebsocketClosed( | ||||||
|                     "Cannot send a ping when the websocket interface " |                     "Cannot send a ping when the websocket interface " | ||||||
|                     "is closed." |                     "is closed." | ||||||
| @@ -761,8 +737,8 @@ class WebsocketImplProtocol: | |||||||
|  |  | ||||||
|             self.pings[data] = self.io_proto.loop.create_future() |             self.pings[data] = self.io_proto.loop.create_future() | ||||||
|  |  | ||||||
|             self.ws_proto.send_ping(data) |             self.connection.send_ping(data) | ||||||
|             await self.send_data(self.ws_proto.data_to_send()) |             await self.send_data(self.connection.data_to_send()) | ||||||
|  |  | ||||||
|             return asyncio.shield(self.pings[data]) |             return asyncio.shield(self.pings[data]) | ||||||
|  |  | ||||||
| @@ -774,15 +750,15 @@ class WebsocketImplProtocol: | |||||||
|         be a string (which will be encoded to UTF-8) or a bytes-like object. |         be a string (which will be encoded to UTF-8) or a bytes-like object. | ||||||
|         """ |         """ | ||||||
|         async with self.conn_mutex: |         async with self.conn_mutex: | ||||||
|             if self.ws_proto.state in (CLOSED, CLOSING): |             if self.connection.state in (CLOSED, CLOSING): | ||||||
|                 # Cannot send pong after transport is shutting down |                 # Cannot send pong after transport is shutting down | ||||||
|                 return |                 return | ||||||
|             if isinstance(data, str): |             if isinstance(data, str): | ||||||
|                 data = data.encode("utf-8") |                 data = data.encode("utf-8") | ||||||
|             elif isinstance(data, (bytearray, memoryview)): |             elif isinstance(data, (bytearray, memoryview)): | ||||||
|                 data = bytes(data) |                 data = bytes(data) | ||||||
|             self.ws_proto.send_pong(data) |             self.connection.send_pong(data) | ||||||
|             await self.send_data(self.ws_proto.data_to_send()) |             await self.send_data(self.connection.data_to_send()) | ||||||
|  |  | ||||||
|     async def send_data(self, data_to_send): |     async def send_data(self, data_to_send): | ||||||
|         for data in data_to_send: |         for data in data_to_send: | ||||||
| @@ -804,7 +780,7 @@ class WebsocketImplProtocol: | |||||||
|                     SanicProtocol.close(self.io_proto, timeout=1.0) |                     SanicProtocol.close(self.io_proto, timeout=1.0) | ||||||
|  |  | ||||||
|     async def async_data_received(self, data_to_send, events_to_process): |     async def async_data_received(self, data_to_send, events_to_process): | ||||||
|         if self.ws_proto.state in (OPEN, CLOSING) and len(data_to_send) > 0: |         if self.connection.state in (OPEN, CLOSING) and len(data_to_send) > 0: | ||||||
|             # receiving data can generate data to send (eg, pong for a ping) |             # receiving data can generate data to send (eg, pong for a ping) | ||||||
|             # send connection.data_to_send() |             # send connection.data_to_send() | ||||||
|             await self.send_data(data_to_send) |             await self.send_data(data_to_send) | ||||||
| @@ -812,9 +788,9 @@ class WebsocketImplProtocol: | |||||||
|             await self.process_events(events_to_process) |             await self.process_events(events_to_process) | ||||||
|  |  | ||||||
|     def data_received(self, data): |     def data_received(self, data): | ||||||
|         self.ws_proto.receive_data(data) |         self.connection.receive_data(data) | ||||||
|         data_to_send = self.ws_proto.data_to_send() |         data_to_send = self.connection.data_to_send() | ||||||
|         events_to_process = self.ws_proto.events_received() |         events_to_process = self.connection.events_received() | ||||||
|         if len(data_to_send) > 0 or len(events_to_process) > 0: |         if len(data_to_send) > 0 or len(events_to_process) > 0: | ||||||
|             asyncio.create_task( |             asyncio.create_task( | ||||||
|                 self.async_data_received(data_to_send, events_to_process) |                 self.async_data_received(data_to_send, events_to_process) | ||||||
| @@ -823,7 +799,7 @@ class WebsocketImplProtocol: | |||||||
|     async def async_eof_received(self, data_to_send, events_to_process): |     async def async_eof_received(self, data_to_send, events_to_process): | ||||||
|         # receiving EOF can generate data to send |         # receiving EOF can generate data to send | ||||||
|         # send connection.data_to_send() |         # send connection.data_to_send() | ||||||
|         if self.ws_proto.state in (OPEN, CLOSING) and len(data_to_send) > 0: |         if self.connection.state in (OPEN, CLOSING) and len(data_to_send) > 0: | ||||||
|             await self.send_data(data_to_send) |             await self.send_data(data_to_send) | ||||||
|         if len(events_to_process) > 0: |         if len(events_to_process) > 0: | ||||||
|             await self.process_events(events_to_process) |             await self.process_events(events_to_process) | ||||||
| @@ -843,9 +819,9 @@ class WebsocketImplProtocol: | |||||||
|             SanicProtocol.close(self.io_proto, timeout=1.0) |             SanicProtocol.close(self.io_proto, timeout=1.0) | ||||||
|  |  | ||||||
|     def eof_received(self) -> Optional[bool]: |     def eof_received(self) -> Optional[bool]: | ||||||
|         self.ws_proto.receive_eof() |         self.connection.receive_eof() | ||||||
|         data_to_send = self.ws_proto.data_to_send() |         data_to_send = self.connection.data_to_send() | ||||||
|         events_to_process = self.ws_proto.events_received() |         events_to_process = self.connection.events_received() | ||||||
|         asyncio.create_task( |         asyncio.create_task( | ||||||
|             self.async_eof_received(data_to_send, events_to_process) |             self.async_eof_received(data_to_send, events_to_process) | ||||||
|         ) |         ) | ||||||
| @@ -855,19 +831,12 @@ class WebsocketImplProtocol: | |||||||
|         """ |         """ | ||||||
|         The WebSocket Connection is Closed. |         The WebSocket Connection is Closed. | ||||||
|         """ |         """ | ||||||
|         if not self.ws_proto.state == CLOSED: |         if not self.connection.state == CLOSED: | ||||||
|             # signal to the websocket connection handler |             # signal to the websocket connection handler | ||||||
|             # we've lost the connection |             # we've lost the connection | ||||||
|             self.ws_proto.fail(code=1006) |             self.connection.fail(code=1006) | ||||||
|             self.ws_proto.state = CLOSED |             self.connection.state = CLOSED | ||||||
|  |  | ||||||
|         self.abort_pings() |         self.abort_pings() | ||||||
|         if self.connection_lost_waiter: |         if self.connection_lost_waiter: | ||||||
|             self.connection_lost_waiter.set_result(None) |             self.connection_lost_waiter.set_result(None) | ||||||
|  |  | ||||||
|     async def __aiter__(self): |  | ||||||
|         try: |  | ||||||
|             while True: |  | ||||||
|                 yield await self.recv() |  | ||||||
|         except ConnectionClosedOK: |  | ||||||
|             return |  | ||||||
|   | |||||||
| @@ -154,7 +154,9 @@ class SignalRouter(BaseRouter): | |||||||
|         try: |         try: | ||||||
|             for signal in signals: |             for signal in signals: | ||||||
|                 params.pop("__trigger__", None) |                 params.pop("__trigger__", None) | ||||||
|                 requirements = signal.extra.requirements |                 requirements = getattr( | ||||||
|  |                     signal.handler, "__requirements__", None | ||||||
|  |                 ) | ||||||
|                 if ( |                 if ( | ||||||
|                     (condition is None and signal.ctx.exclusive is False) |                     (condition is None and signal.ctx.exclusive is False) | ||||||
|                     or (condition is None and not requirements) |                     or (condition is None and not requirements) | ||||||
| @@ -217,13 +219,8 @@ class SignalRouter(BaseRouter): | |||||||
|         if not trigger: |         if not trigger: | ||||||
|             event = ".".join([*parts[:2], "<__trigger__>"]) |             event = ".".join([*parts[:2], "<__trigger__>"]) | ||||||
|  |  | ||||||
|         try: |  | ||||||
|             # Attaching __requirements__ and __trigger__ to the handler |  | ||||||
|             # is deprecated and will be removed in v23.6. |  | ||||||
|         handler.__requirements__ = condition  # type: ignore |         handler.__requirements__ = condition  # type: ignore | ||||||
|         handler.__trigger__ = trigger  # type: ignore |         handler.__trigger__ = trigger  # type: ignore | ||||||
|         except AttributeError: |  | ||||||
|             pass |  | ||||||
|  |  | ||||||
|         signal = super().add( |         signal = super().add( | ||||||
|             event, |             event, | ||||||
| @@ -235,7 +232,6 @@ class SignalRouter(BaseRouter): | |||||||
|         signal.ctx.exclusive = exclusive |         signal.ctx.exclusive = exclusive | ||||||
|         signal.ctx.trigger = trigger |         signal.ctx.trigger = trigger | ||||||
|         signal.ctx.definition = event_definition |         signal.ctx.definition = event_definition | ||||||
|         signal.extra.requirements = condition |  | ||||||
|  |  | ||||||
|         return cast(Signal, signal) |         return cast(Signal, signal) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ from pathlib import Path | |||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.exceptions import SanicException | from sanic.exceptions import SanicException | ||||||
|  | from sanic.response import redirect | ||||||
|  |  | ||||||
|  |  | ||||||
| def create_simple_server(directory: Path): | def create_simple_server(directory: Path): | ||||||
| @@ -11,8 +12,10 @@ def create_simple_server(directory: Path): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     app = Sanic("SimpleServer") |     app = Sanic("SimpleServer") | ||||||
|     app.static( |     app.static("/", directory, name="main") | ||||||
|         "/", directory, name="main", directory_view=True, index="index.html" |  | ||||||
|     ) |     @app.get("/") | ||||||
|  |     def index(_): | ||||||
|  |         return redirect(app.url_for("main", filename="index.html")) | ||||||
|  |  | ||||||
|     return app |     return app | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ class TouchUpMeta(SanicMeta): | |||||||
|         methods = attrs.get("__touchup__") |         methods = attrs.get("__touchup__") | ||||||
|         attrs["__touched__"] = False |         attrs["__touched__"] = False | ||||||
|         if methods: |         if methods: | ||||||
|  |  | ||||||
|             for method in methods: |             for method in methods: | ||||||
|                 if method not in attrs: |                 if method not in attrs: | ||||||
|                     raise SanicException( |                     raise SanicException( | ||||||
|   | |||||||
| @@ -44,9 +44,7 @@ class SharedContext(SimpleNamespace): | |||||||
|                 f"{Colors.YELLOW}with type {Colors.PURPLE}{type(value)} " |                 f"{Colors.YELLOW}with type {Colors.PURPLE}{type(value)} " | ||||||
|                 f"{Colors.YELLOW}was added to shared_ctx. It may not " |                 f"{Colors.YELLOW}was added to shared_ctx. It may not " | ||||||
|                 "not function as intended. Consider using the regular " |                 "not function as intended. Consider using the regular " | ||||||
|                 f"ctx.\nFor more information, please see https://sanic.dev/en" |                 f"ctx. For more information, please see ____.{Colors.END}" | ||||||
|                 "/guide/deployment/manager.html#using-shared-context-between-" |  | ||||||
|                 f"worker-processes.{Colors.END}" |  | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|   | |||||||
| @@ -75,6 +75,7 @@ def load_module_from_file_location( | |||||||
|         location = location.decode(encoding) |         location = location.decode(encoding) | ||||||
|  |  | ||||||
|     if isinstance(location, Path) or "/" in location or "$" in location: |     if isinstance(location, Path) or "/" in location or "$" in location: | ||||||
|  |  | ||||||
|         if not isinstance(location, Path): |         if not isinstance(location, Path): | ||||||
|             # A) Check if location contains any environment variables |             # A) Check if location contains any environment variables | ||||||
|             #    in format ${some_env_var}. |             #    in format ${some_env_var}. | ||||||
|   | |||||||
| @@ -1,18 +0,0 @@ | |||||||
| from enum import IntEnum, auto |  | ||||||
|  |  | ||||||
| from sanic.compat import UpperStrEnum |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class RestartOrder(UpperStrEnum): |  | ||||||
|     SHUTDOWN_FIRST = auto() |  | ||||||
|     STARTUP_FIRST = auto() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ProcessState(IntEnum): |  | ||||||
|     IDLE = auto() |  | ||||||
|     RESTARTING = auto() |  | ||||||
|     STARTING = auto() |  | ||||||
|     STARTED = auto() |  | ||||||
|     ACKED = auto() |  | ||||||
|     JOINED = auto() |  | ||||||
|     TERMINATED = auto() |  | ||||||
| @@ -1,17 +1,23 @@ | |||||||
| from __future__ import annotations | import sys | ||||||
|  |  | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from inspect import isawaitable |  | ||||||
| from multiprocessing.connection import Connection | from multiprocessing.connection import Connection | ||||||
| from os import environ | from signal import SIGINT, SIGTERM | ||||||
| from pathlib import Path | from signal import signal as signal_func | ||||||
| from typing import Any, Dict, Mapping, Union | from socket import AF_INET, SOCK_STREAM, socket, timeout | ||||||
|  | from textwrap import indent | ||||||
|  | from typing import Any, Dict | ||||||
|  |  | ||||||
| from sanic.exceptions import Unauthorized | from sanic.application.logo import get_logo | ||||||
| from sanic.helpers import Default | from sanic.application.motd import MOTDTTY | ||||||
| from sanic.log import logger | from sanic.log import Colors, error_logger, logger | ||||||
| from sanic.request import Request | from sanic.server.socket import configure_socket | ||||||
| from sanic.response import json |  | ||||||
|  |  | ||||||
|  | try:  # no cov | ||||||
|  |     from ujson import dumps, loads | ||||||
|  | except ModuleNotFoundError:  # no cov | ||||||
|  |     from json import dumps, loads  # type: ignore | ||||||
|  |  | ||||||
|  |  | ||||||
| class Inspector: | class Inspector: | ||||||
| @@ -19,105 +25,117 @@ class Inspector: | |||||||
|         self, |         self, | ||||||
|         publisher: Connection, |         publisher: Connection, | ||||||
|         app_info: Dict[str, Any], |         app_info: Dict[str, Any], | ||||||
|         worker_state: Mapping[str, Any], |         worker_state: Dict[str, Any], | ||||||
|         host: str, |         host: str, | ||||||
|         port: int, |         port: int, | ||||||
|         api_key: str, |  | ||||||
|         tls_key: Union[Path, str, Default], |  | ||||||
|         tls_cert: Union[Path, str, Default], |  | ||||||
|     ): |     ): | ||||||
|         self._publisher = publisher |         self._publisher = publisher | ||||||
|  |         self.run = True | ||||||
|         self.app_info = app_info |         self.app_info = app_info | ||||||
|         self.worker_state = worker_state |         self.worker_state = worker_state | ||||||
|         self.host = host |         self.host = host | ||||||
|         self.port = port |         self.port = port | ||||||
|         self.api_key = api_key |  | ||||||
|         self.tls_key = tls_key |  | ||||||
|         self.tls_cert = tls_cert |  | ||||||
|  |  | ||||||
|     def __call__(self, run=True, **_) -> Inspector: |     def __call__(self) -> None: | ||||||
|         from sanic import Sanic |         sock = configure_socket( | ||||||
|  |             {"host": self.host, "port": self.port, "unix": None, "backlog": 1} | ||||||
|         self.app = Sanic("Inspector") |  | ||||||
|         self._setup() |  | ||||||
|         if run: |  | ||||||
|             self.app.run( |  | ||||||
|                 host=self.host, |  | ||||||
|                 port=self.port, |  | ||||||
|                 single_process=True, |  | ||||||
|                 ssl={"key": self.tls_key, "cert": self.tls_cert} |  | ||||||
|                 if not isinstance(self.tls_key, Default) |  | ||||||
|                 and not isinstance(self.tls_cert, Default) |  | ||||||
|                 else None, |  | ||||||
|         ) |         ) | ||||||
|         return self |         assert sock | ||||||
|  |         signal_func(SIGINT, self.stop) | ||||||
|  |         signal_func(SIGTERM, self.stop) | ||||||
|  |  | ||||||
|     def _setup(self): |         logger.info(f"Inspector started on: {sock.getsockname()}") | ||||||
|         self.app.get("/")(self._info) |         sock.settimeout(0.5) | ||||||
|         self.app.post("/<action:str>")(self._action) |         try: | ||||||
|         if self.api_key: |             while self.run: | ||||||
|             self.app.on_request(self._authentication) |                 try: | ||||||
|         environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "true" |                     conn, _ = sock.accept() | ||||||
|  |                 except timeout: | ||||||
|  |                     continue | ||||||
|  |                 else: | ||||||
|  |                     action = conn.recv(64) | ||||||
|  |                     if action == b"reload": | ||||||
|  |                         conn.send(b"\n") | ||||||
|  |                         self.reload() | ||||||
|  |                     elif action == b"shutdown": | ||||||
|  |                         conn.send(b"\n") | ||||||
|  |                         self.shutdown() | ||||||
|  |                     else: | ||||||
|  |                         data = dumps(self.state_to_json()) | ||||||
|  |                         conn.send(data.encode()) | ||||||
|  |                         conn.close() | ||||||
|  |         finally: | ||||||
|  |             logger.debug("Inspector closing") | ||||||
|  |             sock.close() | ||||||
|  |  | ||||||
|     def _authentication(self, request: Request) -> None: |     def stop(self, *_): | ||||||
|         if request.token != self.api_key: |         self.run = False | ||||||
|             raise Unauthorized("Bad API key") |  | ||||||
|  |  | ||||||
|     async def _action(self, request: Request, action: str): |     def state_to_json(self): | ||||||
|         logger.info("Incoming inspector action: %s", action) |  | ||||||
|         output: Any = None |  | ||||||
|         method = getattr(self, action, None) |  | ||||||
|         if method: |  | ||||||
|             kwargs = {} |  | ||||||
|             if request.body: |  | ||||||
|                 kwargs = request.json |  | ||||||
|             args = kwargs.pop("args", ()) |  | ||||||
|             output = method(*args, **kwargs) |  | ||||||
|             if isawaitable(output): |  | ||||||
|                 output = await output |  | ||||||
|  |  | ||||||
|         return await self._respond(request, output) |  | ||||||
|  |  | ||||||
|     async def _info(self, request: Request): |  | ||||||
|         return await self._respond(request, self._state_to_json()) |  | ||||||
|  |  | ||||||
|     async def _respond(self, request: Request, output: Any): |  | ||||||
|         name = request.match_info.get("action", "info") |  | ||||||
|         return json( |  | ||||||
|             {"meta": {"action": name}, "result": output}, |  | ||||||
|             escape_forward_slashes=False, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def _state_to_json(self) -> Dict[str, Any]: |  | ||||||
|         output = {"info": self.app_info} |         output = {"info": self.app_info} | ||||||
|         output["workers"] = self._make_safe(dict(self.worker_state)) |         output["workers"] = self._make_safe(dict(self.worker_state)) | ||||||
|         return output |         return output | ||||||
|  |  | ||||||
|     @staticmethod |     def reload(self): | ||||||
|     def _make_safe(obj: Dict[str, Any]) -> Dict[str, Any]: |         message = "__ALL_PROCESSES__:" | ||||||
|  |         self._publisher.send(message) | ||||||
|  |  | ||||||
|  |     def shutdown(self): | ||||||
|  |         message = "__TERMINATE__" | ||||||
|  |         self._publisher.send(message) | ||||||
|  |  | ||||||
|  |     def _make_safe(self, obj: Dict[str, Any]) -> Dict[str, Any]: | ||||||
|         for key, value in obj.items(): |         for key, value in obj.items(): | ||||||
|             if isinstance(value, dict): |             if isinstance(value, dict): | ||||||
|                 obj[key] = Inspector._make_safe(value) |                 obj[key] = self._make_safe(value) | ||||||
|             elif isinstance(value, datetime): |             elif isinstance(value, datetime): | ||||||
|                 obj[key] = value.isoformat() |                 obj[key] = value.isoformat() | ||||||
|         return obj |         return obj | ||||||
|  |  | ||||||
|     def reload(self, zero_downtime: bool = False) -> None: |  | ||||||
|         message = "__ALL_PROCESSES__:" |  | ||||||
|         if zero_downtime: |  | ||||||
|             message += ":STARTUP_FIRST" |  | ||||||
|         self._publisher.send(message) |  | ||||||
|  |  | ||||||
|     def scale(self, replicas) -> str: | def inspect(host: str, port: int, action: str): | ||||||
|         num_workers = 1 |     out = sys.stdout.write | ||||||
|         if replicas: |     with socket(AF_INET, SOCK_STREAM) as sock: | ||||||
|             num_workers = int(replicas) |         try: | ||||||
|         log_msg = f"Scaling to {num_workers}" |             sock.connect((host, port)) | ||||||
|         logger.info(log_msg) |         except ConnectionRefusedError: | ||||||
|         message = f"__SCALE__:{num_workers}" |             error_logger.error( | ||||||
|         self._publisher.send(message) |                 f"{Colors.RED}Could not connect to inspector at: " | ||||||
|         return log_msg |                 f"{Colors.YELLOW}{(host, port)}{Colors.END}\n" | ||||||
|  |                 "Either the application is not running, or it did not start " | ||||||
|     def shutdown(self) -> None: |                 "an inspector instance." | ||||||
|         message = "__TERMINATE__" |             ) | ||||||
|         self._publisher.send(message) |             sock.close() | ||||||
|  |             sys.exit(1) | ||||||
|  |         sock.sendall(action.encode()) | ||||||
|  |         data = sock.recv(4096) | ||||||
|  |     if action == "raw": | ||||||
|  |         out(data.decode()) | ||||||
|  |     elif action == "pretty": | ||||||
|  |         loaded = loads(data) | ||||||
|  |         display = loaded.pop("info") | ||||||
|  |         extra = display.pop("extra", {}) | ||||||
|  |         display["packages"] = ", ".join(display["packages"]) | ||||||
|  |         MOTDTTY(get_logo(), f"{host}:{port}", display, extra).display( | ||||||
|  |             version=False, | ||||||
|  |             action="Inspecting", | ||||||
|  |             out=out, | ||||||
|  |         ) | ||||||
|  |         for name, info in loaded["workers"].items(): | ||||||
|  |             info = "\n".join( | ||||||
|  |                 f"\t{key}: {Colors.BLUE}{value}{Colors.END}" | ||||||
|  |                 for key, value in info.items() | ||||||
|  |             ) | ||||||
|  |             out( | ||||||
|  |                 "\n" | ||||||
|  |                 + indent( | ||||||
|  |                     "\n".join( | ||||||
|  |                         [ | ||||||
|  |                             f"{Colors.BOLD}{Colors.SANIC}{name}{Colors.END}", | ||||||
|  |                             info, | ||||||
|  |                         ] | ||||||
|  |                     ), | ||||||
|  |                     "  ", | ||||||
|  |                 ) | ||||||
|  |                 + "\n" | ||||||
|  |             ) | ||||||
|   | |||||||
| @@ -5,10 +5,18 @@ import sys | |||||||
|  |  | ||||||
| from importlib import import_module | from importlib import import_module | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union, cast | from typing import ( | ||||||
|  |     TYPE_CHECKING, | ||||||
|  |     Any, | ||||||
|  |     Callable, | ||||||
|  |     Dict, | ||||||
|  |     Optional, | ||||||
|  |     Type, | ||||||
|  |     Union, | ||||||
|  |     cast, | ||||||
|  | ) | ||||||
|  |  | ||||||
| from sanic.http.tls.context import process_to_context | from sanic.http.tls.creators import CertCreator, MkcertCreator, TrustmeCreator | ||||||
| from sanic.http.tls.creators import MkcertCreator, TrustmeCreator |  | ||||||
|  |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
| @@ -98,30 +106,21 @@ class AppLoader: | |||||||
|  |  | ||||||
|  |  | ||||||
| class CertLoader: | class CertLoader: | ||||||
|     _creators = { |     _creator_class: Type[CertCreator] | ||||||
|         "mkcert": MkcertCreator, |  | ||||||
|         "trustme": TrustmeCreator, |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     def __init__(self, ssl_data: Dict[str, Union[str, os.PathLike]]): |     def __init__(self, ssl_data: Dict[str, Union[str, os.PathLike]]): | ||||||
|         self._ssl_data = ssl_data |         creator_name = ssl_data.get("creator") | ||||||
|  |         if creator_name not in ("mkcert", "trustme"): | ||||||
|         creator_name = cast(str, ssl_data.get("creator")) |  | ||||||
|  |  | ||||||
|         self._creator_class = self._creators.get(creator_name) |  | ||||||
|         if not creator_name: |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         if not self._creator_class: |  | ||||||
|             raise RuntimeError(f"Unknown certificate creator: {creator_name}") |             raise RuntimeError(f"Unknown certificate creator: {creator_name}") | ||||||
|  |         elif creator_name == "mkcert": | ||||||
|  |             self._creator_class = MkcertCreator | ||||||
|  |         elif creator_name == "trustme": | ||||||
|  |             self._creator_class = TrustmeCreator | ||||||
|  |  | ||||||
|         self._key = ssl_data["key"] |         self._key = ssl_data["key"] | ||||||
|         self._cert = ssl_data["cert"] |         self._cert = ssl_data["cert"] | ||||||
|         self._localhost = cast(str, ssl_data["localhost"]) |         self._localhost = cast(str, ssl_data["localhost"]) | ||||||
|  |  | ||||||
|     def load(self, app: SanicApp): |     def load(self, app: SanicApp): | ||||||
|         if not self._creator_class: |  | ||||||
|             return process_to_context(self._ssl_data) |  | ||||||
|  |  | ||||||
|         creator = self._creator_class(app, self._key, self._cert) |         creator = self._creator_class(app, self._key, self._cert) | ||||||
|         return creator.generate_cert(self._localhost) |         return creator.generate_cert(self._localhost) | ||||||
|   | |||||||
| @@ -1,16 +1,13 @@ | |||||||
| import os | import os | ||||||
|  | import sys | ||||||
|  |  | ||||||
| from contextlib import suppress |  | ||||||
| from itertools import count |  | ||||||
| from random import choice |  | ||||||
| from signal import SIGINT, SIGTERM, Signals | from signal import SIGINT, SIGTERM, Signals | ||||||
| from signal import signal as signal_func | from signal import signal as signal_func | ||||||
| from typing import Dict, List, Optional | from time import sleep | ||||||
|  | from typing import List, Optional | ||||||
|  |  | ||||||
| from sanic.compat import OS_IS_WINDOWS | from sanic.compat import OS_IS_WINDOWS | ||||||
| from sanic.exceptions import ServerKilled |  | ||||||
| from sanic.log import error_logger, logger | from sanic.log import error_logger, logger | ||||||
| from sanic.worker.constants import RestartOrder |  | ||||||
| from sanic.worker.process import ProcessState, Worker, WorkerProcess | from sanic.worker.process import ProcessState, Worker, WorkerProcess | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -21,8 +18,7 @@ else: | |||||||
|  |  | ||||||
|  |  | ||||||
| class WorkerManager: | class WorkerManager: | ||||||
|     THRESHOLD = WorkerProcess.THRESHOLD |     THRESHOLD = 50 | ||||||
|     MAIN_IDENT = "Sanic-Main" |  | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
| @@ -35,66 +31,39 @@ class WorkerManager: | |||||||
|     ): |     ): | ||||||
|         self.num_server = number |         self.num_server = number | ||||||
|         self.context = context |         self.context = context | ||||||
|         self.transient: Dict[str, Worker] = {} |         self.transient: List[Worker] = [] | ||||||
|         self.durable: Dict[str, Worker] = {} |         self.durable: List[Worker] = [] | ||||||
|         self.monitor_publisher, self.monitor_subscriber = monitor_pubsub |         self.monitor_publisher, self.monitor_subscriber = monitor_pubsub | ||||||
|         self.worker_state = worker_state |         self.worker_state = worker_state | ||||||
|         self.worker_state[self.MAIN_IDENT] = {"pid": self.pid} |         self.worker_state["Sanic-Main"] = {"pid": self.pid} | ||||||
|         self._shutting_down = False |         self.terminated = False | ||||||
|         self._serve = serve |  | ||||||
|         self._server_settings = server_settings |  | ||||||
|         self._server_count = count() |  | ||||||
|  |  | ||||||
|         if number == 0: |         if number == 0: | ||||||
|             raise RuntimeError("Cannot serve with no workers") |             raise RuntimeError("Cannot serve with no workers") | ||||||
|  |  | ||||||
|         for _ in range(number): |         for i in range(number): | ||||||
|             self.create_server() |             self.manage( | ||||||
|  |                 f"{WorkerProcess.SERVER_LABEL}-{i}", | ||||||
|  |                 serve, | ||||||
|  |                 server_settings, | ||||||
|  |                 transient=True, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         signal_func(SIGINT, self.shutdown_signal) |         signal_func(SIGINT, self.shutdown_signal) | ||||||
|         signal_func(SIGTERM, self.shutdown_signal) |         signal_func(SIGTERM, self.shutdown_signal) | ||||||
|  |  | ||||||
|     def manage(self, ident, func, kwargs, transient=False) -> Worker: |     def manage(self, ident, func, kwargs, transient=False): | ||||||
|         container = self.transient if transient else self.durable |         container = self.transient if transient else self.durable | ||||||
|         worker = Worker(ident, func, kwargs, self.context, self.worker_state) |         container.append( | ||||||
|         container[worker.ident] = worker |             Worker(ident, func, kwargs, self.context, self.worker_state) | ||||||
|         return worker |  | ||||||
|  |  | ||||||
|     def create_server(self) -> Worker: |  | ||||||
|         server_number = next(self._server_count) |  | ||||||
|         return self.manage( |  | ||||||
|             f"{WorkerProcess.SERVER_LABEL}-{server_number}", |  | ||||||
|             self._serve, |  | ||||||
|             self._server_settings, |  | ||||||
|             transient=True, |  | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def shutdown_server(self, ident: Optional[str] = None) -> None: |  | ||||||
|         if not ident: |  | ||||||
|             servers = [ |  | ||||||
|                 worker |  | ||||||
|                 for worker in self.transient.values() |  | ||||||
|                 if worker.ident.startswith(WorkerProcess.SERVER_LABEL) |  | ||||||
|             ] |  | ||||||
|             if not servers: |  | ||||||
|                 error_logger.error( |  | ||||||
|                     "Server shutdown failed because a server was not found." |  | ||||||
|                 ) |  | ||||||
|                 return |  | ||||||
|             worker = choice(servers)  # nosec B311 |  | ||||||
|         else: |  | ||||||
|             worker = self.transient[ident] |  | ||||||
|  |  | ||||||
|         for process in worker.processes: |  | ||||||
|             process.terminate() |  | ||||||
|  |  | ||||||
|         del self.transient[worker.ident] |  | ||||||
|  |  | ||||||
|     def run(self): |     def run(self): | ||||||
|         self.start() |         self.start() | ||||||
|         self.monitor() |         self.monitor() | ||||||
|         self.join() |         self.join() | ||||||
|         self.terminate() |         self.terminate() | ||||||
|  |         # self.kill() | ||||||
|  |  | ||||||
|     def start(self): |     def start(self): | ||||||
|         for process in self.processes: |         for process in self.processes: | ||||||
| @@ -116,41 +85,15 @@ class WorkerManager: | |||||||
|             self.join() |             self.join() | ||||||
|  |  | ||||||
|     def terminate(self): |     def terminate(self): | ||||||
|         if not self._shutting_down: |         if not self.terminated: | ||||||
|             for process in self.processes: |             for process in self.processes: | ||||||
|                 process.terminate() |                 process.terminate() | ||||||
|  |         self.terminated = True | ||||||
|  |  | ||||||
|     def restart( |     def restart(self, process_names: Optional[List[str]] = None, **kwargs): | ||||||
|         self, |  | ||||||
|         process_names: Optional[List[str]] = None, |  | ||||||
|         restart_order=RestartOrder.SHUTDOWN_FIRST, |  | ||||||
|         **kwargs, |  | ||||||
|     ): |  | ||||||
|         for process in self.transient_processes: |         for process in self.transient_processes: | ||||||
|             if not process_names or process.name in process_names: |             if not process_names or process.name in process_names: | ||||||
|                 process.restart(restart_order=restart_order, **kwargs) |                 process.restart(**kwargs) | ||||||
|  |  | ||||||
|     def scale(self, num_worker: int): |  | ||||||
|         if num_worker <= 0: |  | ||||||
|             raise ValueError("Cannot scale to 0 workers.") |  | ||||||
|  |  | ||||||
|         change = num_worker - self.num_server |  | ||||||
|         if change == 0: |  | ||||||
|             logger.info( |  | ||||||
|                 f"No change needed. There are already {num_worker} workers." |  | ||||||
|             ) |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         logger.info(f"Scaling from {self.num_server} to {num_worker} workers") |  | ||||||
|         if change > 0: |  | ||||||
|             for _ in range(change): |  | ||||||
|                 worker = self.create_server() |  | ||||||
|                 for process in worker.processes: |  | ||||||
|                     process.start() |  | ||||||
|         else: |  | ||||||
|             for _ in range(abs(change)): |  | ||||||
|                 self.shutdown_server() |  | ||||||
|         self.num_server = num_worker |  | ||||||
|  |  | ||||||
|     def monitor(self): |     def monitor(self): | ||||||
|         self.wait_for_ack() |         self.wait_for_ack() | ||||||
| @@ -166,15 +109,7 @@ class WorkerManager: | |||||||
|                     elif message == "__TERMINATE__": |                     elif message == "__TERMINATE__": | ||||||
|                         self.shutdown() |                         self.shutdown() | ||||||
|                         break |                         break | ||||||
|                     logger.debug( |                     split_message = message.split(":", 1) | ||||||
|                         "Incoming monitor message: %s", |  | ||||||
|                         message, |  | ||||||
|                         extra={"verbosity": 1}, |  | ||||||
|                     ) |  | ||||||
|                     split_message = message.split(":", 2) |  | ||||||
|                     if message.startswith("__SCALE__"): |  | ||||||
|                         self.scale(int(split_message[-1])) |  | ||||||
|                         continue |  | ||||||
|                     processes = split_message[0] |                     processes = split_message[0] | ||||||
|                     reloaded_files = ( |                     reloaded_files = ( | ||||||
|                         split_message[1] if len(split_message) > 1 else None |                         split_message[1] if len(split_message) > 1 else None | ||||||
| @@ -184,17 +119,10 @@ class WorkerManager: | |||||||
|                     ] |                     ] | ||||||
|                     if "__ALL_PROCESSES__" in process_names: |                     if "__ALL_PROCESSES__" in process_names: | ||||||
|                         process_names = None |                         process_names = None | ||||||
|                     order = ( |  | ||||||
|                         RestartOrder.STARTUP_FIRST |  | ||||||
|                         if "STARTUP_FIRST" in split_message |  | ||||||
|                         else RestartOrder.SHUTDOWN_FIRST |  | ||||||
|                     ) |  | ||||||
|                     self.restart( |                     self.restart( | ||||||
|                         process_names=process_names, |                         process_names=process_names, | ||||||
|                         reloaded_files=reloaded_files, |                         reloaded_files=reloaded_files, | ||||||
|                         restart_order=order, |  | ||||||
|                     ) |                     ) | ||||||
|                 self._sync_states() |  | ||||||
|             except InterruptedError: |             except InterruptedError: | ||||||
|                 if not OS_IS_WINDOWS: |                 if not OS_IS_WINDOWS: | ||||||
|                     raise |                     raise | ||||||
| @@ -202,40 +130,17 @@ class WorkerManager: | |||||||
|  |  | ||||||
|     def wait_for_ack(self):  # no cov |     def wait_for_ack(self):  # no cov | ||||||
|         misses = 0 |         misses = 0 | ||||||
|         message = ( |  | ||||||
|             "It seems that one or more of your workers failed to come " |  | ||||||
|             "online in the allowed time. Sanic is shutting down to avoid a " |  | ||||||
|             f"deadlock. The current threshold is {self.THRESHOLD / 10}s. " |  | ||||||
|             "If this problem persists, please check out the documentation " |  | ||||||
|             "https://sanic.dev/en/guide/deployment/manager.html#worker-ack." |  | ||||||
|         ) |  | ||||||
|         while not self._all_workers_ack(): |         while not self._all_workers_ack(): | ||||||
|             if self.monitor_subscriber.poll(0.1): |             sleep(0.1) | ||||||
|                 monitor_msg = self.monitor_subscriber.recv() |  | ||||||
|                 if monitor_msg != "__TERMINATE_EARLY__": |  | ||||||
|                     self.monitor_publisher.send(monitor_msg) |  | ||||||
|                     continue |  | ||||||
|                 misses = self.THRESHOLD |  | ||||||
|                 message = ( |  | ||||||
|                     "One of your worker processes terminated before startup " |  | ||||||
|                     "was completed. Please solve any errors experienced " |  | ||||||
|                     "during startup. If you do not see an exception traceback " |  | ||||||
|                     "in your error logs, try running Sanic in in a single " |  | ||||||
|                     "process using --single-process or single_process=True. " |  | ||||||
|                     "Once you are confident that the server is able to start " |  | ||||||
|                     "without errors you can switch back to multiprocess mode." |  | ||||||
|                 ) |  | ||||||
|             misses += 1 |             misses += 1 | ||||||
|             if misses > self.THRESHOLD: |             if misses > self.THRESHOLD: | ||||||
|                 error_logger.error( |                 error_logger.error("Not all workers are ack. Shutting down.") | ||||||
|                     "Not all workers acknowledged a successful startup. " |  | ||||||
|                     "Shutting down.\n\n" + message |  | ||||||
|                 ) |  | ||||||
|                 self.kill() |                 self.kill() | ||||||
|  |                 sys.exit(1) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def workers(self) -> List[Worker]: |     def workers(self): | ||||||
|         return list(self.transient.values()) + list(self.durable.values()) |         return self.transient + self.durable | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def processes(self): |     def processes(self): | ||||||
| @@ -245,22 +150,15 @@ class WorkerManager: | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def transient_processes(self): |     def transient_processes(self): | ||||||
|         for worker in self.transient.values(): |         for worker in self.transient: | ||||||
|             for process in worker.processes: |             for process in worker.processes: | ||||||
|                 yield process |                 yield process | ||||||
|  |  | ||||||
|     def kill(self): |     def kill(self): | ||||||
|         for process in self.processes: |         for process in self.processes: | ||||||
|             logger.info("Killing %s [%s]", process.name, process.pid) |  | ||||||
|             os.kill(process.pid, SIGKILL) |             os.kill(process.pid, SIGKILL) | ||||||
|         raise ServerKilled |  | ||||||
|  |  | ||||||
|     def shutdown_signal(self, signal, frame): |     def shutdown_signal(self, signal, frame): | ||||||
|         if self._shutting_down: |  | ||||||
|             logger.info("Shutdown interrupted. Killing.") |  | ||||||
|             with suppress(ServerKilled): |  | ||||||
|                 self.kill() |  | ||||||
|  |  | ||||||
|         logger.info("Received signal %s. Shutting down.", Signals(signal).name) |         logger.info("Received signal %s. Shutting down.", Signals(signal).name) | ||||||
|         self.monitor_publisher.send(None) |         self.monitor_publisher.send(None) | ||||||
|         self.shutdown() |         self.shutdown() | ||||||
| @@ -269,7 +167,6 @@ class WorkerManager: | |||||||
|         for process in self.processes: |         for process in self.processes: | ||||||
|             if process.is_alive(): |             if process.is_alive(): | ||||||
|                 process.terminate() |                 process.terminate() | ||||||
|         self._shutting_down = True |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def pid(self): |     def pid(self): | ||||||
| @@ -282,9 +179,3 @@ class WorkerManager: | |||||||
|             if worker_state.get("server") |             if worker_state.get("server") | ||||||
|         ] |         ] | ||||||
|         return all(acked) and len(acked) == self.num_server |         return all(acked) and len(acked) == self.num_server | ||||||
|  |  | ||||||
|     def _sync_states(self): |  | ||||||
|         for process in self.processes: |  | ||||||
|             state = self.worker_state[process.name].get("state") |  | ||||||
|             if state and process.state.name != state: |  | ||||||
|                 process.set_state(ProcessState[state], True) |  | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ from multiprocessing.connection import Connection | |||||||
| from os import environ, getpid | from os import environ, getpid | ||||||
| from typing import Any, Dict | from typing import Any, Dict | ||||||
|  |  | ||||||
| from sanic.log import Colors, logger |  | ||||||
| from sanic.worker.process import ProcessState | from sanic.worker.process import ProcessState | ||||||
| from sanic.worker.state import WorkerState | from sanic.worker.state import WorkerState | ||||||
|  |  | ||||||
| @@ -17,45 +16,20 @@ class WorkerMultiplexer: | |||||||
|         self._state = WorkerState(worker_state, self.name) |         self._state = WorkerState(worker_state, self.name) | ||||||
|  |  | ||||||
|     def ack(self): |     def ack(self): | ||||||
|         logger.debug( |  | ||||||
|             f"{Colors.BLUE}Process ack: {Colors.BOLD}{Colors.SANIC}" |  | ||||||
|             f"%s {Colors.BLUE}[%s]{Colors.END}", |  | ||||||
|             self.name, |  | ||||||
|             self.pid, |  | ||||||
|         ) |  | ||||||
|         self._state._state[self.name] = { |         self._state._state[self.name] = { | ||||||
|             **self._state._state[self.name], |             **self._state._state[self.name], | ||||||
|             "state": ProcessState.ACKED.name, |             "state": ProcessState.ACKED.name, | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     def restart( |     def restart(self, name: str = ""): | ||||||
|         self, |  | ||||||
|         name: str = "", |  | ||||||
|         all_workers: bool = False, |  | ||||||
|         zero_downtime: bool = False, |  | ||||||
|     ): |  | ||||||
|         if name and all_workers: |  | ||||||
|             raise ValueError( |  | ||||||
|                 "Ambiguous restart with both a named process and" |  | ||||||
|                 " all_workers=True" |  | ||||||
|             ) |  | ||||||
|         if not name: |         if not name: | ||||||
|             name = "__ALL_PROCESSES__:" if all_workers else self.name |             name = self.name | ||||||
|         if not name.endswith(":"): |  | ||||||
|             name += ":" |  | ||||||
|         if zero_downtime: |  | ||||||
|             name += ":STARTUP_FIRST" |  | ||||||
|         self._monitor_publisher.send(name) |         self._monitor_publisher.send(name) | ||||||
|  |  | ||||||
|     reload = restart  # no cov |     reload = restart  # no cov | ||||||
|  |  | ||||||
|     def scale(self, num_workers: int): |     def terminate(self): | ||||||
|         message = f"__SCALE__:{num_workers}" |         self._monitor_publisher.send("__TERMINATE__") | ||||||
|         self._monitor_publisher.send(message) |  | ||||||
|  |  | ||||||
|     def terminate(self, early: bool = False): |  | ||||||
|         message = "__TERMINATE_EARLY__" if early else "__TERMINATE__" |  | ||||||
|         self._monitor_publisher.send(message) |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def pid(self) -> int: |     def pid(self) -> int: | ||||||
|   | |||||||
| @@ -1,14 +1,12 @@ | |||||||
| import os | import os | ||||||
|  |  | ||||||
| from datetime import datetime, timezone | from datetime import datetime, timezone | ||||||
|  | from enum import IntEnum, auto | ||||||
| from multiprocessing.context import BaseContext | from multiprocessing.context import BaseContext | ||||||
| from signal import SIGINT | from signal import SIGINT | ||||||
| from threading import Thread |  | ||||||
| from time import sleep |  | ||||||
| from typing import Any, Dict, Set | from typing import Any, Dict, Set | ||||||
|  |  | ||||||
| from sanic.log import Colors, logger | from sanic.log import Colors, logger | ||||||
| from sanic.worker.constants import ProcessState, RestartOrder |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_now(): | def get_now(): | ||||||
| @@ -16,8 +14,15 @@ def get_now(): | |||||||
|     return now |     return now | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ProcessState(IntEnum): | ||||||
|  |     IDLE = auto() | ||||||
|  |     STARTED = auto() | ||||||
|  |     ACKED = auto() | ||||||
|  |     JOINED = auto() | ||||||
|  |     TERMINATED = auto() | ||||||
|  |  | ||||||
|  |  | ||||||
| class WorkerProcess: | class WorkerProcess: | ||||||
|     THRESHOLD = 300  # == 30 seconds |  | ||||||
|     SERVER_LABEL = "Server" |     SERVER_LABEL = "Server" | ||||||
|  |  | ||||||
|     def __init__(self, factory, name, target, kwargs, worker_state): |     def __init__(self, factory, name, target, kwargs, worker_state): | ||||||
| @@ -49,9 +54,8 @@ class WorkerProcess: | |||||||
|             f"{Colors.SANIC}%s{Colors.END}", |             f"{Colors.SANIC}%s{Colors.END}", | ||||||
|             self.name, |             self.name, | ||||||
|         ) |         ) | ||||||
|         self.set_state(ProcessState.STARTING) |  | ||||||
|         self._current_process.start() |  | ||||||
|         self.set_state(ProcessState.STARTED) |         self.set_state(ProcessState.STARTED) | ||||||
|  |         self._process.start() | ||||||
|         if not self.worker_state[self.name].get("starts"): |         if not self.worker_state[self.name].get("starts"): | ||||||
|             self.worker_state[self.name] = { |             self.worker_state[self.name] = { | ||||||
|                 **self.worker_state[self.name], |                 **self.worker_state[self.name], | ||||||
| @@ -63,7 +67,7 @@ class WorkerProcess: | |||||||
|  |  | ||||||
|     def join(self): |     def join(self): | ||||||
|         self.set_state(ProcessState.JOINED) |         self.set_state(ProcessState.JOINED) | ||||||
|         self._current_process.join() |         self._process.join() | ||||||
|  |  | ||||||
|     def terminate(self): |     def terminate(self): | ||||||
|         if self.state is not ProcessState.TERMINATED: |         if self.state is not ProcessState.TERMINATED: | ||||||
| @@ -76,23 +80,21 @@ class WorkerProcess: | |||||||
|             ) |             ) | ||||||
|             self.set_state(ProcessState.TERMINATED, force=True) |             self.set_state(ProcessState.TERMINATED, force=True) | ||||||
|             try: |             try: | ||||||
|  |                 # self._process.terminate() | ||||||
|                 os.kill(self.pid, SIGINT) |                 os.kill(self.pid, SIGINT) | ||||||
|                 del self.worker_state[self.name] |                 del self.worker_state[self.name] | ||||||
|             except (KeyError, AttributeError, ProcessLookupError): |             except (KeyError, AttributeError, ProcessLookupError): | ||||||
|                 ... |                 ... | ||||||
|  |  | ||||||
|     def restart(self, restart_order=RestartOrder.SHUTDOWN_FIRST, **kwargs): |     def restart(self, **kwargs): | ||||||
|         logger.debug( |         logger.debug( | ||||||
|             f"{Colors.BLUE}Restarting a process: {Colors.BOLD}{Colors.SANIC}" |             f"{Colors.BLUE}Restarting a process: {Colors.BOLD}{Colors.SANIC}" | ||||||
|             f"%s {Colors.BLUE}[%s]{Colors.END}", |             f"%s {Colors.BLUE}[%s]{Colors.END}", | ||||||
|             self.name, |             self.name, | ||||||
|             self.pid, |             self.pid, | ||||||
|         ) |         ) | ||||||
|         self.set_state(ProcessState.RESTARTING, force=True) |         self._process.terminate() | ||||||
|         if restart_order is RestartOrder.SHUTDOWN_FIRST: |         self.set_state(ProcessState.IDLE, force=True) | ||||||
|             self._terminate_now() |  | ||||||
|         else: |  | ||||||
|             self._old_process = self._current_process |  | ||||||
|         self.kwargs.update( |         self.kwargs.update( | ||||||
|             {"config": {k.upper(): v for k, v in kwargs.items()}} |             {"config": {k.upper(): v for k, v in kwargs.items()}} | ||||||
|         ) |         ) | ||||||
| @@ -102,9 +104,6 @@ class WorkerProcess: | |||||||
|         except AttributeError: |         except AttributeError: | ||||||
|             raise RuntimeError("Restart failed") |             raise RuntimeError("Restart failed") | ||||||
|  |  | ||||||
|         if restart_order is RestartOrder.STARTUP_FIRST: |  | ||||||
|             self._terminate_soon() |  | ||||||
|  |  | ||||||
|         self.worker_state[self.name] = { |         self.worker_state[self.name] = { | ||||||
|             **self.worker_state[self.name], |             **self.worker_state[self.name], | ||||||
|             "pid": self.pid, |             "pid": self.pid, | ||||||
| @@ -114,14 +113,14 @@ class WorkerProcess: | |||||||
|  |  | ||||||
|     def is_alive(self): |     def is_alive(self): | ||||||
|         try: |         try: | ||||||
|             return self._current_process.is_alive() |             return self._process.is_alive() | ||||||
|         except AssertionError: |         except AssertionError: | ||||||
|             return False |             return False | ||||||
|  |  | ||||||
|     def spawn(self): |     def spawn(self): | ||||||
|         if self.state not in (ProcessState.IDLE, ProcessState.RESTARTING): |         if self.state is not ProcessState.IDLE: | ||||||
|             raise Exception("Cannot spawn a worker process until it is idle.") |             raise Exception("Cannot spawn a worker process until it is idle.") | ||||||
|         self._current_process = self.factory( |         self._process = self.factory( | ||||||
|             name=self.name, |             name=self.name, | ||||||
|             target=self.target, |             target=self.target, | ||||||
|             kwargs=self.kwargs, |             kwargs=self.kwargs, | ||||||
| @@ -130,61 +129,10 @@ class WorkerProcess: | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def pid(self): |     def pid(self): | ||||||
|         return self._current_process.pid |         return self._process.pid | ||||||
|  |  | ||||||
|     def _terminate_now(self): |  | ||||||
|         logger.debug( |  | ||||||
|             f"{Colors.BLUE}Begin restart termination: " |  | ||||||
|             f"{Colors.BOLD}{Colors.SANIC}" |  | ||||||
|             f"%s {Colors.BLUE}[%s]{Colors.END}", |  | ||||||
|             self.name, |  | ||||||
|             self._current_process.pid, |  | ||||||
|         ) |  | ||||||
|         self._current_process.terminate() |  | ||||||
|  |  | ||||||
|     def _terminate_soon(self): |  | ||||||
|         logger.debug( |  | ||||||
|             f"{Colors.BLUE}Begin restart termination: " |  | ||||||
|             f"{Colors.BOLD}{Colors.SANIC}" |  | ||||||
|             f"%s {Colors.BLUE}[%s]{Colors.END}", |  | ||||||
|             self.name, |  | ||||||
|             self._current_process.pid, |  | ||||||
|         ) |  | ||||||
|         termination_thread = Thread(target=self._wait_to_terminate) |  | ||||||
|         termination_thread.start() |  | ||||||
|  |  | ||||||
|     def _wait_to_terminate(self): |  | ||||||
|         logger.debug( |  | ||||||
|             f"{Colors.BLUE}Waiting for process to be acked: " |  | ||||||
|             f"{Colors.BOLD}{Colors.SANIC}" |  | ||||||
|             f"%s {Colors.BLUE}[%s]{Colors.END}", |  | ||||||
|             self.name, |  | ||||||
|             self._old_process.pid, |  | ||||||
|         ) |  | ||||||
|         misses = 0 |  | ||||||
|         while self.state is not ProcessState.ACKED: |  | ||||||
|             sleep(0.1) |  | ||||||
|             misses += 1 |  | ||||||
|             if misses > self.THRESHOLD: |  | ||||||
|                 raise TimeoutError( |  | ||||||
|                     f"Worker {self.name} failed to come ack within " |  | ||||||
|                     f"{self.THRESHOLD / 10} seconds" |  | ||||||
|                 ) |  | ||||||
|         else: |  | ||||||
|             logger.debug( |  | ||||||
|                 f"{Colors.BLUE}Process acked. Terminating: " |  | ||||||
|                 f"{Colors.BOLD}{Colors.SANIC}" |  | ||||||
|                 f"%s {Colors.BLUE}[%s]{Colors.END}", |  | ||||||
|                 self.name, |  | ||||||
|                 self._old_process.pid, |  | ||||||
|             ) |  | ||||||
|             self._old_process.terminate() |  | ||||||
|         delattr(self, "_old_process") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Worker: | class Worker: | ||||||
|     WORKER_PREFIX = "Sanic-" |  | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         ident: str, |         ident: str, | ||||||
| @@ -203,12 +151,8 @@ class Worker: | |||||||
|  |  | ||||||
|     def create_process(self) -> WorkerProcess: |     def create_process(self) -> WorkerProcess: | ||||||
|         process = WorkerProcess( |         process = WorkerProcess( | ||||||
|             # Need to ignore this typing error - The problem is the |             factory=self.context.Process, | ||||||
|             # BaseContext itself has no Process. But, all of its |             name=f"Sanic-{self.ident}-{len(self.processes)}", | ||||||
|             # implementations do. We can safely ignore as it is a typing |  | ||||||
|             # issue in the standard lib. |  | ||||||
|             factory=self.context.Process,  # type: ignore |  | ||||||
|             name=f"{self.WORKER_PREFIX}{self.ident}-{len(self.processes)}", |  | ||||||
|             target=self.serve, |             target=self.serve, | ||||||
|             kwargs={**self.server_settings}, |             kwargs={**self.server_settings}, | ||||||
|             worker_state=self.worker_state, |             worker_state=self.worker_state, | ||||||
|   | |||||||
| @@ -9,7 +9,6 @@ from multiprocessing.connection import Connection | |||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from signal import SIGINT, SIGTERM | from signal import SIGINT, SIGTERM | ||||||
| from signal import signal as signal_func | from signal import signal as signal_func | ||||||
| from time import sleep |  | ||||||
| from typing import Dict, Set | from typing import Dict, Set | ||||||
|  |  | ||||||
| from sanic.server.events import trigger_events | from sanic.server.events import trigger_events | ||||||
| @@ -17,8 +16,6 @@ from sanic.worker.loader import AppLoader | |||||||
|  |  | ||||||
|  |  | ||||||
| class Reloader: | class Reloader: | ||||||
|     INTERVAL = 1.0  # seconds |  | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         publisher: Connection, |         publisher: Connection, | ||||||
| @@ -27,7 +24,7 @@ class Reloader: | |||||||
|         app_loader: AppLoader, |         app_loader: AppLoader, | ||||||
|     ): |     ): | ||||||
|         self._publisher = publisher |         self._publisher = publisher | ||||||
|         self.interval = interval or self.INTERVAL |         self.interval = interval | ||||||
|         self.reload_dirs = reload_dirs |         self.reload_dirs = reload_dirs | ||||||
|         self.run = True |         self.run = True | ||||||
|         self.app_loader = app_loader |         self.app_loader = app_loader | ||||||
| @@ -65,7 +62,6 @@ class Reloader: | |||||||
|                 self.reload(",".join(changed) if changed else "unknown") |                 self.reload(",".join(changed) if changed else "unknown") | ||||||
|                 if after_trigger: |                 if after_trigger: | ||||||
|                     trigger_events(after_trigger, loop, app) |                     trigger_events(after_trigger, loop, app) | ||||||
|             sleep(self.interval) |  | ||||||
|         else: |         else: | ||||||
|             if reloader_stop: |             if reloader_stop: | ||||||
|                 trigger_events(reloader_stop, loop, app) |                 trigger_events(reloader_stop, loop, app) | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import os | import os | ||||||
| import socket | import socket | ||||||
| import warnings |  | ||||||
|  |  | ||||||
| from functools import partial | from functools import partial | ||||||
| from multiprocessing.connection import Connection | from multiprocessing.connection import Connection | ||||||
| @@ -11,13 +10,11 @@ from typing import Any, Dict, List, Optional, Type, Union | |||||||
| from sanic.application.constants import ServerStage | from sanic.application.constants import ServerStage | ||||||
| from sanic.application.state import ApplicationServerInfo | from sanic.application.state import ApplicationServerInfo | ||||||
| from sanic.http.constants import HTTP | from sanic.http.constants import HTTP | ||||||
| from sanic.log import error_logger |  | ||||||
| from sanic.models.server_types import Signal | from sanic.models.server_types import Signal | ||||||
| from sanic.server.protocols.http_protocol import HttpProtocol | from sanic.server.protocols.http_protocol import HttpProtocol | ||||||
| from sanic.server.runners import _serve_http_1, _serve_http_3 | from sanic.server.runners import _serve_http_1, _serve_http_3 | ||||||
| from sanic.worker.loader import AppLoader, CertLoader | from sanic.worker.loader import AppLoader, CertLoader | ||||||
| from sanic.worker.multiplexer import WorkerMultiplexer | from sanic.worker.multiplexer import WorkerMultiplexer | ||||||
| from sanic.worker.process import Worker, WorkerProcess |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def worker_serve( | def worker_serve( | ||||||
| @@ -48,7 +45,6 @@ def worker_serve( | |||||||
|     config=None, |     config=None, | ||||||
|     passthru: Optional[Dict[str, Any]] = None, |     passthru: Optional[Dict[str, Any]] = None, | ||||||
| ): | ): | ||||||
|     try: |  | ||||||
|     from sanic import Sanic |     from sanic import Sanic | ||||||
|  |  | ||||||
|     if app_loader: |     if app_loader: | ||||||
| @@ -80,16 +76,11 @@ def worker_serve( | |||||||
|             info.settings["ssl"] = ssl |             info.settings["ssl"] = ssl | ||||||
|  |  | ||||||
|     # When in a worker process, do some init |     # When in a worker process, do some init | ||||||
|         worker_name = os.environ.get("SANIC_WORKER_NAME") |     if os.environ.get("SANIC_WORKER_NAME"): | ||||||
|         if worker_name and worker_name.startswith( |  | ||||||
|             Worker.WORKER_PREFIX + WorkerProcess.SERVER_LABEL |  | ||||||
|         ): |  | ||||||
|         # Hydrate apps with any passed server info |         # Hydrate apps with any passed server info | ||||||
|  |  | ||||||
|         if monitor_publisher is None: |         if monitor_publisher is None: | ||||||
|                 raise RuntimeError( |             raise RuntimeError("No restart publisher found in worker process") | ||||||
|                     "No restart publisher found in worker process" |  | ||||||
|                 ) |  | ||||||
|         if worker_state is None: |         if worker_state is None: | ||||||
|             raise RuntimeError("No worker state found in worker process") |             raise RuntimeError("No worker state found in worker process") | ||||||
|  |  | ||||||
| @@ -97,9 +88,7 @@ def worker_serve( | |||||||
|         apps = list(Sanic._app_registry.values()) |         apps = list(Sanic._app_registry.values()) | ||||||
|         app.before_server_start(partial(app._start_servers, apps=apps)) |         app.before_server_start(partial(app._start_servers, apps=apps)) | ||||||
|         for a in apps: |         for a in apps: | ||||||
|                 a.multiplexer = WorkerMultiplexer( |             a.multiplexer = WorkerMultiplexer(monitor_publisher, worker_state) | ||||||
|                     monitor_publisher, worker_state |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|     if app.debug: |     if app.debug: | ||||||
|         loop.set_debug(app.debug) |         loop.set_debug(app.debug) | ||||||
| @@ -133,11 +122,3 @@ def worker_serve( | |||||||
|         state, |         state, | ||||||
|         asyncio_server_kwargs, |         asyncio_server_kwargs, | ||||||
|     ) |     ) | ||||||
|     except Exception as e: |  | ||||||
|         warnings.simplefilter("ignore", category=RuntimeWarning) |  | ||||||
|         if monitor_publisher: |  | ||||||
|             error_logger.exception(e) |  | ||||||
|             multiplexer = WorkerMultiplexer(monitor_publisher, {}) |  | ||||||
|             multiplexer.terminate(True) |  | ||||||
|         else: |  | ||||||
|             raise e |  | ||||||
|   | |||||||
| @@ -1,4 +1,2 @@ | |||||||
| [flake8] | [flake8] | ||||||
| ignore = E203, W503 | ignore = E203, W503 | ||||||
| per-file-ignores = |  | ||||||
|     sanic/app.py:E402 |  | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user