Compare commits
	
		
			1 Commits
		
	
	
		
			smoother-p
			...
			start-rest
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | fa864f0bab | 
							
								
								
									
										16
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
								
							| @@ -21,14 +21,7 @@ body: | |||||||
|     id: code |     id: code | ||||||
|     attributes: |     attributes: | ||||||
|       label: Code snippet |       label: Code snippet | ||||||
|       description: | |       description: Relevant source code, make sure to remove what is not necessary. | ||||||
|           Relevant source code, make sure to remove what is not necessary. Please try and format your code so that it is easier to read. For example: |  | ||||||
|  |  | ||||||
|               ```python |  | ||||||
|               from sanic import Sanic |  | ||||||
|  |  | ||||||
|               app = Sanic("Example") |  | ||||||
|               ``` |  | ||||||
|     validations: |     validations: | ||||||
|       required: false |       required: false | ||||||
|   - type: textarea |   - type: textarea | ||||||
| @@ -49,16 +42,11 @@ body: | |||||||
|         - ASGI |         - ASGI | ||||||
|     validations: |     validations: | ||||||
|       required: true |       required: true | ||||||
|   - type: dropdown |   - type: input | ||||||
|     id: os |     id: os | ||||||
|     attributes: |     attributes: | ||||||
|       label: Operating System |       label: Operating System | ||||||
|       description: What OS? |       description: What OS? | ||||||
|       options: |  | ||||||
|         - Linux |  | ||||||
|         - MacOS |  | ||||||
|         - Windows |  | ||||||
|         - Other (tell us in the description) |  | ||||||
|     validations: |     validations: | ||||||
|       required: true |       required: true | ||||||
|   - type: input |   - type: input | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,6 +17,7 @@ jobs: | |||||||
|       matrix: |       matrix: | ||||||
|         os: [ubuntu-latest] |         os: [ubuntu-latest] | ||||||
|         config: |         config: | ||||||
|  |           - { python-version: 3.7, tox-env: security} | ||||||
|           - { 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} | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/pr-python-pypy.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/pr-python-pypy.yml
									
									
									
									
										vendored
									
									
								
							| @@ -5,11 +5,11 @@ on: | |||||||
|       tox-env: |       tox-env: | ||||||
|         description: "Tox Env to run on the PyPy Infra" |         description: "Tox Env to run on the PyPy Infra" | ||||||
|         required: false |         required: false | ||||||
|         default: "pypy310" |         default: "pypy37" | ||||||
|       pypy-version: |       pypy-version: | ||||||
|         description: "Version of PyPy to use" |         description: "Version of PyPy to use" | ||||||
|         required: false |         required: false | ||||||
|         default: "pypy-3.10" |         default: "pypy-3.7" | ||||||
| jobs: | jobs: | ||||||
|   testPyPy: |   testPyPy: | ||||||
|     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} |     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} | ||||||
|   | |||||||
							
								
								
									
										36
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | name: Python 3.7 Tests | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |       - current-release | ||||||
|  |       - "*LTS" | ||||||
|  |     types: [opened, synchronize, reopened, ready_for_review] | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   testPy37: | ||||||
|  |     if: github.event.pull_request.draft == false | ||||||
|  |     name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }} | ||||||
|  |     runs-on: ${{ matrix.os }} | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: true | ||||||
|  |       matrix: | ||||||
|  |         #         os: [ubuntu-latest, macos-latest] | ||||||
|  |         os: [ubuntu-latest] | ||||||
|  |         config: | ||||||
|  |           - { python-version: 3.7, tox-env: py37 } | ||||||
|  |           - { python-version: 3.7, tox-env: py37-no-ext } | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout the Repository | ||||||
|  |         uses: actions/checkout@v2 | ||||||
|  |         id: checkout-branch | ||||||
|  |  | ||||||
|  |       - name: Run Unit Tests | ||||||
|  |         uses: harshanarayana/custom-actions@main | ||||||
|  |         with: | ||||||
|  |           python-version: ${{ matrix.config.python-version }} | ||||||
|  |           test-infra-tool: tox | ||||||
|  |           test-infra-version: latest | ||||||
|  |           action: tests | ||||||
|  |           test-additional-args: "-e=${{ matrix.config.tox-env }}" | ||||||
|  |           test-failure-retry: "3" | ||||||
							
								
								
									
										1
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,6 +17,7 @@ jobs: | |||||||
|       matrix: |       matrix: | ||||||
|         os: [ubuntu-latest] |         os: [ubuntu-latest] | ||||||
|         config: |         config: | ||||||
|  |           # - { python-version: 3.7, tox-env: type-checking} | ||||||
|           - { 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} | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
								
							| @@ -16,10 +16,12 @@ jobs: | |||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|         config: |         config: | ||||||
|  |           - { python-version: 3.7, tox-env: py37-no-ext } | ||||||
|           - { 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: "3.11", tox-env: py310-no-ext } | ||||||
|  |           - { python-version: pypy-3.7, tox-env: pypy37-no-ext } | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout Repository |       - name: Checkout Repository | ||||||
|   | |||||||
							
								
								
									
										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.8", "3.9", "3.10", "3.11"] |         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout repository |       - name: Checkout repository | ||||||
|   | |||||||
							
								
								
									
										55
									
								
								.github/workflows/publish-package.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										55
									
								
								.github/workflows/publish-package.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,39 +1,28 @@ | |||||||
| name: Upload Python Package | name: Publish Artifacts | ||||||
|  |  | ||||||
| on: | on: | ||||||
|   release: |   release: | ||||||
|     types: [created] |     types: [created] | ||||||
|   workflow_dispatch: |  | ||||||
| jobs: | jobs: | ||||||
|   build-n-publish: |   publishPythonPackage: | ||||||
|     name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI |     name: Publishing Sanic Release Artifacts | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|  |  | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: true | ||||||
|  |       matrix: | ||||||
|  |         python-version: ["3.10"] | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|     - uses: actions/checkout@v3 |       - name: Checkout Repository | ||||||
|     - name: Set up Python |         uses: actions/checkout@v2 | ||||||
|       uses: actions/setup-python@v4 |  | ||||||
|       with: |       - name: Publish Python Package | ||||||
|         python-version: "3.x" |         uses: harshanarayana/custom-actions@main | ||||||
|     - name: Install pypa/build |         with: | ||||||
|       run: >- |           python-version: ${{ matrix.python-version }} | ||||||
|         python3 -m |           package-infra-name: "twine" | ||||||
|         pip install |           pypi-user: __token__ | ||||||
|         build |           pypi-access-token: ${{ secrets.PYPI_ACCESS_TOKEN }} | ||||||
|         --user |           action: "package-publish" | ||||||
|     - name: Build a binary wheel and a source tarball |           pypi-verify-metadata: "true" | ||||||
|       run: >- |  | ||||||
|         python3 -m |  | ||||||
|         build |  | ||||||
|         --sdist |  | ||||||
|         --wheel |  | ||||||
|         --outdir dist/ |  | ||||||
|         . |  | ||||||
|     # - name: Publish distribution 📦 to Test PyPI |  | ||||||
|     #   uses: pypa/gh-action-pypi-publish@release/v1 |  | ||||||
|     #   with: |  | ||||||
|     #     password: ${{ secrets.SANIC_TEST_PYPI_API_TOKEN }} |  | ||||||
|     #     repository-url: https://test.pypi.org/legacy/ |  | ||||||
|     - name: Publish distribution 📦 to PyPI |  | ||||||
|       uses: pypa/gh-action-pypi-publish@release/v1 |  | ||||||
|       with: |  | ||||||
|         password: ${{ secrets.SANIC_PYPI_API_TOKEN }} |  | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								README.rst
									
									
									
									
									
								
							| @@ -11,7 +11,7 @@ Sanic | Build fast. Run fast. | |||||||
|     :stub-columns: 1 |     :stub-columns: 1 | ||||||
|  |  | ||||||
|     * - Build |     * - Build | ||||||
|       - | |Py310Test| |Py39Test| |Py38Test| |       - | |Py310Test| |Py39Test| |Py38Test| |Py37Test| | ||||||
|     * - Docs |     * - Docs | ||||||
|       - | |UserGuide| |Documentation| |       - | |UserGuide| |Documentation| | ||||||
|     * - Package |     * - Package | ||||||
| @@ -19,7 +19,7 @@ Sanic | Build fast. Run fast. | |||||||
|     * - Support |     * - Support | ||||||
|       - | |Forums| |Discord| |Awesome| |       - | |Forums| |Discord| |Awesome| | ||||||
|     * - Stats |     * - Stats | ||||||
|       - | |Monthly Downloads| |Weekly Downloads| |Conda downloads| |       - | |Downloads| |WkDownloads| |Conda downloads| | ||||||
|  |  | ||||||
| .. |UserGuide| image:: https://img.shields.io/badge/user%20guide-sanic-ff0068 | .. |UserGuide| image:: https://img.shields.io/badge/user%20guide-sanic-ff0068 | ||||||
|    :target: https://sanicframework.org/ |    :target: https://sanicframework.org/ | ||||||
| @@ -33,6 +33,8 @@ Sanic | Build fast. Run fast. | |||||||
|    :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml |    :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml | ||||||
| .. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main | .. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main | ||||||
|    :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml |    :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml | ||||||
|  | .. |Py37Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python37.yml/badge.svg?branch=main | ||||||
|  |    :target: https://github.com/sanic-org/sanic/actions/workflows/pr-python37.yml | ||||||
| .. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest | .. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest | ||||||
|    :target: http://sanic.readthedocs.io/en/latest/?badge=latest |    :target: http://sanic.readthedocs.io/en/latest/?badge=latest | ||||||
| .. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg | .. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg | ||||||
| @@ -50,23 +52,19 @@ Sanic | Build fast. Run fast. | |||||||
| .. |Awesome| image:: https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg | .. |Awesome| image:: https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg | ||||||
|     :alt: Awesome Sanic List |     :alt: Awesome Sanic List | ||||||
|     :target: https://github.com/mekicha/awesome-sanic |     :target: https://github.com/mekicha/awesome-sanic | ||||||
| .. |Monthly Downloads| image:: https://img.shields.io/pypi/dm/sanic.svg | .. |Downloads| image:: https://pepy.tech/badge/sanic/month | ||||||
|     :alt: Downloads |     :alt: Downloads | ||||||
|     :target: https://pepy.tech/project/sanic |     :target: https://pepy.tech/project/sanic | ||||||
| .. |Weekly Downloads| image:: https://img.shields.io/pypi/dw/sanic.svg | .. |WkDownloads| image:: https://pepy.tech/badge/sanic/week | ||||||
|     :alt: Downloads |     :alt: Downloads | ||||||
|     :target: https://pepy.tech/project/sanic |     :target: https://pepy.tech/project/sanic | ||||||
| .. |Conda downloads| image:: https://img.shields.io/conda/dn/conda-forge/sanic.svg | .. |Conda downloads| image:: https://img.shields.io/conda/dn/conda-forge/sanic.svg | ||||||
|     :alt: Downloads |     :alt: Downloads | ||||||
|     :target: https://anaconda.org/conda-forge/sanic |     :target: https://anaconda.org/conda-forge/sanic | ||||||
| .. |Linode| image:: https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg |  | ||||||
|     :alt: Linode |  | ||||||
|     :target: https://www.linode.com |  | ||||||
|     :width: 200px |  | ||||||
|  |  | ||||||
| .. end-badges | .. end-badges | ||||||
|  |  | ||||||
| Sanic is a **Python 3.8+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy. | Sanic is a **Python 3.7+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy. | ||||||
|  |  | ||||||
| Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanicframework.org/en/guide/deployment/running.html#asgi>`_. | Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanicframework.org/en/guide/deployment/running.html#asgi>`_. | ||||||
|  |  | ||||||
| @@ -79,7 +77,7 @@ The goal of the project is to provide a simple way to get up and running a highl | |||||||
| Sponsor | Sponsor | ||||||
| ------- | ------- | ||||||
|  |  | ||||||
| Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic. | Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.  | ||||||
|  |  | ||||||
| Thanks to `Linode <https://www.linode.com>`_ for their contribution towards the development and community of Sanic. | Thanks to `Linode <https://www.linode.com>`_ for their contribution towards the development and community of Sanic. | ||||||
|  |  | ||||||
| @@ -143,17 +141,17 @@ And, we can verify it is working: ``curl localhost:8000 -i`` | |||||||
|  |  | ||||||
| **Now, let's go build something fast!** | **Now, let's go build something fast!** | ||||||
|  |  | ||||||
| Minimum Python version is 3.8. If you need Python 3.7 support, please use v22.12LTS. | Minimum Python version is 3.7. If you need Python 3.6 support, please use v20.12LTS. | ||||||
|  |  | ||||||
| Documentation | Documentation | ||||||
| ------------- | ------------- | ||||||
|  |  | ||||||
| `User Guide <https://sanic.dev>`__ and `API Documentation <http://sanic.readthedocs.io/>`__. | `User Guide <https://sanicframework.org>`__ and `API Documentation <http://sanic.readthedocs.io/>`__. | ||||||
|  |  | ||||||
| Changelog | Changelog | ||||||
| --------- | --------- | ||||||
|  |  | ||||||
| `Release Changelogs <https://sanic.readthedocs.io/en/stable/sanic/changelog.html>`__. | `Release Changelogs <https://github.com/sanic-org/sanic/blob/master/CHANGELOG.rst>`__. | ||||||
|  |  | ||||||
|  |  | ||||||
| Questions and Discussion | Questions and Discussion | ||||||
| @@ -165,3 +163,8 @@ Contribution | |||||||
| ------------ | ------------ | ||||||
|  |  | ||||||
| We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/sanic-org/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://github.com/sanic-org/sanic/blob/master/CONTRIBUTING.rst>`_. | We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/sanic-org/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://github.com/sanic-org/sanic/blob/master/CONTRIBUTING.rst>`_. | ||||||
|  |  | ||||||
|  | .. |Linode| image:: https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg | ||||||
|  |     :alt: Linode | ||||||
|  |     :target: https://www.linode.com | ||||||
|  |     :width: 200px | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ | |||||||
| | 🔷 In support release | | 🔷 In support release | ||||||
| | | | | ||||||
|  |  | ||||||
| .. mdinclude:: ./releases/23/23.6.md |  | ||||||
| .. mdinclude:: ./releases/23/23.3.md | .. mdinclude:: ./releases/23/23.3.md | ||||||
| .. mdinclude:: ./releases/22/22.12.md | .. mdinclude:: ./releases/22/22.12.md | ||||||
| .. mdinclude:: ./releases/22/22.9.md | .. mdinclude:: ./releases/22/22.9.md | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| ## Version 23.3.0 | ## Version 23.3.0 🔶 | ||||||
|  |  | ||||||
| ### Features | ### Features | ||||||
| - [#2545](https://github.com/sanic-org/sanic/pull/2545) Standardize init of exceptions for more consistent control of HTTP responses using exceptions | - [#2545](https://github.com/sanic-org/sanic/pull/2545) Standardize init of exceptions for more consistent control of HTTP responses using exceptions | ||||||
|   | |||||||
| @@ -1,33 +0,0 @@ | |||||||
| ## Version 23.6.0  🔶 |  | ||||||
|  |  | ||||||
| ### Features |  | ||||||
| - [#2670](https://github.com/sanic-org/sanic/pull/2670) Increase `KEEP_ALIVE_TIMEOUT` default to 120 seconds |  | ||||||
| - [#2716](https://github.com/sanic-org/sanic/pull/2716) Adding allow route overwrite option in blueprint |  | ||||||
| - [#2724](https://github.com/sanic-org/sanic/pull/2724) and [#2792](https://github.com/sanic-org/sanic/pull/2792) Add a new exception signal for ALL exceptions raised anywhere in application |  | ||||||
| - [#2727](https://github.com/sanic-org/sanic/pull/2727) Add name prefixing to BP groups |  | ||||||
| - [#2754](https://github.com/sanic-org/sanic/pull/2754) Update request type on middleware types |  | ||||||
| - [#2770](https://github.com/sanic-org/sanic/pull/2770) Better exception message on startup time application induced import error |  | ||||||
| - [#2776](https://github.com/sanic-org/sanic/pull/2776) Set multiprocessing start method early |  | ||||||
| - [#2785](https://github.com/sanic-org/sanic/pull/2785) Add custom typing to config and ctx objects |  | ||||||
| - [#2790](https://github.com/sanic-org/sanic/pull/2790) Add `request.client_ip` |  | ||||||
|  |  | ||||||
| ### Bugfixes |  | ||||||
| - [#2728](https://github.com/sanic-org/sanic/pull/2728) Fix traversals for intended results |  | ||||||
| - [#2729](https://github.com/sanic-org/sanic/pull/2729) Handle case when headers argument of ResponseStream constructor is None |  | ||||||
| - [#2737](https://github.com/sanic-org/sanic/pull/2737) Fix type annotation for `JSONREsponse` default content type |  | ||||||
| - [#2740](https://github.com/sanic-org/sanic/pull/2740) Use Sanic's serializer for JSON responses in the Inspector |  | ||||||
| - [#2760](https://github.com/sanic-org/sanic/pull/2760) Support for `Request.get_current` in ASGI mode |  | ||||||
| - [#2773](https://github.com/sanic-org/sanic/pull/2773) Alow Blueprint routes to explicitly define error_format |  | ||||||
| - [#2774](https://github.com/sanic-org/sanic/pull/2774) Resolve headers on different renderers |  | ||||||
| - [#2782](https://github.com/sanic-org/sanic/pull/2782) Resolve pypy compatibility issues |  | ||||||
|  |  | ||||||
| ### Deprecations and Removals |  | ||||||
| - [#2777](https://github.com/sanic-org/sanic/pull/2777) Remove Python 3.7 support |  | ||||||
|  |  | ||||||
| ### Developer infrastructure |  | ||||||
| - [#2766](https://github.com/sanic-org/sanic/pull/2766) Unpin setuptools version |  | ||||||
| - [#2779](https://github.com/sanic-org/sanic/pull/2779) Run keep alive tests in loop to get available port |  | ||||||
|  |  | ||||||
| ### Improved Documentation |  | ||||||
| - [#2741](https://github.com/sanic-org/sanic/pull/2741) Better documentation examples about running Sanic |  | ||||||
| From that list, the items to highlight in the release notes: |  | ||||||
| @@ -1,11 +1,6 @@ | |||||||
| from types import SimpleNamespace |  | ||||||
|  |  | ||||||
| from typing_extensions import TypeAlias |  | ||||||
|  |  | ||||||
| from sanic.__version__ import __version__ | from sanic.__version__ import __version__ | ||||||
| from sanic.app import Sanic | from sanic.app import Sanic | ||||||
| from sanic.blueprints import Blueprint | from sanic.blueprints import Blueprint | ||||||
| from sanic.config import Config |  | ||||||
| from sanic.constants import HTTPMethod | from sanic.constants import HTTPMethod | ||||||
| from sanic.exceptions import ( | from sanic.exceptions import ( | ||||||
|     BadRequest, |     BadRequest, | ||||||
| @@ -37,29 +32,15 @@ from sanic.response import ( | |||||||
| from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket | from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket | ||||||
|  |  | ||||||
|  |  | ||||||
| DefaultSanic: TypeAlias = "Sanic[Config, SimpleNamespace]" |  | ||||||
| """ |  | ||||||
| A type alias for a Sanic app with a default config and namespace. |  | ||||||
| """ |  | ||||||
|  |  | ||||||
| DefaultRequest: TypeAlias = Request[DefaultSanic, SimpleNamespace] |  | ||||||
| """ |  | ||||||
| A type alias for a request with a default Sanic app and namespace. |  | ||||||
| """ |  | ||||||
|  |  | ||||||
| __all__ = ( | __all__ = ( | ||||||
|     "__version__", |     "__version__", | ||||||
|     # Common objects |     # Common objects | ||||||
|     "Sanic", |     "Sanic", | ||||||
|     "Config", |  | ||||||
|     "Blueprint", |     "Blueprint", | ||||||
|     "HTTPMethod", |     "HTTPMethod", | ||||||
|     "HTTPResponse", |     "HTTPResponse", | ||||||
|     "Request", |     "Request", | ||||||
|     "Websocket", |     "Websocket", | ||||||
|     # Common types |  | ||||||
|     "DefaultSanic", |  | ||||||
|     "DefaultRequest", |  | ||||||
|     # Common exceptions |     # Common exceptions | ||||||
|     "BadRequest", |     "BadRequest", | ||||||
|     "ExpectationFailed", |     "ExpectationFailed", | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| __version__ = "23.6.0" | __version__ = "23.3.1" | ||||||
|   | |||||||
							
								
								
									
										215
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										215
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -16,7 +16,7 @@ from asyncio import ( | |||||||
| from asyncio.futures import Future | from asyncio.futures import Future | ||||||
| from collections import defaultdict, deque | from collections import defaultdict, deque | ||||||
| from contextlib import contextmanager, suppress | from contextlib import contextmanager, suppress | ||||||
| from functools import partial, wraps | 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 | ||||||
| @@ -28,11 +28,9 @@ from typing import ( | |||||||
|     AnyStr, |     AnyStr, | ||||||
|     Awaitable, |     Awaitable, | ||||||
|     Callable, |     Callable, | ||||||
|     ClassVar, |  | ||||||
|     Coroutine, |     Coroutine, | ||||||
|     Deque, |     Deque, | ||||||
|     Dict, |     Dict, | ||||||
|     Generic, |  | ||||||
|     Iterable, |     Iterable, | ||||||
|     Iterator, |     Iterator, | ||||||
|     List, |     List, | ||||||
| @@ -42,8 +40,6 @@ from typing import ( | |||||||
|     Type, |     Type, | ||||||
|     TypeVar, |     TypeVar, | ||||||
|     Union, |     Union, | ||||||
|     cast, |  | ||||||
|     overload, |  | ||||||
| ) | ) | ||||||
| from urllib.parse import urlencode, urlunparse | from urllib.parse import urlencode, urlunparse | ||||||
|  |  | ||||||
| @@ -58,12 +54,7 @@ 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, ServerError, 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, _default | ||||||
| from sanic.http import Stage | from sanic.http import Stage | ||||||
| @@ -86,7 +77,7 @@ from sanic.request import Request | |||||||
| from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream | from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream | ||||||
| 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 Event, Signal, SignalRouter | from sanic.signals import Signal, SignalRouter | ||||||
| from sanic.touchup import TouchUp, TouchUpMeta | from sanic.touchup import TouchUp, TouchUpMeta | ||||||
| from sanic.types.shared_ctx import SharedContext | from sanic.types.shared_ctx import SharedContext | ||||||
| from sanic.worker.inspector import Inspector | from sanic.worker.inspector import Inspector | ||||||
| @@ -104,17 +95,8 @@ if TYPE_CHECKING: | |||||||
| if OS_IS_WINDOWS:  # no cov | if OS_IS_WINDOWS:  # no cov | ||||||
|     enable_windows_color_support() |     enable_windows_color_support() | ||||||
|  |  | ||||||
| ctx_type = TypeVar("ctx_type") |  | ||||||
| config_type = TypeVar("config_type", bound=Config) |  | ||||||
|  |  | ||||||
|  | class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta): | ||||||
| class Sanic( |  | ||||||
|     Generic[config_type, ctx_type], |  | ||||||
|     StaticHandleMixin, |  | ||||||
|     BaseSanic, |  | ||||||
|     StartupMixin, |  | ||||||
|     metaclass=TouchUpMeta, |  | ||||||
| ): |  | ||||||
|     """ |     """ | ||||||
|     The main application instance |     The main application instance | ||||||
|     """ |     """ | ||||||
| @@ -169,102 +151,14 @@ class Sanic( | |||||||
|         "websocket_tasks", |         "websocket_tasks", | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     _app_registry: ClassVar[Dict[str, "Sanic"]] = {} |     _app_registry: Dict[str, "Sanic"] = {} | ||||||
|     test_mode: ClassVar[bool] = False |     test_mode = False | ||||||
|  |  | ||||||
|     @overload |  | ||||||
|     def __init__( |  | ||||||
|         self: Sanic[Config, SimpleNamespace], |  | ||||||
|         name: str, |  | ||||||
|         config: None = None, |  | ||||||
|         ctx: None = None, |  | ||||||
|         router: Optional[Router] = None, |  | ||||||
|         signal_router: Optional[SignalRouter] = None, |  | ||||||
|         error_handler: Optional[ErrorHandler] = None, |  | ||||||
|         env_prefix: Optional[str] = SANIC_PREFIX, |  | ||||||
|         request_class: Optional[Type[Request]] = None, |  | ||||||
|         strict_slashes: bool = False, |  | ||||||
|         log_config: Optional[Dict[str, Any]] = None, |  | ||||||
|         configure_logging: bool = True, |  | ||||||
|         dumps: Optional[Callable[..., AnyStr]] = None, |  | ||||||
|         loads: Optional[Callable[..., Any]] = None, |  | ||||||
|         inspector: bool = False, |  | ||||||
|         inspector_class: Optional[Type[Inspector]] = None, |  | ||||||
|         certloader_class: Optional[Type[CertLoader]] = None, |  | ||||||
|     ) -> None: |  | ||||||
|         ... |  | ||||||
|  |  | ||||||
|     @overload |  | ||||||
|     def __init__( |  | ||||||
|         self: Sanic[config_type, SimpleNamespace], |  | ||||||
|         name: str, |  | ||||||
|         config: Optional[config_type] = None, |  | ||||||
|         ctx: None = None, |  | ||||||
|         router: Optional[Router] = None, |  | ||||||
|         signal_router: Optional[SignalRouter] = None, |  | ||||||
|         error_handler: Optional[ErrorHandler] = None, |  | ||||||
|         env_prefix: Optional[str] = SANIC_PREFIX, |  | ||||||
|         request_class: Optional[Type[Request]] = None, |  | ||||||
|         strict_slashes: bool = False, |  | ||||||
|         log_config: Optional[Dict[str, Any]] = None, |  | ||||||
|         configure_logging: bool = True, |  | ||||||
|         dumps: Optional[Callable[..., AnyStr]] = None, |  | ||||||
|         loads: Optional[Callable[..., Any]] = None, |  | ||||||
|         inspector: bool = False, |  | ||||||
|         inspector_class: Optional[Type[Inspector]] = None, |  | ||||||
|         certloader_class: Optional[Type[CertLoader]] = None, |  | ||||||
|     ) -> None: |  | ||||||
|         ... |  | ||||||
|  |  | ||||||
|     @overload |  | ||||||
|     def __init__( |  | ||||||
|         self: Sanic[Config, ctx_type], |  | ||||||
|         name: str, |  | ||||||
|         config: None = None, |  | ||||||
|         ctx: Optional[ctx_type] = None, |  | ||||||
|         router: Optional[Router] = None, |  | ||||||
|         signal_router: Optional[SignalRouter] = None, |  | ||||||
|         error_handler: Optional[ErrorHandler] = None, |  | ||||||
|         env_prefix: Optional[str] = SANIC_PREFIX, |  | ||||||
|         request_class: Optional[Type[Request]] = None, |  | ||||||
|         strict_slashes: bool = False, |  | ||||||
|         log_config: Optional[Dict[str, Any]] = None, |  | ||||||
|         configure_logging: bool = True, |  | ||||||
|         dumps: Optional[Callable[..., AnyStr]] = None, |  | ||||||
|         loads: Optional[Callable[..., Any]] = None, |  | ||||||
|         inspector: bool = False, |  | ||||||
|         inspector_class: Optional[Type[Inspector]] = None, |  | ||||||
|         certloader_class: Optional[Type[CertLoader]] = None, |  | ||||||
|     ) -> None: |  | ||||||
|         ... |  | ||||||
|  |  | ||||||
|     @overload |  | ||||||
|     def __init__( |  | ||||||
|         self: Sanic[config_type, ctx_type], |  | ||||||
|         name: str, |  | ||||||
|         config: Optional[config_type] = None, |  | ||||||
|         ctx: Optional[ctx_type] = None, |  | ||||||
|         router: Optional[Router] = None, |  | ||||||
|         signal_router: Optional[SignalRouter] = None, |  | ||||||
|         error_handler: Optional[ErrorHandler] = None, |  | ||||||
|         env_prefix: Optional[str] = SANIC_PREFIX, |  | ||||||
|         request_class: Optional[Type[Request]] = None, |  | ||||||
|         strict_slashes: bool = False, |  | ||||||
|         log_config: Optional[Dict[str, Any]] = None, |  | ||||||
|         configure_logging: bool = True, |  | ||||||
|         dumps: Optional[Callable[..., AnyStr]] = None, |  | ||||||
|         loads: Optional[Callable[..., Any]] = None, |  | ||||||
|         inspector: bool = False, |  | ||||||
|         inspector_class: Optional[Type[Inspector]] = None, |  | ||||||
|         certloader_class: Optional[Type[CertLoader]] = None, |  | ||||||
|     ) -> None: |  | ||||||
|         ... |  | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         name: str, |         name: Optional[str] = None, | ||||||
|         config: Optional[config_type] = None, |         config: Optional[Config] = None, | ||||||
|         ctx: Optional[ctx_type] = None, |         ctx: Optional[Any] = None, | ||||||
|         router: Optional[Router] = None, |         router: Optional[Router] = None, | ||||||
|         signal_router: Optional[SignalRouter] = None, |         signal_router: Optional[SignalRouter] = None, | ||||||
|         error_handler: Optional[ErrorHandler] = None, |         error_handler: Optional[ErrorHandler] = None, | ||||||
| @@ -292,9 +186,7 @@ class Sanic( | |||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         # First setup config |         # First setup config | ||||||
|         self.config: config_type = cast( |         self.config: Config = config or Config(env_prefix=env_prefix) | ||||||
|             config_type, config or Config(env_prefix=env_prefix) |  | ||||||
|         ) |  | ||||||
|         if inspector: |         if inspector: | ||||||
|             self.config.INSPECTOR = inspector |             self.config.INSPECTOR = inspector | ||||||
|  |  | ||||||
| @@ -318,7 +210,7 @@ class Sanic( | |||||||
|             certloader_class or CertLoader |             certloader_class or CertLoader | ||||||
|         ) |         ) | ||||||
|         self.configure_logging: bool = configure_logging |         self.configure_logging: bool = configure_logging | ||||||
|         self.ctx: ctx_type = cast(ctx_type, 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.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) | ||||||
| @@ -603,19 +495,6 @@ class Sanic( | |||||||
|                 raise NotFound("Could not find signal %s" % event) |                 raise NotFound("Could not find signal %s" % event) | ||||||
|         return await wait_for(signal.ctx.event.wait(), timeout=timeout) |         return await wait_for(signal.ctx.event.wait(), timeout=timeout) | ||||||
|  |  | ||||||
|     def report_exception( |  | ||||||
|         self, handler: Callable[[Sanic, Exception], Coroutine[Any, Any, None]] |  | ||||||
|     ): |  | ||||||
|         @wraps(handler) |  | ||||||
|         async def report(exception: Exception) -> None: |  | ||||||
|             await handler(self, exception) |  | ||||||
|  |  | ||||||
|         self.add_signal( |  | ||||||
|             handler=report, event=Event.SERVER_EXCEPTION_REPORT.value |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         return report |  | ||||||
|  |  | ||||||
|     def enable_websocket(self, enable=True): |     def enable_websocket(self, enable=True): | ||||||
|         """Enable or disable the support for websocket. |         """Enable or disable the support for websocket. | ||||||
|  |  | ||||||
| @@ -887,12 +766,10 @@ class Sanic( | |||||||
|         :raises ServerError: response 500 |         :raises ServerError: response 500 | ||||||
|         """ |         """ | ||||||
|         response = None |         response = None | ||||||
|         if not getattr(exception, "__dispatched__", False): |         await self.dispatch( | ||||||
|             ...  # DO NOT REMOVE THIS LINE. IT IS NEEDED FOR TOUCHUP. |             "server.lifecycle.exception", | ||||||
|             await self.dispatch( |             context={"exception": exception}, | ||||||
|                 "server.exception.report", |         ) | ||||||
|                 context={"exception": exception}, |  | ||||||
|             ) |  | ||||||
|         await self.dispatch( |         await self.dispatch( | ||||||
|             "http.lifecycle.exception", |             "http.lifecycle.exception", | ||||||
|             inline=True, |             inline=True, | ||||||
| @@ -1323,28 +1200,13 @@ class Sanic( | |||||||
|         app, |         app, | ||||||
|         loop, |         loop, | ||||||
|     ): |     ): | ||||||
|         async def do(task): |         if callable(task): | ||||||
|             try: |             try: | ||||||
|                 if callable(task): |                 task = task(app) | ||||||
|                     try: |             except TypeError: | ||||||
|                         task = task(app) |                 task = task() | ||||||
|                     except TypeError: |  | ||||||
|                         task = task() |  | ||||||
|                 if isawaitable(task): |  | ||||||
|                     await task |  | ||||||
|             except CancelledError: |  | ||||||
|                 error_logger.warning( |  | ||||||
|                     f"Task {task} was cancelled before it completed." |  | ||||||
|                 ) |  | ||||||
|                 raise |  | ||||||
|             except Exception as e: |  | ||||||
|                 await app.dispatch( |  | ||||||
|                     "server.exception.report", |  | ||||||
|                     context={"exception": e}, |  | ||||||
|                 ) |  | ||||||
|                 raise |  | ||||||
|  |  | ||||||
|         return do(task) |         return task | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _loop_add_task( |     def _loop_add_task( | ||||||
| @@ -1358,9 +1220,18 @@ class Sanic( | |||||||
|     ) -> Task: |     ) -> Task: | ||||||
|         if not isinstance(task, Future): |         if not isinstance(task, Future): | ||||||
|             prepped = cls._prep_task(task, app, loop) |             prepped = cls._prep_task(task, app, loop) | ||||||
|             task = loop.create_task(prepped, name=name) |             if sys.version_info < (3, 8):  # no cov | ||||||
|  |                 task = loop.create_task(prepped) | ||||||
|  |                 if name: | ||||||
|  |                     error_logger.warning( | ||||||
|  |                         "Cannot set a name for a task when using Python 3.7. " | ||||||
|  |                         "Your task will be created without a name." | ||||||
|  |                     ) | ||||||
|  |                 task.get_name = lambda: name | ||||||
|  |             else: | ||||||
|  |                 task = loop.create_task(prepped, name=name) | ||||||
|  |  | ||||||
|         if name and register: |         if name and register and sys.version_info > (3, 7): | ||||||
|             app._task_registry[name] = task |             app._task_registry[name] = task | ||||||
|  |  | ||||||
|         return task |         return task | ||||||
| @@ -1741,20 +1612,6 @@ class Sanic( | |||||||
|         if hasattr(self, "multiplexer"): |         if hasattr(self, "multiplexer"): | ||||||
|             self.multiplexer.ack() |             self.multiplexer.ack() | ||||||
|  |  | ||||||
|     def set_serving(self, serving: bool) -> None: |  | ||||||
|         """Set the serving state of the application. |  | ||||||
|  |  | ||||||
|         This method is used to set the serving state of the application. |  | ||||||
|         It is used internally by Sanic and should not typically be called |  | ||||||
|         manually. |  | ||||||
|  |  | ||||||
|         Args: |  | ||||||
|             serving (bool): Whether the application is serving. |  | ||||||
|         """ |  | ||||||
|         self.state.is_running = serving |  | ||||||
|         if hasattr(self, "multiplexer"): |  | ||||||
|             self.multiplexer.set_serving(serving) |  | ||||||
|  |  | ||||||
|     async def _server_event( |     async def _server_event( | ||||||
|         self, |         self, | ||||||
|         concern: str, |         concern: str, | ||||||
| @@ -1812,7 +1669,10 @@ class Sanic( | |||||||
|     def inspector(self): |     def inspector(self): | ||||||
|         if environ.get("SANIC_WORKER_PROCESS") or not self._inspector: |         if environ.get("SANIC_WORKER_PROCESS") or not self._inspector: | ||||||
|             raise SanicException( |             raise SanicException( | ||||||
|                 "Can only access the inspector from the main process" |                 "Can only access the inspector from the main process " | ||||||
|  |                 "after main_process_start has run. For example, you most " | ||||||
|  |                 "likely want to use it inside the @app.main_process_ready " | ||||||
|  |                 "event listener." | ||||||
|             ) |             ) | ||||||
|         return self._inspector |         return self._inspector | ||||||
|  |  | ||||||
| @@ -1820,6 +1680,9 @@ class Sanic( | |||||||
|     def manager(self): |     def manager(self): | ||||||
|         if environ.get("SANIC_WORKER_PROCESS") or not self._manager: |         if environ.get("SANIC_WORKER_PROCESS") or not self._manager: | ||||||
|             raise SanicException( |             raise SanicException( | ||||||
|                 "Can only access the manager from the main process" |                 "Can only access the manager from the main process " | ||||||
|  |                 "after main_process_start has run. For example, you most " | ||||||
|  |                 "likely want to use it inside the @app.main_process_ready " | ||||||
|  |                 "event listener." | ||||||
|             ) |             ) | ||||||
|         return self._manager |         return self._manager | ||||||
|   | |||||||
| @@ -111,7 +111,7 @@ class Blueprint(BaseSanic): | |||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         name: str, |         name: str = None, | ||||||
|         url_prefix: Optional[str] = None, |         url_prefix: Optional[str] = None, | ||||||
|         host: Optional[Union[List[str], str]] = None, |         host: Optional[Union[List[str], str]] = None, | ||||||
|         version: Optional[Union[int, str, float]] = None, |         version: Optional[Union[int, str, float]] = None, | ||||||
| @@ -319,10 +319,6 @@ class Blueprint(BaseSanic): | |||||||
|             # 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) | ||||||
|  |  | ||||||
|             route_error_format = ( |  | ||||||
|                 future.error_format if future.error_format else error_format |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|             version_prefix = self.version_prefix |             version_prefix = self.version_prefix | ||||||
|             for prefix in ( |             for prefix in ( | ||||||
|                 future.version_prefix, |                 future.version_prefix, | ||||||
| @@ -362,7 +358,7 @@ class Blueprint(BaseSanic): | |||||||
|                 future.unquote, |                 future.unquote, | ||||||
|                 future.static, |                 future.static, | ||||||
|                 version_prefix, |                 version_prefix, | ||||||
|                 route_error_format, |                 error_format, | ||||||
|                 future.route_context, |                 future.route_context, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -43,14 +43,14 @@ DEFAULT_CONFIG = { | |||||||
|     "DEPRECATION_FILTER": "once", |     "DEPRECATION_FILTER": "once", | ||||||
|     "FORWARDED_FOR_HEADER": "X-Forwarded-For", |     "FORWARDED_FOR_HEADER": "X-Forwarded-For", | ||||||
|     "FORWARDED_SECRET": None, |     "FORWARDED_SECRET": None, | ||||||
|     "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, |     "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0,  # 15 sec | ||||||
|     "INSPECTOR": False, |     "INSPECTOR": False, | ||||||
|     "INSPECTOR_HOST": "localhost", |     "INSPECTOR_HOST": "localhost", | ||||||
|     "INSPECTOR_PORT": 6457, |     "INSPECTOR_PORT": 6457, | ||||||
|     "INSPECTOR_TLS_KEY": _default, |     "INSPECTOR_TLS_KEY": _default, | ||||||
|     "INSPECTOR_TLS_CERT": _default, |     "INSPECTOR_TLS_CERT": _default, | ||||||
|     "INSPECTOR_API_KEY": "", |     "INSPECTOR_API_KEY": "", | ||||||
|     "KEEP_ALIVE_TIMEOUT": 120, |     "KEEP_ALIVE_TIMEOUT": 5,  # 5 seconds | ||||||
|     "KEEP_ALIVE": True, |     "KEEP_ALIVE": True, | ||||||
|     "LOCAL_CERT_CREATOR": LocalCertCreator.AUTO, |     "LOCAL_CERT_CREATOR": LocalCertCreator.AUTO, | ||||||
|     "LOCAL_TLS_KEY": _default, |     "LOCAL_TLS_KEY": _default, | ||||||
| @@ -61,16 +61,16 @@ DEFAULT_CONFIG = { | |||||||
|     "NOISY_EXCEPTIONS": False, |     "NOISY_EXCEPTIONS": False, | ||||||
|     "PROXIES_COUNT": None, |     "PROXIES_COUNT": None, | ||||||
|     "REAL_IP_HEADER": None, |     "REAL_IP_HEADER": None, | ||||||
|     "REQUEST_BUFFER_SIZE": 65536, |     "REQUEST_BUFFER_SIZE": 65536,  # 64 KiB | ||||||
|     "REQUEST_MAX_HEADER_SIZE": 8192,  # Cannot exceed 16384 |     "REQUEST_MAX_HEADER_SIZE": 8192,  # 8 KiB, but cannot exceed 16384 | ||||||
|     "REQUEST_ID_HEADER": "X-Request-ID", |     "REQUEST_ID_HEADER": "X-Request-ID", | ||||||
|     "REQUEST_MAX_SIZE": 100_000_000, |     "REQUEST_MAX_SIZE": 100000000,  # 100 megabytes | ||||||
|     "REQUEST_TIMEOUT": 60, |     "REQUEST_TIMEOUT": 60,  # 60 seconds | ||||||
|     "RESPONSE_TIMEOUT": 60, |     "RESPONSE_TIMEOUT": 60,  # 60 seconds | ||||||
|     "TLS_CERT_PASSWORD": "", |     "TLS_CERT_PASSWORD": "", | ||||||
|     "TOUCHUP": _default, |     "TOUCHUP": _default, | ||||||
|     "USE_UVLOOP": _default, |     "USE_UVLOOP": _default, | ||||||
|     "WEBSOCKET_MAX_SIZE": 2**20,  # 1 MiB |     "WEBSOCKET_MAX_SIZE": 2**20,  # 1 megabyte | ||||||
|     "WEBSOCKET_PING_INTERVAL": 20, |     "WEBSOCKET_PING_INTERVAL": 20, | ||||||
|     "WEBSOCKET_PING_TIMEOUT": 20, |     "WEBSOCKET_PING_TIMEOUT": 20, | ||||||
| } | } | ||||||
|   | |||||||
| @@ -312,7 +312,7 @@ def exception_response( | |||||||
|     debug: bool, |     debug: bool, | ||||||
|     fallback: str, |     fallback: str, | ||||||
|     base: t.Type[BaseRenderer], |     base: t.Type[BaseRenderer], | ||||||
|     renderer: t.Optional[t.Type[BaseRenderer]] = None, |     renderer: t.Type[t.Optional[BaseRenderer]] = None, | ||||||
| ) -> HTTPResponse: | ) -> HTTPResponse: | ||||||
|     """ |     """ | ||||||
|     Render a response for the default FALLBACK exception handler. |     Render a response for the default FALLBACK exception handler. | ||||||
|   | |||||||
| @@ -90,7 +90,7 @@ class SanicException(Exception): | |||||||
|  |  | ||||||
|         super().__init__(message) |         super().__init__(message) | ||||||
|  |  | ||||||
|         self.status_code = status_code or self.status_code |         self.status_code = status_code | ||||||
|         self.quiet = quiet |         self.quiet = quiet | ||||||
|         self.headers = headers |         self.headers = headers | ||||||
|  |  | ||||||
|   | |||||||
| @@ -436,7 +436,7 @@ def format_http1_response(status: int, headers: HeaderBytesIterable) -> bytes: | |||||||
|  |  | ||||||
| def parse_credentials( | def parse_credentials( | ||||||
|     header: Optional[str], |     header: Optional[str], | ||||||
|     prefixes: Optional[Union[List, Tuple, Set]] = None, |     prefixes: Union[List, Tuple, Set] = None, | ||||||
| ) -> Tuple[Optional[str], Optional[str]]: | ) -> Tuple[Optional[str], Optional[str]]: | ||||||
|     """Parses any header with the aim to retrieve any credentials from it.""" |     """Parses any header with the aim to retrieve any credentials from it.""" | ||||||
|     if not prefixes or not isinstance(prefixes, (list, tuple, set)): |     if not prefixes or not isinstance(prefixes, (list, tuple, set)): | ||||||
|   | |||||||
| @@ -38,15 +38,3 @@ class ExceptionMixin(metaclass=SanicMeta): | |||||||
|             return handler |             return handler | ||||||
|  |  | ||||||
|         return decorator |         return decorator | ||||||
|  |  | ||||||
|     def all_exceptions(self, handler): |  | ||||||
|         """ |  | ||||||
|         This method enables the process of creating a global exception |  | ||||||
|         handler for the current blueprint under question. |  | ||||||
|  |  | ||||||
|         :param handler: A coroutine function to handle exceptions |  | ||||||
|  |  | ||||||
|         :return a decorated method to handle global exceptions for any |  | ||||||
|             route registered under this blueprint. |  | ||||||
|         """ |  | ||||||
|         return self.exception(Exception)(handler) |  | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ from typing import Any, Callable, Dict, Optional, Set, Union | |||||||
| from sanic.base.meta import SanicMeta | from sanic.base.meta import SanicMeta | ||||||
| from sanic.models.futures import FutureSignal | from sanic.models.futures import FutureSignal | ||||||
| from sanic.models.handler_types import SignalHandler | from sanic.models.handler_types import SignalHandler | ||||||
| from sanic.signals import Event, Signal | from sanic.signals import Signal | ||||||
| from sanic.types import HashableDict | from sanic.types import HashableDict | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -80,9 +80,3 @@ class SignalMixin(metaclass=SanicMeta): | |||||||
|  |  | ||||||
|     def event(self, event: str): |     def event(self, event: str): | ||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
|  |  | ||||||
|     def catch_exception(self, handler): |  | ||||||
|         async def signal_handler(exception: Exception): |  | ||||||
|             await handler(self, exception) |  | ||||||
|  |  | ||||||
|         self.signal(Event.SERVER_LIFECYCLE_EXCEPTION)(signal_handler) |  | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ from __future__ import annotations | |||||||
| import os | import os | ||||||
| import platform | import platform | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from asyncio import ( | from asyncio import ( | ||||||
|     AbstractEventLoop, |     AbstractEventLoop, | ||||||
|     CancelledError, |     CancelledError, | ||||||
| @@ -15,13 +16,7 @@ from asyncio import ( | |||||||
| from contextlib import suppress | from contextlib import suppress | ||||||
| from functools import partial | from functools import partial | ||||||
| from importlib import import_module | from importlib import import_module | ||||||
| from multiprocessing import ( | from multiprocessing import Manager, Pipe, get_context | ||||||
|     Manager, |  | ||||||
|     Pipe, |  | ||||||
|     get_context, |  | ||||||
|     get_start_method, |  | ||||||
|     set_start_method, |  | ||||||
| ) |  | ||||||
| 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 SHUT_RDWR, socket | ||||||
| @@ -30,7 +25,6 @@ from typing import ( | |||||||
|     TYPE_CHECKING, |     TYPE_CHECKING, | ||||||
|     Any, |     Any, | ||||||
|     Callable, |     Callable, | ||||||
|     ClassVar, |  | ||||||
|     Dict, |     Dict, | ||||||
|     List, |     List, | ||||||
|     Mapping, |     Mapping, | ||||||
| @@ -64,13 +58,13 @@ 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 | from sanic.server.runners import serve | ||||||
| from sanic.server.socket import configure_socket, remove_unix_socket | from sanic.server.socket import configure_socket, remove_unix_socket | ||||||
| from sanic.worker.constants import ProcessState |  | ||||||
| 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 | ||||||
| from sanic.worker.reloader import Reloader | from sanic.worker.reloader import Reloader | ||||||
| from sanic.worker.serve import worker_serve | from sanic.worker.serve import worker_serve | ||||||
|  |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from sanic import Sanic |     from sanic import Sanic | ||||||
|     from sanic.application.state import ApplicationState |     from sanic.application.state import ApplicationState | ||||||
| @@ -87,17 +81,13 @@ else:  # no cov | |||||||
|  |  | ||||||
|  |  | ||||||
| class StartupMixin(metaclass=SanicMeta): | class StartupMixin(metaclass=SanicMeta): | ||||||
|     _app_registry: ClassVar[Dict[str, Sanic]] |     _app_registry: Dict[str, Sanic] | ||||||
|  |  | ||||||
|     config: Config |     config: Config | ||||||
|     listeners: Dict[str, List[ListenerType[Any]]] |     listeners: Dict[str, List[ListenerType[Any]]] | ||||||
|     state: ApplicationState |     state: ApplicationState | ||||||
|     websocket_enabled: bool |     websocket_enabled: bool | ||||||
|     multiplexer: WorkerMultiplexer |     multiplexer: WorkerMultiplexer | ||||||
|  |     start_method: StartMethod = _default | ||||||
|     test_mode: ClassVar[bool] |  | ||||||
|     start_method: ClassVar[StartMethod] = _default |  | ||||||
|     START_METHOD_SET: ClassVar[bool] = False |  | ||||||
|  |  | ||||||
|     def setup_loop(self): |     def setup_loop(self): | ||||||
|         if not self.asgi: |         if not self.asgi: | ||||||
| @@ -701,26 +691,11 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|             else "spawn" |             else "spawn" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def _set_startup_method(cls) -> None: |  | ||||||
|         if cls.START_METHOD_SET and not cls.test_mode: |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         method = cls._get_startup_method() |  | ||||||
|         set_start_method(method, force=cls.test_mode) |  | ||||||
|         cls.START_METHOD_SET = True |  | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _get_context(cls) -> BaseContext: |     def _get_context(cls) -> BaseContext: | ||||||
|         method = cls._get_startup_method() |         method = cls._get_startup_method() | ||||||
|         logger.debug("Creating multiprocessing context using '%s'", method) |         logger.debug("Creating multiprocessing context using '%s'", method) | ||||||
|         actual = get_start_method() |         return get_context(method) | ||||||
|         if method != actual: |  | ||||||
|             raise RuntimeError( |  | ||||||
|                 f"Start method '{method}' was requested, but '{actual}' " |  | ||||||
|                 "was actually set." |  | ||||||
|             ) |  | ||||||
|         return get_context() |  | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def serve( |     def serve( | ||||||
| @@ -730,7 +705,6 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|         app_loader: Optional[AppLoader] = None, |         app_loader: Optional[AppLoader] = None, | ||||||
|         factory: Optional[Callable[[], Sanic]] = None, |         factory: Optional[Callable[[], Sanic]] = None, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         cls._set_startup_method() |  | ||||||
|         os.environ["SANIC_MOTD_OUTPUT"] = "true" |         os.environ["SANIC_MOTD_OUTPUT"] = "true" | ||||||
|         apps = list(cls._app_registry.values()) |         apps = list(cls._app_registry.values()) | ||||||
|         if factory: |         if factory: | ||||||
| @@ -892,6 +866,7 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|                 app.router.reset() |                 app.router.reset() | ||||||
|                 app.signal_router.reset() |                 app.signal_router.reset() | ||||||
|  |  | ||||||
|  |             sync_manager.shutdown() | ||||||
|             for sock in socks: |             for sock in socks: | ||||||
|                 try: |                 try: | ||||||
|                     sock.shutdown(SHUT_RDWR) |                     sock.shutdown(SHUT_RDWR) | ||||||
| @@ -903,33 +878,12 @@ class StartupMixin(metaclass=SanicMeta): | |||||||
|             loop.close() |             loop.close() | ||||||
|             cls._cleanup_env_vars() |             cls._cleanup_env_vars() | ||||||
|             cls._cleanup_apps() |             cls._cleanup_apps() | ||||||
|  |  | ||||||
|             from time import sleep |  | ||||||
|  |  | ||||||
|             limit = 100 |  | ||||||
|             while cls._get_process_states(worker_state): |  | ||||||
|                 sleep(0.1) |  | ||||||
|                 limit -= 1 |  | ||||||
|                 if limit <= 0: |  | ||||||
|                     error_logger.warning( |  | ||||||
|                         "Worker shutdown timed out. " |  | ||||||
|                         "Some processes may still be running." |  | ||||||
|                     ) |  | ||||||
|                     break |  | ||||||
|             sync_manager.shutdown() |  | ||||||
|             unix = kwargs.get("unix") |             unix = kwargs.get("unix") | ||||||
|             if unix: |             if unix: | ||||||
|                 remove_unix_socket(unix) |                 remove_unix_socket(unix) | ||||||
|             logger.info("Goodbye.") |  | ||||||
|         if exit_code: |         if exit_code: | ||||||
|             os._exit(exit_code) |             os._exit(exit_code) | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def _get_process_states(worker_state) -> List[str]: |  | ||||||
|         return [ |  | ||||||
|             state for s in worker_state.values() if (state := s.get("state")) |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def serve_single(cls, primary: Optional[Sanic] = None) -> None: |     def serve_single(cls, primary: Optional[Sanic] = None) -> None: | ||||||
|         os.environ["SANIC_MOTD_OUTPUT"] = "true" |         os.environ["SANIC_MOTD_OUTPUT"] = "true" | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import asyncio | import asyncio | ||||||
|  | import sys | ||||||
|  |  | ||||||
| from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union | from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union | ||||||
|  |  | ||||||
| @@ -15,10 +16,20 @@ ASGIReceive = Callable[[], Awaitable[ASGIMessage]] | |||||||
|  |  | ||||||
| class MockProtocol:  # no cov | class MockProtocol:  # no cov | ||||||
|     def __init__(self, transport: "MockTransport", loop): |     def __init__(self, transport: "MockTransport", loop): | ||||||
|  |         # This should be refactored when < 3.8 support is dropped | ||||||
|         self.transport = transport |         self.transport = transport | ||||||
|         self._not_paused = asyncio.Event() |         # Fixup for 3.8+; Sanic still supports 3.7 where loop is required | ||||||
|         self._not_paused.set() |         loop = loop if sys.version_info[:2] < (3, 8) else None | ||||||
|         self._complete = asyncio.Event() |         # Optional in 3.9, necessary in 3.10 because the parameter "loop" | ||||||
|  |         # was completely removed | ||||||
|  |         if not loop: | ||||||
|  |             self._not_paused = asyncio.Event() | ||||||
|  |             self._not_paused.set() | ||||||
|  |             self._complete = asyncio.Event() | ||||||
|  |         else: | ||||||
|  |             self._not_paused = asyncio.Event(loop=loop) | ||||||
|  |             self._not_paused.set() | ||||||
|  |             self._complete = asyncio.Event(loop=loop) | ||||||
|  |  | ||||||
|     def pause_writing(self) -> None: |     def pause_writing(self) -> None: | ||||||
|         self._not_paused.clear() |         self._not_paused.clear() | ||||||
|   | |||||||
| @@ -2,13 +2,11 @@ from __future__ import annotations | |||||||
|  |  | ||||||
| from contextvars import ContextVar | from contextvars import ContextVar | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
| from types import SimpleNamespace |  | ||||||
| from typing import ( | from typing import ( | ||||||
|     TYPE_CHECKING, |     TYPE_CHECKING, | ||||||
|     Any, |     Any, | ||||||
|     DefaultDict, |     DefaultDict, | ||||||
|     Dict, |     Dict, | ||||||
|     Generic, |  | ||||||
|     List, |     List, | ||||||
|     Optional, |     Optional, | ||||||
|     Tuple, |     Tuple, | ||||||
| @@ -17,7 +15,6 @@ from typing import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| from sanic_routing.route import Route | from sanic_routing.route import Route | ||||||
| from typing_extensions import TypeVar |  | ||||||
|  |  | ||||||
| from sanic.http.constants import HTTP  # type: ignore | from sanic.http.constants import HTTP  # type: ignore | ||||||
| from sanic.http.stream import Stream | from sanic.http.stream import Stream | ||||||
| @@ -26,13 +23,13 @@ from sanic.models.http_types import Credentials | |||||||
|  |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from sanic.app import Sanic |  | ||||||
|     from sanic.config import Config |  | ||||||
|     from sanic.server import ConnInfo |     from sanic.server import ConnInfo | ||||||
|  |     from sanic.app import Sanic | ||||||
|  |  | ||||||
| import uuid | import uuid | ||||||
|  |  | ||||||
| from collections import defaultdict | from collections import defaultdict | ||||||
|  | from types import SimpleNamespace | ||||||
| from urllib.parse import parse_qs, parse_qsl, urlunparse | from urllib.parse import parse_qs, parse_qsl, urlunparse | ||||||
|  |  | ||||||
| from httptools import parse_url | from httptools import parse_url | ||||||
| @@ -71,21 +68,8 @@ try: | |||||||
| except ImportError: | except ImportError: | ||||||
|     from json import loads as json_loads  # type: ignore |     from json import loads as json_loads  # type: ignore | ||||||
|  |  | ||||||
| if TYPE_CHECKING: |  | ||||||
|     # The default argument of TypeVar is proposed to be added in Python 3.13 |  | ||||||
|     # by PEP 696 (https://www.python.org/dev/peps/pep-0696/). |  | ||||||
|     # Therefore, we use typing_extensions.TypeVar for compatibility. |  | ||||||
|     # For more information, see: |  | ||||||
|     # https://discuss.python.org/t/pep-696-type-defaults-for-typevarlikes |  | ||||||
|     sanic_type = TypeVar( |  | ||||||
|         "sanic_type", bound=Sanic, default=Sanic[Config, SimpleNamespace] |  | ||||||
|     ) |  | ||||||
| else: |  | ||||||
|     sanic_type = TypeVar("sanic_type") |  | ||||||
| ctx_type = TypeVar("ctx_type") |  | ||||||
|  |  | ||||||
|  | class Request: | ||||||
| class Request(Generic[sanic_type, ctx_type]): |  | ||||||
|     """ |     """ | ||||||
|     Properties of an HTTP request such as URL, headers, etc. |     Properties of an HTTP request such as URL, headers, etc. | ||||||
|     """ |     """ | ||||||
| @@ -96,7 +80,6 @@ class Request(Generic[sanic_type, ctx_type]): | |||||||
|     __slots__ = ( |     __slots__ = ( | ||||||
|         "__weakref__", |         "__weakref__", | ||||||
|         "_cookies", |         "_cookies", | ||||||
|         "_ctx", |  | ||||||
|         "_id", |         "_id", | ||||||
|         "_ip", |         "_ip", | ||||||
|         "_parsed_url", |         "_parsed_url", | ||||||
| @@ -113,6 +96,7 @@ class Request(Generic[sanic_type, ctx_type]): | |||||||
|         "app", |         "app", | ||||||
|         "body", |         "body", | ||||||
|         "conn_info", |         "conn_info", | ||||||
|  |         "ctx", | ||||||
|         "head", |         "head", | ||||||
|         "headers", |         "headers", | ||||||
|         "method", |         "method", | ||||||
| @@ -141,7 +125,7 @@ class Request(Generic[sanic_type, ctx_type]): | |||||||
|         version: str, |         version: str, | ||||||
|         method: str, |         method: str, | ||||||
|         transport: TransportProtocol, |         transport: TransportProtocol, | ||||||
|         app: sanic_type, |         app: Sanic, | ||||||
|         head: bytes = b"", |         head: bytes = b"", | ||||||
|         stream_id: int = 0, |         stream_id: int = 0, | ||||||
|     ): |     ): | ||||||
| @@ -165,7 +149,7 @@ class Request(Generic[sanic_type, ctx_type]): | |||||||
|         # Init but do not inhale |         # Init but do not inhale | ||||||
|         self.body = b"" |         self.body = b"" | ||||||
|         self.conn_info: Optional[ConnInfo] = None |         self.conn_info: Optional[ConnInfo] = None | ||||||
|         self._ctx: Optional[ctx_type] = None |         self.ctx = SimpleNamespace() | ||||||
|         self.parsed_accept: Optional[AcceptList] = None |         self.parsed_accept: Optional[AcceptList] = None | ||||||
|         self.parsed_args: DefaultDict[ |         self.parsed_args: DefaultDict[ | ||||||
|             Tuple[bool, bool, str, str], RequestParameters |             Tuple[bool, bool, str, str], RequestParameters | ||||||
| @@ -192,10 +176,6 @@ class Request(Generic[sanic_type, ctx_type]): | |||||||
|         class_name = self.__class__.__name__ |         class_name = self.__class__.__name__ | ||||||
|         return f"<{class_name}: {self.method} {self.path}>" |         return f"<{class_name}: {self.method} {self.path}>" | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def make_context() -> ctx_type: |  | ||||||
|         return cast(ctx_type, SimpleNamespace()) |  | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def get_current(cls) -> Request: |     def get_current(cls) -> Request: | ||||||
|         """ |         """ | ||||||
| @@ -225,15 +205,6 @@ class Request(Generic[sanic_type, ctx_type]): | |||||||
|     def generate_id(*_): |     def generate_id(*_): | ||||||
|         return uuid.uuid4() |         return uuid.uuid4() | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def ctx(self) -> ctx_type: |  | ||||||
|         """ |  | ||||||
|         :return: The current request context |  | ||||||
|         """ |  | ||||||
|         if not self._ctx: |  | ||||||
|             self._ctx = self.make_context() |  | ||||||
|         return self._ctx |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def stream_id(self): |     def stream_id(self): | ||||||
|         """ |         """ | ||||||
| @@ -838,31 +809,19 @@ class Request(Generic[sanic_type, ctx_type]): | |||||||
|     @property |     @property | ||||||
|     def remote_addr(self) -> str: |     def remote_addr(self) -> str: | ||||||
|         """ |         """ | ||||||
|         Client IP address, if available from proxy. |         Client IP address, if available. | ||||||
|  |         1. proxied remote address `self.forwarded['for']` | ||||||
|  |         2. local remote address `self.ip` | ||||||
|  |  | ||||||
|         :return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string |         :return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string | ||||||
|         :rtype: str |         :rtype: str | ||||||
|         """ |         """ | ||||||
|         if not hasattr(self, "_remote_addr"): |         if not hasattr(self, "_remote_addr"): | ||||||
|             self._remote_addr = str(self.forwarded.get("for", "")) |             self._remote_addr = str( | ||||||
|  |                 self.forwarded.get("for", "") | ||||||
|  |             )  # or self.ip | ||||||
|         return self._remote_addr |         return self._remote_addr | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def client_ip(self) -> str: |  | ||||||
|         """ |  | ||||||
|         Client IP address. |  | ||||||
|         1. proxied remote address `self.forwarded['for']` |  | ||||||
|         2. local peer address `self.ip` |  | ||||||
|  |  | ||||||
|         New in Sanic 23.6. Prefer this over `remote_addr` for determining the |  | ||||||
|         client address regardless of whether the service runs behind a proxy |  | ||||||
|         or not (proxy deployment needs separate configuration). |  | ||||||
|  |  | ||||||
|         :return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string |  | ||||||
|         :rtype: str |  | ||||||
|         """ |  | ||||||
|         return self.remote_addr or self.ip |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def scheme(self) -> str: |     def scheme(self) -> str: | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -75,7 +75,7 @@ class Router(BaseRouter): | |||||||
|         strict_slashes: bool = False, |         strict_slashes: bool = False, | ||||||
|         stream: bool = False, |         stream: bool = False, | ||||||
|         ignore_body: bool = False, |         ignore_body: bool = False, | ||||||
|         version: Optional[Union[str, float, int]] = None, |         version: Union[str, float, int] = None, | ||||||
|         name: Optional[str] = None, |         name: Optional[str] = None, | ||||||
|         unquote: bool = False, |         unquote: bool = False, | ||||||
|         static: bool = False, |         static: bool = False, | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import sys | ||||||
|  |  | ||||||
| from ssl import SSLContext | from ssl import SSLContext | ||||||
| from typing import TYPE_CHECKING, Dict, Optional, Type, Union | from typing import TYPE_CHECKING, Dict, Optional, Type, Union | ||||||
|  |  | ||||||
| @@ -122,15 +124,17 @@ def _setup_system_signals( | |||||||
|     register_sys_signals: bool, |     register_sys_signals: bool, | ||||||
|     loop: asyncio.AbstractEventLoop, |     loop: asyncio.AbstractEventLoop, | ||||||
| ) -> None:  # no cov | ) -> None:  # no cov | ||||||
|     signal_func(SIGINT, SIG_IGN) |     # Ignore SIGINT when run_multiple | ||||||
|     signal_func(SIGTERM, SIG_IGN) |     if run_multiple: | ||||||
|     os.environ["SANIC_WORKER_PROCESS"] = "true" |         signal_func(SIGINT, SIG_IGN) | ||||||
|  |         os.environ["SANIC_WORKER_PROCESS"] = "true" | ||||||
|  |  | ||||||
|     # Register signals for graceful termination |     # Register signals for graceful termination | ||||||
|     if register_sys_signals: |     if register_sys_signals: | ||||||
|         if OS_IS_WINDOWS: |         if OS_IS_WINDOWS: | ||||||
|             ctrlc_workaround_for_windows(app) |             ctrlc_workaround_for_windows(app) | ||||||
|         else: |         else: | ||||||
|             for _signal in [SIGINT, SIGTERM]: |             for _signal in [SIGTERM] if run_multiple else [SIGINT, SIGTERM]: | ||||||
|                 loop.add_signal_handler( |                 loop.add_signal_handler( | ||||||
|                     _signal, partial(app.stop, terminate=False) |                     _signal, partial(app.stop, terminate=False) | ||||||
|                 ) |                 ) | ||||||
| @@ -141,6 +145,8 @@ def _run_server_forever(loop, before_stop, after_stop, cleanup, unix): | |||||||
|     try: |     try: | ||||||
|         server_logger.info("Starting worker [%s]", pid) |         server_logger.info("Starting worker [%s]", pid) | ||||||
|         loop.run_forever() |         loop.run_forever() | ||||||
|  |     except KeyboardInterrupt: | ||||||
|  |         pass | ||||||
|     finally: |     finally: | ||||||
|         server_logger.info("Stopping worker [%s]", pid) |         server_logger.info("Stopping worker [%s]", pid) | ||||||
|  |  | ||||||
| @@ -152,7 +158,6 @@ def _run_server_forever(loop, before_stop, after_stop, cleanup, unix): | |||||||
|         loop.run_until_complete(after_stop()) |         loop.run_until_complete(after_stop()) | ||||||
|         remove_unix_socket(unix) |         remove_unix_socket(unix) | ||||||
|         loop.close() |         loop.close() | ||||||
|         server_logger.info("Worker complete [%s]", pid) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def _serve_http_1( | def _serve_http_1( | ||||||
| @@ -246,7 +251,8 @@ def _serve_http_1( | |||||||
|             loop.run_until_complete(asyncio.sleep(0.1)) |             loop.run_until_complete(asyncio.sleep(0.1)) | ||||||
|             start_shutdown = start_shutdown + 0.1 |             start_shutdown = start_shutdown + 0.1 | ||||||
|  |  | ||||||
|         app.shutdown_tasks(graceful - start_shutdown) |         if sys.version_info > (3, 7): | ||||||
|  |             app.shutdown_tasks(graceful - start_shutdown) | ||||||
|  |  | ||||||
|         # Force close non-idle connection after waiting for |         # Force close non-idle connection after waiting for | ||||||
|         # graceful_shutdown_timeout |         # graceful_shutdown_timeout | ||||||
| @@ -256,11 +262,8 @@ def _serve_http_1( | |||||||
|             else: |             else: | ||||||
|                 conn.abort() |                 conn.abort() | ||||||
|  |  | ||||||
|         app.set_serving(False) |  | ||||||
|  |  | ||||||
|     _setup_system_signals(app, run_multiple, register_sys_signals, loop) |     _setup_system_signals(app, run_multiple, register_sys_signals, loop) | ||||||
|     loop.run_until_complete(app._server_event("init", "after")) |     loop.run_until_complete(app._server_event("init", "after")) | ||||||
|     app.set_serving(True) |  | ||||||
|     _run_server_forever( |     _run_server_forever( | ||||||
|         loop, |         loop, | ||||||
|         partial(app._server_event, "shutdown", "before"), |         partial(app._server_event, "shutdown", "before"), | ||||||
|   | |||||||
| @@ -96,7 +96,6 @@ class WebsocketFrameAssembler: | |||||||
|         If ``timeout`` is set and elapses before a complete message is |         If ``timeout`` is set and elapses before a complete message is | ||||||
|         received, :meth:`get` returns ``None``. |         received, :meth:`get` returns ``None``. | ||||||
|         """ |         """ | ||||||
|         completed: bool |  | ||||||
|         async with self.read_mutex: |         async with self.read_mutex: | ||||||
|             if timeout is not None and timeout <= 0: |             if timeout is not None and timeout <= 0: | ||||||
|                 if not self.message_complete.is_set(): |                 if not self.message_complete.is_set(): | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ from websockets.frames import Frame, Opcode | |||||||
|  |  | ||||||
|  |  | ||||||
| try:  # websockets < 11.0 | try:  # websockets < 11.0 | ||||||
|     from websockets.connection import Event, State  # type: ignore |     from websockets.connection import Event, State | ||||||
|     from websockets.server import ServerConnection as ServerProtocol |     from websockets.server import ServerConnection as ServerProtocol | ||||||
| except ImportError:  # websockets >= 11.0 | except ImportError:  # websockets >= 11.0 | ||||||
|     from websockets.protocol import Event, State  # type: ignore |     from websockets.protocol import Event, State  # type: ignore | ||||||
|   | |||||||
| @@ -16,11 +16,11 @@ from sanic.models.handler_types import SignalHandler | |||||||
|  |  | ||||||
|  |  | ||||||
| class Event(Enum): | class Event(Enum): | ||||||
|     SERVER_EXCEPTION_REPORT = "server.exception.report" |  | ||||||
|     SERVER_INIT_AFTER = "server.init.after" |     SERVER_INIT_AFTER = "server.init.after" | ||||||
|     SERVER_INIT_BEFORE = "server.init.before" |     SERVER_INIT_BEFORE = "server.init.before" | ||||||
|     SERVER_SHUTDOWN_AFTER = "server.shutdown.after" |     SERVER_SHUTDOWN_AFTER = "server.shutdown.after" | ||||||
|     SERVER_SHUTDOWN_BEFORE = "server.shutdown.before" |     SERVER_SHUTDOWN_BEFORE = "server.shutdown.before" | ||||||
|  |     SERVER_LIFECYCLE_EXCEPTION = "server.lifecycle.exception" | ||||||
|     HTTP_LIFECYCLE_BEGIN = "http.lifecycle.begin" |     HTTP_LIFECYCLE_BEGIN = "http.lifecycle.begin" | ||||||
|     HTTP_LIFECYCLE_COMPLETE = "http.lifecycle.complete" |     HTTP_LIFECYCLE_COMPLETE = "http.lifecycle.complete" | ||||||
|     HTTP_LIFECYCLE_EXCEPTION = "http.lifecycle.exception" |     HTTP_LIFECYCLE_EXCEPTION = "http.lifecycle.exception" | ||||||
| @@ -40,11 +40,11 @@ class Event(Enum): | |||||||
|  |  | ||||||
| RESERVED_NAMESPACES = { | RESERVED_NAMESPACES = { | ||||||
|     "server": ( |     "server": ( | ||||||
|         Event.SERVER_EXCEPTION_REPORT.value, |  | ||||||
|         Event.SERVER_INIT_AFTER.value, |         Event.SERVER_INIT_AFTER.value, | ||||||
|         Event.SERVER_INIT_BEFORE.value, |         Event.SERVER_INIT_BEFORE.value, | ||||||
|         Event.SERVER_SHUTDOWN_AFTER.value, |         Event.SERVER_SHUTDOWN_AFTER.value, | ||||||
|         Event.SERVER_SHUTDOWN_BEFORE.value, |         Event.SERVER_SHUTDOWN_BEFORE.value, | ||||||
|  |         Event.SERVER_LIFECYCLE_EXCEPTION.value, | ||||||
|     ), |     ), | ||||||
|     "http": ( |     "http": ( | ||||||
|         Event.HTTP_LIFECYCLE_BEGIN.value, |         Event.HTTP_LIFECYCLE_BEGIN.value, | ||||||
| @@ -174,12 +174,11 @@ class SignalRouter(BaseRouter): | |||||||
|             if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1: |             if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1: | ||||||
|                 error_logger.exception(e) |                 error_logger.exception(e) | ||||||
|  |  | ||||||
|             if event != Event.SERVER_EXCEPTION_REPORT.value: |             if event != Event.SERVER_LIFECYCLE_EXCEPTION.value: | ||||||
|                 await self.dispatch( |                 await self.dispatch( | ||||||
|                     Event.SERVER_EXCEPTION_REPORT.value, |                     Event.SERVER_LIFECYCLE_EXCEPTION.value, | ||||||
|                     context={"exception": e}, |                     context={"exception": e}, | ||||||
|                 ) |                 ) | ||||||
|                 setattr(e, "__dispatched__", True) |  | ||||||
|             raise e |             raise e | ||||||
|         finally: |         finally: | ||||||
|             for signal_event in events: |             for signal_event in events: | ||||||
| @@ -230,6 +229,14 @@ 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.__trigger__ = trigger  # type: ignore | ||||||
|  |         except AttributeError: | ||||||
|  |             pass | ||||||
|  |  | ||||||
|         signal = super().add( |         signal = super().add( | ||||||
|             event, |             event, | ||||||
|             handler, |             handler, | ||||||
|   | |||||||
| @@ -16,3 +16,5 @@ class ProcessState(IntEnum): | |||||||
|     ACKED = auto() |     ACKED = auto() | ||||||
|     JOINED = auto() |     JOINED = auto() | ||||||
|     TERMINATED = auto() |     TERMINATED = auto() | ||||||
|  |     FAILED = auto() | ||||||
|  |     COMPLETED = auto() | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| import os | import os | ||||||
|  |  | ||||||
| from contextlib import suppress | from contextlib import suppress | ||||||
| from itertools import count | from enum import IntEnum, auto | ||||||
|  | from itertools import chain, count | ||||||
| from random import choice | 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 Any, Callable, Dict, List, Optional | from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple | ||||||
|  |  | ||||||
| from sanic.compat import OS_IS_WINDOWS | from sanic.compat import OS_IS_WINDOWS | ||||||
| from sanic.exceptions import ServerKilled | from sanic.exceptions import ServerKilled | ||||||
| @@ -13,13 +13,17 @@ from sanic.log import error_logger, logger | |||||||
| from sanic.worker.constants import RestartOrder | from sanic.worker.constants import RestartOrder | ||||||
| from sanic.worker.process import ProcessState, Worker, WorkerProcess | from sanic.worker.process import ProcessState, Worker, WorkerProcess | ||||||
|  |  | ||||||
|  |  | ||||||
| if not OS_IS_WINDOWS: | if not OS_IS_WINDOWS: | ||||||
|     from signal import SIGKILL |     from signal import SIGKILL | ||||||
| else: | else: | ||||||
|     SIGKILL = SIGINT |     SIGKILL = SIGINT | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MonitorCycle(IntEnum): | ||||||
|  |     BREAK = auto() | ||||||
|  |     CONTINUE = auto() | ||||||
|  |  | ||||||
|  |  | ||||||
| class WorkerManager: | class WorkerManager: | ||||||
|     THRESHOLD = WorkerProcess.THRESHOLD |     THRESHOLD = WorkerProcess.THRESHOLD | ||||||
|     MAIN_IDENT = "Sanic-Main" |     MAIN_IDENT = "Sanic-Main" | ||||||
| @@ -60,6 +64,8 @@ class WorkerManager: | |||||||
|         func: Callable[..., Any], |         func: Callable[..., Any], | ||||||
|         kwargs: Dict[str, Any], |         kwargs: Dict[str, Any], | ||||||
|         transient: bool = False, |         transient: bool = False, | ||||||
|  |         restartable: Optional[bool] = None, | ||||||
|  |         tracked: bool = True, | ||||||
|         workers: int = 1, |         workers: int = 1, | ||||||
|     ) -> Worker: |     ) -> Worker: | ||||||
|         """ |         """ | ||||||
| @@ -75,14 +81,35 @@ class WorkerManager: | |||||||
|             then the Worker Manager will restart the process along |             then the Worker Manager will restart the process along | ||||||
|             with any global restart (ex: auto-reload), defaults to False |             with any global restart (ex: auto-reload), defaults to False | ||||||
|         :type transient: bool, optional |         :type transient: bool, optional | ||||||
|  |         :param restartable: Whether to mark the process as restartable. If | ||||||
|  |             True then the Worker Manager will be able to restart the process | ||||||
|  |             if prompted. If transient=True, this property will be implied | ||||||
|  |             to be True, defaults to None | ||||||
|  |         :type restartable: Optional[bool], optional | ||||||
|  |         :param tracked: Whether to track the process after completion, | ||||||
|  |             defaults to True | ||||||
|         :param workers: The number of worker processes to run, defaults to 1 |         :param workers: The number of worker processes to run, defaults to 1 | ||||||
|         :type workers: int, optional |         :type workers: int, optional | ||||||
|         :return: The Worker instance |         :return: The Worker instance | ||||||
|         :rtype: Worker |         :rtype: Worker | ||||||
|         """ |         """ | ||||||
|  |         if ident in self.transient or ident in self.durable: | ||||||
|  |             raise ValueError(f"Worker {ident} already exists") | ||||||
|  |         restartable = restartable if restartable is not None else transient | ||||||
|  |         if transient and not restartable: | ||||||
|  |             raise ValueError( | ||||||
|  |                 "Cannot create a transient worker that is not restartable" | ||||||
|  |             ) | ||||||
|         container = self.transient if transient else self.durable |         container = self.transient if transient else self.durable | ||||||
|         worker = Worker( |         worker = Worker( | ||||||
|             ident, func, kwargs, self.context, self.worker_state, workers |             ident, | ||||||
|  |             func, | ||||||
|  |             kwargs, | ||||||
|  |             self.context, | ||||||
|  |             self.worker_state, | ||||||
|  |             workers, | ||||||
|  |             restartable, | ||||||
|  |             tracked, | ||||||
|         ) |         ) | ||||||
|         container[worker.ident] = worker |         container[worker.ident] = worker | ||||||
|         return worker |         return worker | ||||||
| @@ -94,6 +121,7 @@ class WorkerManager: | |||||||
|             self._serve, |             self._serve, | ||||||
|             self._server_settings, |             self._server_settings, | ||||||
|             transient=True, |             transient=True, | ||||||
|  |             restartable=True, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def shutdown_server(self, ident: Optional[str] = None) -> None: |     def shutdown_server(self, ident: Optional[str] = None) -> None: | ||||||
| @@ -122,7 +150,6 @@ class WorkerManager: | |||||||
|         self.monitor() |         self.monitor() | ||||||
|         self.join() |         self.join() | ||||||
|         self.terminate() |         self.terminate() | ||||||
|         self.cleanup() |  | ||||||
|  |  | ||||||
|     def start(self): |     def start(self): | ||||||
|         for process in self.processes: |         for process in self.processes: | ||||||
| @@ -148,20 +175,38 @@ class WorkerManager: | |||||||
|             for process in self.processes: |             for process in self.processes: | ||||||
|                 process.terminate() |                 process.terminate() | ||||||
|  |  | ||||||
|     def cleanup(self): |  | ||||||
|         """Cleanup the worker processes.""" |  | ||||||
|         for process in self.processes: |  | ||||||
|             process.exit() |  | ||||||
|  |  | ||||||
|     def restart( |     def restart( | ||||||
|         self, |         self, | ||||||
|         process_names: Optional[List[str]] = None, |         process_names: Optional[List[str]] = None, | ||||||
|         restart_order=RestartOrder.SHUTDOWN_FIRST, |         restart_order=RestartOrder.SHUTDOWN_FIRST, | ||||||
|         **kwargs, |         **kwargs, | ||||||
|     ): |     ): | ||||||
|  |         restarted = set() | ||||||
|         for process in self.transient_processes: |         for process in self.transient_processes: | ||||||
|             if not process_names or process.name in process_names: |             if process.restartable and ( | ||||||
|  |                 not process_names or process.name in process_names | ||||||
|  |             ): | ||||||
|                 process.restart(restart_order=restart_order, **kwargs) |                 process.restart(restart_order=restart_order, **kwargs) | ||||||
|  |                 restarted.add(process.name) | ||||||
|  |         if process_names: | ||||||
|  |             for process in self.durable_processes: | ||||||
|  |                 if process.restartable and process.name in process_names: | ||||||
|  |                     if process.state not in ( | ||||||
|  |                         ProcessState.COMPLETED, | ||||||
|  |                         ProcessState.FAILED, | ||||||
|  |                     ): | ||||||
|  |                         error_logger.error( | ||||||
|  |                             f"Cannot restart process {process.name} because " | ||||||
|  |                             "it is not in a final state. Current state is: " | ||||||
|  |                             f"{process.state.name}." | ||||||
|  |                         ) | ||||||
|  |                         continue | ||||||
|  |                     process.restart(restart_order=restart_order, **kwargs) | ||||||
|  |                     restarted.add(process.name) | ||||||
|  |         if process_names and not restarted: | ||||||
|  |             error_logger.error( | ||||||
|  |                 f"Failed to restart processes: {', '.join(process_names)}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|     def scale(self, num_worker: int): |     def scale(self, num_worker: int): | ||||||
|         if num_worker <= 0: |         if num_worker <= 0: | ||||||
| @@ -189,45 +234,13 @@ class WorkerManager: | |||||||
|         self.wait_for_ack() |         self.wait_for_ack() | ||||||
|         while True: |         while True: | ||||||
|             try: |             try: | ||||||
|                 if self.monitor_subscriber.poll(0.1): |                 cycle = self._poll_monitor() | ||||||
|                     message = self.monitor_subscriber.recv() |                 if cycle is MonitorCycle.BREAK: | ||||||
|                     logger.debug( |                     break | ||||||
|                         f"Monitor message: {message}", extra={"verbosity": 2} |                 elif cycle is MonitorCycle.CONTINUE: | ||||||
|                     ) |                     continue | ||||||
|                     if not message: |  | ||||||
|                         break |  | ||||||
|                     elif message == "__TERMINATE__": |  | ||||||
|                         self.shutdown() |  | ||||||
|                         break |  | ||||||
|                     logger.debug( |  | ||||||
|                         "Incoming monitor message: %s", |  | ||||||
|                         message, |  | ||||||
|                         extra={"verbosity": 1}, |  | ||||||
|                     ) |  | ||||||
|                     split_message = message.split(":", 2) |  | ||||||
|                     if message.startswith("__SCALE__"): |  | ||||||
|                         self.scale(int(split_message[-1])) |  | ||||||
|                         continue |  | ||||||
|                     processes = split_message[0] |  | ||||||
|                     reloaded_files = ( |  | ||||||
|                         split_message[1] if len(split_message) > 1 else None |  | ||||||
|                     ) |  | ||||||
|                     process_names = [ |  | ||||||
|                         name.strip() for name in processes.split(",") |  | ||||||
|                     ] |  | ||||||
|                     if "__ALL_PROCESSES__" in process_names: |  | ||||||
|                         process_names = None |  | ||||||
|                     order = ( |  | ||||||
|                         RestartOrder.STARTUP_FIRST |  | ||||||
|                         if "STARTUP_FIRST" in split_message |  | ||||||
|                         else RestartOrder.SHUTDOWN_FIRST |  | ||||||
|                     ) |  | ||||||
|                     self.restart( |  | ||||||
|                         process_names=process_names, |  | ||||||
|                         reloaded_files=reloaded_files, |  | ||||||
|                         restart_order=order, |  | ||||||
|                     ) |  | ||||||
|                 self._sync_states() |                 self._sync_states() | ||||||
|  |                 self._cleanup_non_tracked_workers() | ||||||
|             except InterruptedError: |             except InterruptedError: | ||||||
|                 if not OS_IS_WINDOWS: |                 if not OS_IS_WINDOWS: | ||||||
|                     raise |                     raise | ||||||
| @@ -270,6 +283,10 @@ class WorkerManager: | |||||||
|     def workers(self) -> List[Worker]: |     def workers(self) -> List[Worker]: | ||||||
|         return list(self.transient.values()) + list(self.durable.values()) |         return list(self.transient.values()) + list(self.durable.values()) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def all_workers(self) -> Iterable[Tuple[str, Worker]]: | ||||||
|  |         return chain(self.transient.items(), self.durable.items()) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def processes(self): |     def processes(self): | ||||||
|         for worker in self.workers: |         for worker in self.workers: | ||||||
| @@ -282,6 +299,12 @@ class WorkerManager: | |||||||
|             for process in worker.processes: |             for process in worker.processes: | ||||||
|                 yield process |                 yield process | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def durable_processes(self): | ||||||
|  |         for worker in self.durable.values(): | ||||||
|  |             for process in worker.processes: | ||||||
|  |                 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) |             logger.info("Killing %s [%s]", process.name, process.pid) | ||||||
| @@ -304,6 +327,25 @@ class WorkerManager: | |||||||
|                 process.terminate() |                 process.terminate() | ||||||
|         self._shutting_down = True |         self._shutting_down = True | ||||||
|  |  | ||||||
|  |     def remove_worker(self, worker: Worker) -> None: | ||||||
|  |         if worker.tracked: | ||||||
|  |             error_logger.error( | ||||||
|  |                 f"Worker {worker.ident} is tracked and cannot be removed." | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|  |         if worker.has_alive_processes(): | ||||||
|  |             error_logger.error( | ||||||
|  |                 f"Worker {worker.ident} has alive processes and cannot be " | ||||||
|  |                 "removed." | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|  |         self.transient.pop(worker.ident, None) | ||||||
|  |         self.durable.pop(worker.ident, None) | ||||||
|  |         for process in worker.processes: | ||||||
|  |             self.worker_state.pop(process.name, None) | ||||||
|  |         logger.info("Removed worker %s", worker.ident) | ||||||
|  |         del worker | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def pid(self): |     def pid(self): | ||||||
|         return os.getpid() |         return os.getpid() | ||||||
| @@ -323,5 +365,97 @@ class WorkerManager: | |||||||
|             except KeyError: |             except KeyError: | ||||||
|                 process.set_state(ProcessState.TERMINATED, True) |                 process.set_state(ProcessState.TERMINATED, True) | ||||||
|                 continue |                 continue | ||||||
|  |             if not process.is_alive(): | ||||||
|  |                 state = "FAILED" if process.exitcode else "COMPLETED" | ||||||
|             if state and process.state.name != state: |             if state and process.state.name != state: | ||||||
|                 process.set_state(ProcessState[state], True) |                 process.set_state(ProcessState[state], True) | ||||||
|  |  | ||||||
|  |     def _cleanup_non_tracked_workers(self) -> None: | ||||||
|  |         to_remove = [ | ||||||
|  |             worker | ||||||
|  |             for worker in self.workers | ||||||
|  |             if not worker.tracked and not worker.has_alive_processes() | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         for worker in to_remove: | ||||||
|  |             self.remove_worker(worker) | ||||||
|  |  | ||||||
|  |     def _poll_monitor(self) -> Optional[MonitorCycle]: | ||||||
|  |         if self.monitor_subscriber.poll(0.1): | ||||||
|  |             message = self.monitor_subscriber.recv() | ||||||
|  |             logger.debug(f"Monitor message: {message}", extra={"verbosity": 2}) | ||||||
|  |             if not message: | ||||||
|  |                 return MonitorCycle.BREAK | ||||||
|  |             elif message == "__TERMINATE__": | ||||||
|  |                 self._handle_terminate() | ||||||
|  |                 return MonitorCycle.BREAK | ||||||
|  |             elif isinstance(message, tuple) and len(message) == 7: | ||||||
|  |                 self._handle_manage(*message) | ||||||
|  |                 return MonitorCycle.CONTINUE | ||||||
|  |             elif not isinstance(message, str): | ||||||
|  |                 error_logger.error( | ||||||
|  |                     "Monitor received an invalid message: %s", message | ||||||
|  |                 ) | ||||||
|  |                 return MonitorCycle.CONTINUE | ||||||
|  |             return self._handle_message(message) | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     def _handle_terminate(self) -> None: | ||||||
|  |         self.shutdown() | ||||||
|  |  | ||||||
|  |     def _handle_message(self, message: str) -> Optional[MonitorCycle]: | ||||||
|  |         logger.debug( | ||||||
|  |             "Incoming monitor message: %s", | ||||||
|  |             message, | ||||||
|  |             extra={"verbosity": 1}, | ||||||
|  |         ) | ||||||
|  |         split_message = message.split(":", 2) | ||||||
|  |         if message.startswith("__SCALE__"): | ||||||
|  |             self.scale(int(split_message[-1])) | ||||||
|  |             return MonitorCycle.CONTINUE | ||||||
|  |  | ||||||
|  |         processes = split_message[0] | ||||||
|  |         reloaded_files = split_message[1] if len(split_message) > 1 else None | ||||||
|  |         process_names: Optional[List[str]] = [ | ||||||
|  |             name.strip() for name in processes.split(",") | ||||||
|  |         ] | ||||||
|  |         if process_names and "__ALL_PROCESSES__" in process_names: | ||||||
|  |             process_names = None | ||||||
|  |         order = ( | ||||||
|  |             RestartOrder.STARTUP_FIRST | ||||||
|  |             if "STARTUP_FIRST" in split_message | ||||||
|  |             else RestartOrder.SHUTDOWN_FIRST | ||||||
|  |         ) | ||||||
|  |         self.restart( | ||||||
|  |             process_names=process_names, | ||||||
|  |             reloaded_files=reloaded_files, | ||||||
|  |             restart_order=order, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     def _handle_manage( | ||||||
|  |         self, | ||||||
|  |         ident: str, | ||||||
|  |         func: Callable[..., Any], | ||||||
|  |         kwargs: Dict[str, Any], | ||||||
|  |         transient: bool, | ||||||
|  |         restartable: Optional[bool], | ||||||
|  |         tracked: bool, | ||||||
|  |         workers: int, | ||||||
|  |     ) -> None: | ||||||
|  |         try: | ||||||
|  |             worker = self.manage( | ||||||
|  |                 ident, | ||||||
|  |                 func, | ||||||
|  |                 kwargs, | ||||||
|  |                 transient=transient, | ||||||
|  |                 restartable=restartable, | ||||||
|  |                 tracked=tracked, | ||||||
|  |                 workers=workers, | ||||||
|  |             ) | ||||||
|  |         except Exception: | ||||||
|  |             error_logger.exception("Failed to manage worker %s", ident) | ||||||
|  |         else: | ||||||
|  |             for process in worker.processes: | ||||||
|  |                 process.start() | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| from multiprocessing.connection import Connection | from multiprocessing.connection import Connection | ||||||
| from os import environ, getpid | from os import environ, getpid | ||||||
| from typing import Any, Dict | from typing import Any, Callable, Dict, Optional | ||||||
|  |  | ||||||
| from sanic.log import Colors, logger | from sanic.log import Colors, logger | ||||||
| from sanic.worker.process import ProcessState | from sanic.worker.process import ProcessState | ||||||
| @@ -28,23 +28,26 @@ class WorkerMultiplexer: | |||||||
|             "state": ProcessState.ACKED.name, |             "state": ProcessState.ACKED.name, | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     def set_serving(self, serving: bool) -> None: |     def manage( | ||||||
|         """Set the worker to serving. |         self, | ||||||
|  |         ident: str, | ||||||
|         Args: |         func: Callable[..., Any], | ||||||
|             serving (bool): Whether the worker is serving. |         kwargs: Dict[str, Any], | ||||||
|         """ |         transient: bool = False, | ||||||
|         self._state._state[self.name] = { |         restartable: Optional[bool] = None, | ||||||
|             **self._state._state[self.name], |         tracked: bool = False, | ||||||
|             "serving": serving, |         workers: int = 1, | ||||||
|         } |     ) -> None: | ||||||
|  |         bundle = ( | ||||||
|     def exit(self): |             ident, | ||||||
|         """Run cleanup at worker exit.""" |             func, | ||||||
|         try: |             kwargs, | ||||||
|             del self._state._state[self.name] |             transient, | ||||||
|         except ConnectionRefusedError: |             restartable, | ||||||
|             logger.debug("Monitor process has already exited.") |             tracked, | ||||||
|  |             workers, | ||||||
|  |         ) | ||||||
|  |         self._monitor_publisher.send(bundle) | ||||||
|  |  | ||||||
|     def restart( |     def restart( | ||||||
|         self, |         self, | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| import os | import os | ||||||
|  |  | ||||||
| from datetime import datetime, timezone | from datetime import datetime, timezone | ||||||
| from multiprocessing.context import BaseContext | from multiprocessing.context import BaseContext | ||||||
| from signal import SIGINT | from signal import SIGINT | ||||||
| @@ -20,13 +19,22 @@ class WorkerProcess: | |||||||
|     THRESHOLD = 300  # == 30 seconds |     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, | ||||||
|  |         restartable: bool = False, | ||||||
|  |     ): | ||||||
|         self.state = ProcessState.IDLE |         self.state = ProcessState.IDLE | ||||||
|         self.factory = factory |         self.factory = factory | ||||||
|         self.name = name |         self.name = name | ||||||
|         self.target = target |         self.target = target | ||||||
|         self.kwargs = kwargs |         self.kwargs = kwargs | ||||||
|         self.worker_state = worker_state |         self.worker_state = worker_state | ||||||
|  |         self.restartable = restartable | ||||||
|         if self.name not in self.worker_state: |         if self.name not in self.worker_state: | ||||||
|             self.worker_state[self.name] = { |             self.worker_state[self.name] = { | ||||||
|                 "server": self.SERVER_LABEL in self.name |                 "server": self.SERVER_LABEL in self.name | ||||||
| @@ -65,20 +73,6 @@ class WorkerProcess: | |||||||
|         self.set_state(ProcessState.JOINED) |         self.set_state(ProcessState.JOINED) | ||||||
|         self._current_process.join() |         self._current_process.join() | ||||||
|  |  | ||||||
|     def exit(self): |  | ||||||
|         limit = 100 |  | ||||||
|         while self.is_alive() and limit > 0: |  | ||||||
|             sleep(0.1) |  | ||||||
|             limit -= 1 |  | ||||||
|  |  | ||||||
|         if not self.is_alive(): |  | ||||||
|             try: |  | ||||||
|                 del self.worker_state[self.name] |  | ||||||
|             except ConnectionRefusedError: |  | ||||||
|                 logger.debug("Monitor process has already exited.") |  | ||||||
|             except KeyError: |  | ||||||
|                 logger.debug("Could not find worker state to delete.") |  | ||||||
|  |  | ||||||
|     def terminate(self): |     def terminate(self): | ||||||
|         if self.state is not ProcessState.TERMINATED: |         if self.state is not ProcessState.TERMINATED: | ||||||
|             logger.debug( |             logger.debug( | ||||||
| @@ -91,6 +85,7 @@ class WorkerProcess: | |||||||
|             self.set_state(ProcessState.TERMINATED, force=True) |             self.set_state(ProcessState.TERMINATED, force=True) | ||||||
|             try: |             try: | ||||||
|                 os.kill(self.pid, SIGINT) |                 os.kill(self.pid, SIGINT) | ||||||
|  |                 del self.worker_state[self.name] | ||||||
|             except (KeyError, AttributeError, ProcessLookupError): |             except (KeyError, AttributeError, ProcessLookupError): | ||||||
|                 ... |                 ... | ||||||
|  |  | ||||||
| @@ -131,16 +126,6 @@ class WorkerProcess: | |||||||
|         except AssertionError: |         except AssertionError: | ||||||
|             return False |             return False | ||||||
|  |  | ||||||
|     # def _run(self, **kwargs): |  | ||||||
|     #     atexit.register(self._exit) |  | ||||||
|     #     self.target(**kwargs) |  | ||||||
|  |  | ||||||
|     # def _exit(self): |  | ||||||
|     #     try: |  | ||||||
|     #         del self.worker_state[self.name] |  | ||||||
|     #     except ConnectionRefusedError: |  | ||||||
|     #         logger.debug("Monitor process has already exited.") |  | ||||||
|  |  | ||||||
|     def spawn(self): |     def spawn(self): | ||||||
|         if self.state not in (ProcessState.IDLE, ProcessState.RESTARTING): |         if self.state not in (ProcessState.IDLE, ProcessState.RESTARTING): | ||||||
|             raise Exception("Cannot spawn a worker process until it is idle.") |             raise Exception("Cannot spawn a worker process until it is idle.") | ||||||
| @@ -155,6 +140,10 @@ class WorkerProcess: | |||||||
|     def pid(self): |     def pid(self): | ||||||
|         return self._current_process.pid |         return self._current_process.pid | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def exitcode(self): | ||||||
|  |         return self._current_process.exitcode | ||||||
|  |  | ||||||
|     def _terminate_now(self): |     def _terminate_now(self): | ||||||
|         logger.debug( |         logger.debug( | ||||||
|             f"{Colors.BLUE}Begin restart termination: " |             f"{Colors.BLUE}Begin restart termination: " | ||||||
| @@ -216,6 +205,8 @@ class Worker: | |||||||
|         context: BaseContext, |         context: BaseContext, | ||||||
|         worker_state: Dict[str, Any], |         worker_state: Dict[str, Any], | ||||||
|         num: int = 1, |         num: int = 1, | ||||||
|  |         restartable: bool = False, | ||||||
|  |         tracked: bool = True, | ||||||
|     ): |     ): | ||||||
|         self.ident = ident |         self.ident = ident | ||||||
|         self.num = num |         self.num = num | ||||||
| @@ -224,6 +215,8 @@ class Worker: | |||||||
|         self.server_settings = server_settings |         self.server_settings = server_settings | ||||||
|         self.worker_state = worker_state |         self.worker_state = worker_state | ||||||
|         self.processes: Set[WorkerProcess] = set() |         self.processes: Set[WorkerProcess] = set() | ||||||
|  |         self.restartable = restartable | ||||||
|  |         self.tracked = tracked | ||||||
|         for _ in range(num): |         for _ in range(num): | ||||||
|             self.create_process() |             self.create_process() | ||||||
|  |  | ||||||
| @@ -238,6 +231,10 @@ class Worker: | |||||||
|             target=self.serve, |             target=self.serve, | ||||||
|             kwargs={**self.server_settings}, |             kwargs={**self.server_settings}, | ||||||
|             worker_state=self.worker_state, |             worker_state=self.worker_state, | ||||||
|  |             restartable=self.restartable, | ||||||
|         ) |         ) | ||||||
|         self.processes.add(process) |         self.processes.add(process) | ||||||
|         return process |         return process | ||||||
|  |  | ||||||
|  |     def has_alive_processes(self) -> bool: | ||||||
|  |         return any(process.is_alive() for process in self.processes) | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								setup.py
									
									
									
									
									
								
							| @@ -83,11 +83,12 @@ setup_kwargs = { | |||||||
|     "packages": find_packages(exclude=("tests", "tests.*")), |     "packages": find_packages(exclude=("tests", "tests.*")), | ||||||
|     "package_data": {"sanic": ["py.typed", "pages/styles/*"]}, |     "package_data": {"sanic": ["py.typed", "pages/styles/*"]}, | ||||||
|     "platforms": "any", |     "platforms": "any", | ||||||
|     "python_requires": ">=3.8", |     "python_requires": ">=3.7", | ||||||
|     "classifiers": [ |     "classifiers": [ | ||||||
|         "Development Status :: 4 - Beta", |         "Development Status :: 4 - Beta", | ||||||
|         "Environment :: Web Environment", |         "Environment :: Web Environment", | ||||||
|         "License :: OSI Approved :: MIT License", |         "License :: OSI Approved :: MIT License", | ||||||
|  |         "Programming Language :: Python :: 3.7", | ||||||
|         "Programming Language :: Python :: 3.8", |         "Programming Language :: Python :: 3.8", | ||||||
|         "Programming Language :: Python :: 3.9", |         "Programming Language :: Python :: 3.9", | ||||||
|         "Programming Language :: Python :: 3.10", |         "Programming Language :: Python :: 3.10", | ||||||
| @@ -103,7 +104,7 @@ ujson = "ujson>=1.35" + env_dependency | |||||||
| uvloop = "uvloop>=0.15.0" + env_dependency | uvloop = "uvloop>=0.15.0" + env_dependency | ||||||
| types_ujson = "types-ujson" + env_dependency | types_ujson = "types-ujson" + env_dependency | ||||||
| requirements = [ | requirements = [ | ||||||
|     "sanic-routing>=23.6.0", |     "sanic-routing>=22.8.0", | ||||||
|     "httptools>=0.0.10", |     "httptools>=0.0.10", | ||||||
|     uvloop, |     uvloop, | ||||||
|     ujson, |     ujson, | ||||||
| @@ -112,11 +113,10 @@ requirements = [ | |||||||
|     "multidict>=5.0,<7.0", |     "multidict>=5.0,<7.0", | ||||||
|     "html5tagger>=1.2.1", |     "html5tagger>=1.2.1", | ||||||
|     "tracerite>=1.0.0", |     "tracerite>=1.0.0", | ||||||
|     "typing-extensions>=4.4.0", |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
| tests_require = [ | tests_require = [ | ||||||
|     "sanic-testing>=23.6.0", |     "sanic-testing>=23.3.0", | ||||||
|     "pytest==7.1.*", |     "pytest==7.1.*", | ||||||
|     "coverage", |     "coverage", | ||||||
|     "beautifulsoup4", |     "beautifulsoup4", | ||||||
| @@ -127,7 +127,7 @@ tests_require = [ | |||||||
|     "black", |     "black", | ||||||
|     "isort>=5.0.0", |     "isort>=5.0.0", | ||||||
|     "bandit", |     "bandit", | ||||||
|     "mypy", |     "mypy>=0.901,<0.910", | ||||||
|     "docutils", |     "docutils", | ||||||
|     "pygments", |     "pygments", | ||||||
|     "uvicorn<0.15.0", |     "uvicorn<0.15.0", | ||||||
| @@ -143,7 +143,6 @@ docs_require = [ | |||||||
|     "m2r2", |     "m2r2", | ||||||
|     "enum-tools[sphinx]", |     "enum-tools[sphinx]", | ||||||
|     "mistune<2.0.0", |     "mistune<2.0.0", | ||||||
|     "autodocsumm>=0.2.11", |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
| dev_require = tests_require + [ | dev_require = tests_require + [ | ||||||
|   | |||||||
| @@ -293,7 +293,7 @@ def test_handle_request_with_nested_sanic_exception( | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_app_name_required(): | def test_app_name_required(): | ||||||
|     with pytest.raises(TypeError): |     with pytest.raises(SanicException): | ||||||
|         Sanic() |         Sanic() | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,8 +2,6 @@ import logging | |||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| import sanic |  | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.config import Config | from sanic.config import Config | ||||||
| from sanic.errorpages import TextRenderer, exception_response, guess_mime | from sanic.errorpages import TextRenderer, exception_response, guess_mime | ||||||
| @@ -207,27 +205,6 @@ def test_route_error_response_from_explicit_format(app): | |||||||
|     assert response.content_type == "text/plain; charset=utf-8" |     assert response.content_type == "text/plain; charset=utf-8" | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_blueprint_error_response_from_explicit_format(app): |  | ||||||
|     bp = sanic.Blueprint("MyBlueprint") |  | ||||||
|  |  | ||||||
|     @bp.get("/text", error_format="json") |  | ||||||
|     def text_response(request): |  | ||||||
|         raise Exception("oops") |  | ||||||
|         return text("Never gonna see this") |  | ||||||
|  |  | ||||||
|     @bp.get("/json", error_format="text") |  | ||||||
|     def json_response(request): |  | ||||||
|         raise Exception("oops") |  | ||||||
|         return json({"message": "Never gonna see this"}) |  | ||||||
|  |  | ||||||
|     app.blueprint(bp) |  | ||||||
|     _, response = app.test_client.get("/text") |  | ||||||
|     assert response.content_type == "application/json" |  | ||||||
|  |  | ||||||
|     _, response = app.test_client.get("/json") |  | ||||||
|     assert response.content_type == "text/plain; charset=utf-8" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_unknown_fallback_format(app): | def test_unknown_fallback_format(app): | ||||||
|     with pytest.raises(SanicException, match="Unknown format: bad"): |     with pytest.raises(SanicException, match="Unknown format: bad"): | ||||||
|         app.config.FALLBACK_ERROR_FORMAT = "bad" |         app.config.FALLBACK_ERROR_FORMAT = "bad" | ||||||
|   | |||||||
| @@ -17,7 +17,6 @@ from sanic.response import text | |||||||
| CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} | CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} | ||||||
|  |  | ||||||
| PORT = 42001  # test_keep_alive_timeout_reuse doesn't work with random port | PORT = 42001  # test_keep_alive_timeout_reuse doesn't work with random port | ||||||
| MAX_LOOPS = 15 |  | ||||||
| port_counter = count() | port_counter = count() | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -70,35 +69,23 @@ def test_keep_alive_timeout_reuse(): | |||||||
|     """If the server keep-alive timeout and client keep-alive timeout are |     """If the server keep-alive timeout and client keep-alive timeout are | ||||||
|     both longer than the delay, the client _and_ server will successfully |     both longer than the delay, the client _and_ server will successfully | ||||||
|     reuse the existing connection.""" |     reuse the existing connection.""" | ||||||
|     loops = 0 |     port = get_port() | ||||||
|     while True: |     loop = asyncio.new_event_loop() | ||||||
|         port = get_port() |     asyncio.set_event_loop(loop) | ||||||
|         loop = asyncio.new_event_loop() |     client = ReusableClient(keep_alive_timeout_app_reuse, loop=loop, port=port) | ||||||
|         asyncio.set_event_loop(loop) |     with client: | ||||||
|         client = ReusableClient( |         headers = {"Connection": "keep-alive"} | ||||||
|             keep_alive_timeout_app_reuse, loop=loop, port=port |         request, response = client.get("/1", headers=headers) | ||||||
|         ) |         assert response.status == 200 | ||||||
|         try: |         assert response.text == "OK" | ||||||
|             with client: |         assert request.protocol.state["requests_count"] == 1 | ||||||
|                 headers = {"Connection": "keep-alive"} |  | ||||||
|                 request, response = client.get("/1", headers=headers) |  | ||||||
|                 assert response.status == 200 |  | ||||||
|                 assert response.text == "OK" |  | ||||||
|                 assert request.protocol.state["requests_count"] == 1 |  | ||||||
|  |  | ||||||
|                 loop.run_until_complete(aio_sleep(1)) |         loop.run_until_complete(aio_sleep(1)) | ||||||
|  |  | ||||||
|                 request, response = client.get("/1") |         request, response = client.get("/1") | ||||||
|                 assert response.status == 200 |         assert response.status == 200 | ||||||
|                 assert response.text == "OK" |         assert response.text == "OK" | ||||||
|                 assert request.protocol.state["requests_count"] == 2 |         assert request.protocol.state["requests_count"] == 2 | ||||||
|         except OSError: |  | ||||||
|             loops += 1 |  | ||||||
|             if loops > MAX_LOOPS: |  | ||||||
|                 raise |  | ||||||
|             continue |  | ||||||
|         else: |  | ||||||
|             break |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif( | @pytest.mark.skipif( | ||||||
| @@ -110,35 +97,23 @@ def test_keep_alive_timeout_reuse(): | |||||||
| def test_keep_alive_client_timeout(): | def test_keep_alive_client_timeout(): | ||||||
|     """If the server keep-alive timeout is longer than the client |     """If the server keep-alive timeout is longer than the client | ||||||
|     keep-alive timeout, client will try to create a new connection here.""" |     keep-alive timeout, client will try to create a new connection here.""" | ||||||
|     loops = 0 |     port = get_port() | ||||||
|     while True: |     loop = asyncio.new_event_loop() | ||||||
|         try: |     asyncio.set_event_loop(loop) | ||||||
|             port = get_port() |     client = ReusableClient( | ||||||
|             loop = asyncio.new_event_loop() |         keep_alive_app_client_timeout, loop=loop, port=port | ||||||
|             asyncio.set_event_loop(loop) |     ) | ||||||
|             client = ReusableClient( |     with client: | ||||||
|                 keep_alive_app_client_timeout, loop=loop, port=port |         headers = {"Connection": "keep-alive"} | ||||||
|             ) |         request, response = client.get("/1", headers=headers, timeout=1) | ||||||
|             with client: |  | ||||||
|                 headers = {"Connection": "keep-alive"} |  | ||||||
|                 request, response = client.get( |  | ||||||
|                     "/1", headers=headers, timeout=1 |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|                 assert response.status == 200 |         assert response.status == 200 | ||||||
|                 assert response.text == "OK" |         assert response.text == "OK" | ||||||
|                 assert request.protocol.state["requests_count"] == 1 |         assert request.protocol.state["requests_count"] == 1 | ||||||
|  |  | ||||||
|                 loop.run_until_complete(aio_sleep(2)) |         loop.run_until_complete(aio_sleep(2)) | ||||||
|                 request, response = client.get("/1", timeout=1) |         request, response = client.get("/1", timeout=1) | ||||||
|                 assert request.protocol.state["requests_count"] == 1 |         assert request.protocol.state["requests_count"] == 1 | ||||||
|         except OSError: |  | ||||||
|             loops += 1 |  | ||||||
|             if loops > MAX_LOOPS: |  | ||||||
|                 raise |  | ||||||
|             continue |  | ||||||
|         else: |  | ||||||
|             break |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif( | @pytest.mark.skipif( | ||||||
| @@ -150,36 +125,24 @@ def test_keep_alive_server_timeout(): | |||||||
|     keep-alive timeout, the client will either a 'Connection reset' error |     keep-alive timeout, the client will either a 'Connection reset' error | ||||||
|     _or_ a new connection. Depending on how the event-loop handles the |     _or_ a new connection. Depending on how the event-loop handles the | ||||||
|     broken server connection.""" |     broken server connection.""" | ||||||
|     loops = 0 |     port = get_port() | ||||||
|     while True: |     loop = asyncio.new_event_loop() | ||||||
|         try: |     asyncio.set_event_loop(loop) | ||||||
|             port = get_port() |     client = ReusableClient( | ||||||
|             loop = asyncio.new_event_loop() |         keep_alive_app_server_timeout, loop=loop, port=port | ||||||
|             asyncio.set_event_loop(loop) |     ) | ||||||
|             client = ReusableClient( |     with client: | ||||||
|                 keep_alive_app_server_timeout, loop=loop, port=port |         headers = {"Connection": "keep-alive"} | ||||||
|             ) |         request, response = client.get("/1", headers=headers, timeout=60) | ||||||
|             with client: |  | ||||||
|                 headers = {"Connection": "keep-alive"} |  | ||||||
|                 request, response = client.get( |  | ||||||
|                     "/1", headers=headers, timeout=60 |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|                 assert response.status == 200 |         assert response.status == 200 | ||||||
|                 assert response.text == "OK" |         assert response.text == "OK" | ||||||
|                 assert request.protocol.state["requests_count"] == 1 |         assert request.protocol.state["requests_count"] == 1 | ||||||
|  |  | ||||||
|                 loop.run_until_complete(aio_sleep(3)) |         loop.run_until_complete(aio_sleep(3)) | ||||||
|                 request, response = client.get("/1", timeout=60) |         request, response = client.get("/1", timeout=60) | ||||||
|  |  | ||||||
|                 assert request.protocol.state["requests_count"] == 1 |         assert request.protocol.state["requests_count"] == 1 | ||||||
|         except OSError: |  | ||||||
|             loops += 1 |  | ||||||
|             if loops > MAX_LOOPS: |  | ||||||
|                 raise |  | ||||||
|             continue |  | ||||||
|         else: |  | ||||||
|             break |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.skipif( | @pytest.mark.skipif( | ||||||
| @@ -187,34 +150,20 @@ def test_keep_alive_server_timeout(): | |||||||
|     reason="Not testable with current client", |     reason="Not testable with current client", | ||||||
| ) | ) | ||||||
| def test_keep_alive_connection_context(): | def test_keep_alive_connection_context(): | ||||||
|     loops = 0 |     port = get_port() | ||||||
|     while True: |     loop = asyncio.new_event_loop() | ||||||
|         try: |     asyncio.set_event_loop(loop) | ||||||
|             port = get_port() |     client = ReusableClient(keep_alive_app_context, loop=loop, port=port) | ||||||
|             loop = asyncio.new_event_loop() |     with client: | ||||||
|             asyncio.set_event_loop(loop) |         headers = {"Connection": "keep-alive"} | ||||||
|             client = ReusableClient( |         request1, _ = client.post("/ctx", headers=headers) | ||||||
|                 keep_alive_app_context, loop=loop, port=port |  | ||||||
|             ) |  | ||||||
|             with client: |  | ||||||
|                 headers = {"Connection": "keep-alive"} |  | ||||||
|                 request1, _ = client.post("/ctx", headers=headers) |  | ||||||
|  |  | ||||||
|                 loop.run_until_complete(aio_sleep(1)) |         loop.run_until_complete(aio_sleep(1)) | ||||||
|                 request2, response = client.get("/ctx") |         request2, response = client.get("/ctx") | ||||||
|  |  | ||||||
|                 assert response.text == "hello" |         assert response.text == "hello" | ||||||
|                 assert id(request1.conn_info.ctx) == id(request2.conn_info.ctx) |         assert id(request1.conn_info.ctx) == id(request2.conn_info.ctx) | ||||||
|                 assert ( |         assert ( | ||||||
|                     request1.conn_info.ctx.foo |             request1.conn_info.ctx.foo == request2.conn_info.ctx.foo == "hello" | ||||||
|                     == request2.conn_info.ctx.foo |         ) | ||||||
|                     == "hello" |         assert request2.protocol.state["requests_count"] == 2 | ||||||
|                 ) |  | ||||||
|                 assert request2.protocol.state["requests_count"] == 2 |  | ||||||
|         except OSError: |  | ||||||
|             loops += 1 |  | ||||||
|             if loops > MAX_LOOPS: |  | ||||||
|                 raise |  | ||||||
|             continue |  | ||||||
|         else: |  | ||||||
|             break |  | ||||||
|   | |||||||
| @@ -126,7 +126,7 @@ def test_redirect_with_params(app, test_str): | |||||||
|  |  | ||||||
|     @app.route("/api/v2/test/<test>/", unquote=True) |     @app.route("/api/v2/test/<test>/", unquote=True) | ||||||
|     async def target_handler(request, test): |     async def target_handler(request, test): | ||||||
|         assert test == quote(test_str) |         assert test == test_str | ||||||
|         return text("OK") |         return text("OK") | ||||||
|  |  | ||||||
|     _, response = app.test_client.get(f"/api/v1/test/{use_in_uri}/") |     _, response = app.test_client.get(f"/api/v1/test/{use_in_uri}/") | ||||||
|   | |||||||
| @@ -310,29 +310,3 @@ def test_request_idempotent(method, idempotent): | |||||||
| def test_request_cacheable(method, cacheable): | def test_request_cacheable(method, cacheable): | ||||||
|     request = Request(b"/", {}, None, method, None, None) |     request = Request(b"/", {}, None, method, None, None) | ||||||
|     assert request.is_cacheable is cacheable |     assert request.is_cacheable is cacheable | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_custom_ctx(): |  | ||||||
|     class CustomContext: |  | ||||||
|         FOO = "foo" |  | ||||||
|  |  | ||||||
|     class CustomRequest(Request[Sanic, CustomContext]): |  | ||||||
|         @staticmethod |  | ||||||
|         def make_context() -> CustomContext: |  | ||||||
|             return CustomContext() |  | ||||||
|  |  | ||||||
|     app = Sanic("Test", request_class=CustomRequest) |  | ||||||
|  |  | ||||||
|     @app.get("/") |  | ||||||
|     async def handler(request: CustomRequest): |  | ||||||
|         return response.json( |  | ||||||
|             [ |  | ||||||
|                 isinstance(request, CustomRequest), |  | ||||||
|                 isinstance(request.ctx, CustomContext), |  | ||||||
|                 request.ctx.FOO, |  | ||||||
|             ] |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     _, resp = app.test_client.get("/") |  | ||||||
|  |  | ||||||
|     assert resp.json == [True, True, "foo"] |  | ||||||
|   | |||||||
| @@ -513,7 +513,6 @@ def test_standard_forwarded(app): | |||||||
|     request, response = app.test_client.get("/", headers=headers) |     request, response = app.test_client.get("/", headers=headers) | ||||||
|     assert response.json == {"for": "127.0.0.2", "proto": "ws"} |     assert response.json == {"for": "127.0.0.2", "proto": "ws"} | ||||||
|     assert request.remote_addr == "127.0.0.2" |     assert request.remote_addr == "127.0.0.2" | ||||||
|     assert request.client_ip == "127.0.0.2" |  | ||||||
|     assert request.scheme == "ws" |     assert request.scheme == "ws" | ||||||
|     assert request.server_name == "local.site" |     assert request.server_name == "local.site" | ||||||
|     assert request.server_port == 80 |     assert request.server_port == 80 | ||||||
| @@ -738,7 +737,6 @@ def test_remote_addr_with_two_proxies(app): | |||||||
|     headers = {"X-Forwarded-For": "127.0.1.1"} |     headers = {"X-Forwarded-For": "127.0.1.1"} | ||||||
|     request, response = app.test_client.get("/", headers=headers) |     request, response = app.test_client.get("/", headers=headers) | ||||||
|     assert request.remote_addr == "" |     assert request.remote_addr == "" | ||||||
|     assert request.client_ip == "127.0.0.1" |  | ||||||
|     assert response.body == b"" |     assert response.body == b"" | ||||||
|  |  | ||||||
|     headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} |     headers = {"X-Forwarded-For": "127.0.0.1, 127.0.1.2"} | ||||||
|   | |||||||
| @@ -1,14 +1,12 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import os | import os | ||||||
| import signal | import signal | ||||||
|  |  | ||||||
| from queue import Queue | from queue import Queue | ||||||
| from types import SimpleNamespace | from types import SimpleNamespace | ||||||
| from typing import Optional | from typing import Optional | ||||||
| from unittest.mock import MagicMock | from unittest.mock import MagicMock | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from sanic_testing.testing import HOST, PORT | from sanic_testing.testing import HOST, PORT | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| @@ -160,7 +158,7 @@ def test_signal_server_lifecycle_exception(app: Sanic): | |||||||
|     async def hello_route(request): |     async def hello_route(request): | ||||||
|         return HTTPResponse() |         return HTTPResponse() | ||||||
|  |  | ||||||
|     @app.signal(Event.SERVER_EXCEPTION_REPORT) |     @app.signal(Event.SERVER_LIFECYCLE_EXCEPTION) | ||||||
|     async def test_signal(exception: Exception): |     async def test_signal(exception: Exception): | ||||||
|         nonlocal trigger |         nonlocal trigger | ||||||
|         trigger = exception |         trigger = exception | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ import asyncio | |||||||
|  |  | ||||||
| from enum import Enum | from enum import Enum | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
| from itertools import count |  | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| @@ -10,7 +9,6 @@ from sanic_routing.exceptions import NotFound | |||||||
|  |  | ||||||
| from sanic import Blueprint, Sanic, empty | from sanic import Blueprint, Sanic, empty | ||||||
| from sanic.exceptions import InvalidSignal, SanicException | from sanic.exceptions import InvalidSignal, SanicException | ||||||
| from sanic.signals import Event |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_add_signal(app): | def test_add_signal(app): | ||||||
| @@ -429,114 +427,3 @@ def test_signal_reservation(app, event, expected): | |||||||
|             app.signal(event)(lambda: ...) |             app.signal(event)(lambda: ...) | ||||||
|     else: |     else: | ||||||
|         app.signal(event)(lambda: ...) |         app.signal(event)(lambda: ...) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.asyncio |  | ||||||
| async def test_report_exception(app: Sanic): |  | ||||||
|     @app.report_exception |  | ||||||
|     async def catch_any_exception(app: Sanic, exception: Exception): |  | ||||||
|         ... |  | ||||||
|  |  | ||||||
|     @app.route("/") |  | ||||||
|     async def handler(request): |  | ||||||
|         1 / 0 |  | ||||||
|  |  | ||||||
|     app.signal_router.finalize() |  | ||||||
|  |  | ||||||
|     registered_signal_handlers = [ |  | ||||||
|         handler |  | ||||||
|         for handler, *_ in app.signal_router.get( |  | ||||||
|             Event.SERVER_EXCEPTION_REPORT.value |  | ||||||
|         ) |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     assert catch_any_exception in registered_signal_handlers |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_report_exception_runs(app: Sanic): |  | ||||||
|     event = asyncio.Event() |  | ||||||
|  |  | ||||||
|     @app.report_exception |  | ||||||
|     async def catch_any_exception(app: Sanic, exception: Exception): |  | ||||||
|         event.set() |  | ||||||
|  |  | ||||||
|     @app.route("/") |  | ||||||
|     async def handler(request): |  | ||||||
|         1 / 0 |  | ||||||
|  |  | ||||||
|     app.test_client.get("/") |  | ||||||
|  |  | ||||||
|     assert event.is_set() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_report_exception_runs_once_inline(app: Sanic): |  | ||||||
|     event = asyncio.Event() |  | ||||||
|     c = count() |  | ||||||
|  |  | ||||||
|     @app.report_exception |  | ||||||
|     async def catch_any_exception(app: Sanic, exception: Exception): |  | ||||||
|         event.set() |  | ||||||
|         next(c) |  | ||||||
|  |  | ||||||
|     @app.route("/") |  | ||||||
|     async def handler(request): |  | ||||||
|         ... |  | ||||||
|  |  | ||||||
|     @app.signal(Event.HTTP_ROUTING_AFTER.value) |  | ||||||
|     async def after_routing(**_): |  | ||||||
|         1 / 0 |  | ||||||
|  |  | ||||||
|     app.test_client.get("/") |  | ||||||
|  |  | ||||||
|     assert event.is_set() |  | ||||||
|     assert next(c) == 1 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_report_exception_runs_once_custom(app: Sanic): |  | ||||||
|     event = asyncio.Event() |  | ||||||
|     c = count() |  | ||||||
|  |  | ||||||
|     @app.report_exception |  | ||||||
|     async def catch_any_exception(app: Sanic, exception: Exception): |  | ||||||
|         event.set() |  | ||||||
|         next(c) |  | ||||||
|  |  | ||||||
|     @app.route("/") |  | ||||||
|     async def handler(request): |  | ||||||
|         await app.dispatch("one.two.three") |  | ||||||
|         return empty() |  | ||||||
|  |  | ||||||
|     @app.signal("one.two.three") |  | ||||||
|     async def one_two_three(**_): |  | ||||||
|         1 / 0 |  | ||||||
|  |  | ||||||
|     app.test_client.get("/") |  | ||||||
|  |  | ||||||
|     assert event.is_set() |  | ||||||
|     assert next(c) == 1 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_report_exception_runs_task(app: Sanic): |  | ||||||
|     c = count() |  | ||||||
|  |  | ||||||
|     async def task_1(): |  | ||||||
|         next(c) |  | ||||||
|  |  | ||||||
|     async def task_2(app): |  | ||||||
|         next(c) |  | ||||||
|  |  | ||||||
|     @app.report_exception |  | ||||||
|     async def catch_any_exception(app: Sanic, exception: Exception): |  | ||||||
|         next(c) |  | ||||||
|  |  | ||||||
|     @app.route("/") |  | ||||||
|     async def handler(request): |  | ||||||
|         app.add_task(task_1) |  | ||||||
|         app.add_task(task_1()) |  | ||||||
|         app.add_task(task_2) |  | ||||||
|         app.add_task(task_2(app)) |  | ||||||
|         return empty() |  | ||||||
|  |  | ||||||
|     app.test_client.get("/") |  | ||||||
|  |  | ||||||
|     assert next(c) == 4 |  | ||||||
|   | |||||||
| @@ -1,10 +0,0 @@ | |||||||
| from sanic import Sanic |  | ||||||
| from sanic.config import Config |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class CustomConfig(Config): |  | ||||||
|     pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("test", config=CustomConfig()) |  | ||||||
| reveal_type(app) |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| from sanic import Sanic |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Foo: |  | ||||||
|     pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("test", ctx=Foo()) |  | ||||||
| reveal_type(app) |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| from sanic import Sanic |  | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("test") |  | ||||||
| reveal_type(app) |  | ||||||
| @@ -1,14 +0,0 @@ | |||||||
| from sanic import Sanic |  | ||||||
| from sanic.config import Config |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class CustomConfig(Config): |  | ||||||
|     pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Foo: |  | ||||||
|     pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("test", config=CustomConfig(), ctx=Foo()) |  | ||||||
| reveal_type(app) |  | ||||||
| @@ -1,17 +0,0 @@ | |||||||
| from types import SimpleNamespace |  | ||||||
|  |  | ||||||
| from sanic import Request, Sanic |  | ||||||
| from sanic.config import Config |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Foo: |  | ||||||
|     pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("test") |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.get("/") |  | ||||||
| async def handler(request: Request[Sanic[Config, SimpleNamespace], Foo]): |  | ||||||
|     reveal_type(request.ctx) |  | ||||||
|     reveal_type(request.app) |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| from types import SimpleNamespace |  | ||||||
|  |  | ||||||
| from sanic import Request, Sanic |  | ||||||
| from sanic.config import Config |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class CustomConfig(Config): |  | ||||||
|     pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic("test", config=CustomConfig()) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.get("/") |  | ||||||
| async def handler( |  | ||||||
|     request: Request[Sanic[CustomConfig, SimpleNamespace], SimpleNamespace] |  | ||||||
| ): |  | ||||||
|     reveal_type(request.ctx) |  | ||||||
|     reveal_type(request.app) |  | ||||||
| @@ -1,34 +0,0 @@ | |||||||
| from sanic import Request, Sanic |  | ||||||
| from sanic.config import Config |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class CustomConfig(Config): |  | ||||||
|     pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Foo: |  | ||||||
|     pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class RequestContext: |  | ||||||
|     foo: Foo |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class CustomRequest(Request[Sanic[CustomConfig, Foo], RequestContext]): |  | ||||||
|     @staticmethod |  | ||||||
|     def make_context() -> RequestContext: |  | ||||||
|         ctx = RequestContext() |  | ||||||
|         ctx.foo = Foo() |  | ||||||
|         return ctx |  | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic( |  | ||||||
|     "test", config=CustomConfig(), ctx=Foo(), request_class=CustomRequest |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.get("/") |  | ||||||
| async def handler(request: CustomRequest): |  | ||||||
|     reveal_type(request) |  | ||||||
|     reveal_type(request.ctx) |  | ||||||
|     reveal_type(request.app) |  | ||||||
| @@ -1,127 +0,0 @@ | |||||||
| # flake8: noqa: E501 |  | ||||||
|  |  | ||||||
| import subprocess |  | ||||||
| import sys |  | ||||||
|  |  | ||||||
| from pathlib import Path |  | ||||||
| from typing import List, Tuple |  | ||||||
|  |  | ||||||
| import pytest |  | ||||||
|  |  | ||||||
|  |  | ||||||
| CURRENT_DIR = Path(__file__).parent |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def run_check(path_location: str) -> str: |  | ||||||
|     """Use mypy to check the given path location and return the output.""" |  | ||||||
|  |  | ||||||
|     mypy_path = "mypy" |  | ||||||
|     path = CURRENT_DIR / path_location |  | ||||||
|     command = [mypy_path, path.resolve().as_posix()] |  | ||||||
|  |  | ||||||
|     process = subprocess.run( |  | ||||||
|         command, |  | ||||||
|         stdout=subprocess.PIPE, |  | ||||||
|         stderr=subprocess.PIPE, |  | ||||||
|         universal_newlines=True, |  | ||||||
|     ) |  | ||||||
|     output = process.stdout + process.stderr |  | ||||||
|     return output |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.mark.parametrize( |  | ||||||
|     "path_location,expected", |  | ||||||
|     ( |  | ||||||
|         ( |  | ||||||
|             "app_default.py", |  | ||||||
|             [ |  | ||||||
|                 ( |  | ||||||
|                     "sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]", |  | ||||||
|                     5, |  | ||||||
|                 ) |  | ||||||
|             ], |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             "app_custom_config.py", |  | ||||||
|             [ |  | ||||||
|                 ( |  | ||||||
|                     "sanic.app.Sanic[app_custom_config.CustomConfig, types.SimpleNamespace]", |  | ||||||
|                     10, |  | ||||||
|                 ) |  | ||||||
|             ], |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             "app_custom_ctx.py", |  | ||||||
|             [("sanic.app.Sanic[sanic.config.Config, app_custom_ctx.Foo]", 9)], |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             "app_fully_custom.py", |  | ||||||
|             [ |  | ||||||
|                 ( |  | ||||||
|                     "sanic.app.Sanic[app_fully_custom.CustomConfig, app_fully_custom.Foo]", |  | ||||||
|                     14, |  | ||||||
|                 ) |  | ||||||
|             ], |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             "request_custom_sanic.py", |  | ||||||
|             [ |  | ||||||
|                 ("types.SimpleNamespace", 18), |  | ||||||
|                 ( |  | ||||||
|                     "sanic.app.Sanic[request_custom_sanic.CustomConfig, types.SimpleNamespace]", |  | ||||||
|                     19, |  | ||||||
|                 ), |  | ||||||
|             ], |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             "request_custom_ctx.py", |  | ||||||
|             [ |  | ||||||
|                 ("request_custom_ctx.Foo", 16), |  | ||||||
|                 ( |  | ||||||
|                     "sanic.app.Sanic[sanic.config.Config, types.SimpleNamespace]", |  | ||||||
|                     17, |  | ||||||
|                 ), |  | ||||||
|             ], |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             "request_fully_custom.py", |  | ||||||
|             [ |  | ||||||
|                 ("request_fully_custom.CustomRequest", 32), |  | ||||||
|                 ("request_fully_custom.RequestContext", 33), |  | ||||||
|                 ( |  | ||||||
|                     "sanic.app.Sanic[request_fully_custom.CustomConfig, request_fully_custom.Foo]", |  | ||||||
|                     34, |  | ||||||
|                 ), |  | ||||||
|             ], |  | ||||||
|         ), |  | ||||||
|     ), |  | ||||||
| ) |  | ||||||
| def test_check_app_default( |  | ||||||
|     path_location: str, expected: List[Tuple[str, int]] |  | ||||||
| ) -> None: |  | ||||||
|     output = run_check(f"samples/{path_location}") |  | ||||||
|  |  | ||||||
|     for text, number in expected: |  | ||||||
|         current = CURRENT_DIR / f"samples/{path_location}" |  | ||||||
|         path = current.relative_to(CURRENT_DIR.parent) |  | ||||||
|  |  | ||||||
|         target = Path.cwd() |  | ||||||
|         while True: |  | ||||||
|             note = _text_from_path(current, path, target, number, text) |  | ||||||
|             try: |  | ||||||
|                 assert note in output, output |  | ||||||
|             except AssertionError: |  | ||||||
|                 target = target.parent |  | ||||||
|                 if not target.exists(): |  | ||||||
|                     raise |  | ||||||
|             else: |  | ||||||
|                 break |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def _text_from_path( |  | ||||||
|     base: Path, path: Path, target: Path, number: int, text: str |  | ||||||
| ) -> str: |  | ||||||
|     relative_to_cwd = base.relative_to(target) |  | ||||||
|     prefix = ".".join(relative_to_cwd.parts[:-1]) |  | ||||||
|     text = text.replace(path.stem, f"{prefix}.{path.stem}") |  | ||||||
|     return f'{path}:{number}: note: Revealed type is "{text}"' |  | ||||||
							
								
								
									
										8
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -1,14 +1,14 @@ | |||||||
| [tox] | [tox] | ||||||
| envlist = py38, py39, py310, py311, pyNightly, pypy310, {py38,py39,py310,py311,pyNightly,pypy310}-no-ext, lint, check, security, docs, type-checking | envlist = py37, py38, py39, py310, py311, pyNightly, pypy37, {py37,py38,py39,py310,py311,pyNightly,pypy37}-no-ext, lint, check, security, docs, type-checking | ||||||
|  |  | ||||||
| [testenv] | [testenv] | ||||||
| usedevelop = true | usedevelop = true | ||||||
| setenv = | setenv = | ||||||
|     {py38,py39,py310,py311,pyNightly}-no-ext: SANIC_NO_UJSON=1 |     {py37,py38,py39,py310,py311,pyNightly}-no-ext: SANIC_NO_UJSON=1 | ||||||
|     {py38,py39,py310,py311,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 |     {py37,py38,py39,py310,py311,pyNightly}-no-ext: SANIC_NO_UVLOOP=1 | ||||||
| extras = test, http3 | extras = test, http3 | ||||||
| deps = | deps = | ||||||
|     httpx>=0.23 |     httpx==0.23 | ||||||
| allowlist_externals = | allowlist_externals = | ||||||
|     pytest |     pytest | ||||||
|     coverage |     coverage | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user