From 95a9ac35b9c3b621f2d453ac59d4cad3da85b4ac Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 15 Nov 2024 10:32:05 -0800 Subject: [PATCH 1/3] refresh to match pallets - use flit as build backend - pin development dependencies using pip-compile - stop using dependabot - update tox envs and requirements - add tox envs for updating pins in actions, pre-commit, and pip-compile - use ruff for linting and formatting - transfer copyright to pallets - use global code of conduct - convert readme to markdown - new contributing guide - update issue templates - add .editorconfig - update .gitignore - add license and changes to docs - simplify docs config - add myst parser in preparation for markdown docs - remove license for previous logo --- .editorconfig | 13 ++ .github/ISSUE_TEMPLATE/bug-report.md | 4 +- .github/ISSUE_TEMPLATE/config.yml | 12 +- .github/ISSUE_TEMPLATE/feature-request.md | 6 +- .github/dependabot.yml | 6 - .github/pull_request_template.md | 21 +- .github/workflows/lock.yaml | 15 +- .github/workflows/pre-commit.yaml | 16 ++ .github/workflows/publish.yaml | 34 +-- .github/workflows/tests.yaml | 68 +++--- .gitignore | 20 +- .pre-commit-config.yaml | 32 +-- .readthedocs.yaml | 11 +- CODE_OF_CONDUCT.md | 76 ------- CONTRIBUTING.md | 108 ++++++++++ CONTRIBUTING.rst | 186 ----------------- LICENSE | 22 -- LICENSE.txt | 20 ++ README.md | 90 ++++++++ README.rst | 137 ------------ artwork/LICENSE | 12 -- docs/changes.rst | 4 + docs/conf.py | 194 +++-------------- docs/index.rst | 6 +- docs/license.rst | 5 + pyproject.toml | 147 +++++++------ requirements/build.in | 1 + requirements/build.txt | 8 + requirements/dev.in | 6 + requirements/dev.txt | 244 ++++++++++++++++++++++ requirements/docs.in | 3 + requirements/docs.txt | 79 +++++++ requirements/tests.in | 6 + requirements/tests.txt | 34 +++ requirements/typing.in | 7 + requirements/typing.txt | 36 ++++ setup.cfg | 7 - tox.ini | 103 ++++----- 38 files changed, 925 insertions(+), 874 deletions(-) create mode 100644 .editorconfig delete mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/pre-commit.yaml delete mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md delete mode 100644 CONTRIBUTING.rst delete mode 100644 LICENSE create mode 100644 LICENSE.txt create mode 100644 README.md delete mode 100644 README.rst delete mode 100644 artwork/LICENSE create mode 100644 docs/changes.rst create mode 100644 docs/license.rst create mode 100644 requirements/build.in create mode 100644 requirements/build.txt create mode 100644 requirements/dev.in create mode 100644 requirements/dev.txt create mode 100644 requirements/docs.in create mode 100644 requirements/docs.txt create mode 100644 requirements/tests.in create mode 100644 requirements/tests.txt create mode 100644 requirements/typing.in create mode 100644 requirements/typing.txt delete mode 100644 setup.cfg diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..2ff985a6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +charset = utf-8 +max_line_length = 88 + +[*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}] +indent_size = 2 diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index f3c77cd6..8f54d68d 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -4,8 +4,8 @@ about: Report a bug in Quart (not other projects which depend on Quart) --- diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7eddce48..78bfae6a 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,11 @@ blank_issues_enabled: false contact_links: - name: Security issue - url: security@palletsprojects.com - about: Do not report security issues publicly. Email our security contact. + url: https://github.com/pallets/quart/security/advisories/new + about: Do not report security issues publicly. Create a private advisory. - name: Questions - url: https://stackoverflow.com/questions/tagged/quart?tab=Frequent - about: Search for and ask questions about your code on Stack Overflow. - - name: Questions and discussions + url: https://github.com/pallets/quart/discussions/ + about: Ask questions about your own code on the Discussions tab. + - name: Questions on url: https://discord.gg/pallets - about: Discuss questions about your code on our Discord chat. + about: Ask questions about your own code on our Discord chat. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 23e443a5..587f8cbb 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -5,11 +5,11 @@ about: Suggest a new feature for Quart diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 8ac6b8c4..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "monthly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 29fd35f8..c208b17f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,7 @@ - -- fixes # - - - -Checklist: - -- [ ] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change. -- [ ] Add or update relevant docs, in the docs folder and in code. -- [ ] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue. -- [ ] Add `.. versionchanged::` entries in any relevant code docs. -- [ ] Run `pre-commit` hooks and fix any issues. -- [ ] Run `pytest` and `tox`, no tests failed. diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml index d6bbb5fd..b3b80059 100644 --- a/.github/workflows/lock.yaml +++ b/.github/workflows/lock.yaml @@ -1,25 +1,22 @@ -name: 'Lock threads' -# Lock closed issues that have not received any further activity for -# two weeks. This does not close open issues, only humans may do that. -# We find that it is easier to respond to new issues with fresh examples -# rather than continuing discussions on old issues. +name: Lock inactive closed issues +# Lock closed issues that have not received any further activity for two weeks. +# This does not close open issues, only humans may do that. It is easier to +# respond to new issues with fresh examples rather than continuing discussions +# on old issues. on: schedule: - cron: '0 0 * * *' - permissions: issues: write pull-requests: write - concurrency: group: lock - jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v5 + - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 with: issue-inactive-days: 14 pr-inactive-days: 14 diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 00000000..263d42b3 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,16 @@ +name: pre-commit +on: + pull_request: + push: + branches: [main, stable] +jobs: + main: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + with: + python-version: 3.x + - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 + - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 + if: ${{ !cancelled() }} diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index c12a4638..783c5ad8 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -9,40 +9,42 @@ jobs: outputs: hash: ${{ steps.hash.outputs.hash }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: '3.x' - - run: pip install poetry - - run: poetry build + cache: pip + cache-dependency-path: requirements*/*.txt + - run: pip install -r requirements/build.txt + # Use the commit date instead of the current date during the build. + - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV + - run: python -m build # Generate hashes used for provenance. - name: generate hash id: hash run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: path: ./dist - provenance: - needs: ['build'] + needs: [build] permissions: actions: read id-token: write contents: write # Can't pin with hash due to how this workflow works. - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 with: base64-subjects: ${{ needs.build.outputs.hash }} - create-release: # Upload the sdist, wheels, and provenance to a GitHub release. They remain # available as build artifacts for a while as well. - needs: ['provenance'] + needs: [provenance] runs-on: ubuntu-latest permissions: contents: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - name: create release run: > gh release create --draft --repo ${{ github.repository }} @@ -51,15 +53,17 @@ jobs: env: GH_TOKEN: ${{ github.token }} publish-pypi: - needs: ['provenance'] + needs: [provenance] # Wait for approval before attempting to upload to PyPI. This allows reviewing the # files in the draft release. - environment: 'publish' + environment: + name: publish + url: https://pypi.org/project/Quart/${{ github.ref_name }} runs-on: ubuntu-latest permissions: id-token: write steps: - - uses: actions/download-artifact@v4 - - uses: pypa/gh-action-pypi-publish@release/v1 + - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + - uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2 with: packages-dir: artifact/ diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 5c62c046..336b7fa7 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,54 +1,48 @@ name: Tests on: push: - branches: - - main - - pallets - paths-ignore: - - 'docs/**' - - '*.md' - - '*.rst' + branches: [main, stable] + paths-ignore: ['docs/**', '*.md', '*.rst'] pull_request: - branches: - - main - paths-ignore: - - 'docs/**' - - '*.md' - - '*.rst' + paths-ignore: [ 'docs/**', '*.md', '*.rst' ] jobs: tests: - name: ${{ matrix.name }} - runs-on: ${{ matrix.os }} + name: ${{ matrix.name || matrix.python }} + runs-on: ${{ matrix.os || 'ubuntu-latest' }} strategy: fail-fast: false matrix: include: - - {name: Linux, python: '3.13', os: ubuntu-latest, tox: py313} - - {name: Windows, python: '3.13', os: windows-latest, tox: py313} - - {name: Mac, python: '3.13', os: macos-latest, tox: py313} - - {name: '3.13', python: '3.13', os: ubuntu-latest, tox: py313} - - {name: '3.12', python: '3.12', os: ubuntu-latest, tox: py312} - - {name: '3.11', python: '3.11', os: ubuntu-latest, tox: py311} - - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39} - - {name: Typing, python: '3.13', os: ubuntu-latest, tox: mypy} - - {name: Package, python: '3.13', os: ubuntu-latest, tox: package} - - {name: Lint, python: '3.13', os: ubuntu-latest, tox: pep8} - - {name: Format, python: '3.13', os: ubuntu-latest, tox: format} + - {python: '3.13'} + - {name: Windows, python: '3.13', os: windows-latest} + - {name: Mac, python: '3.13', os: macos-latest} + - {python: '3.12'} + - {python: '3.11'} + - {python: '3.10'} + - {python: '3.9'} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: ${{ matrix.python }} - - name: update pip - run: | - pip install -U wheel - pip install -U setuptools - python -m pip install -U pip + allow-prereleases: true + cache: pip + cache-dependency-path: requirements*/*.txt + - run: pip install tox + - run: tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }} + typing: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + with: + python-version: '3.x' + cache: pip + cache-dependency-path: requirements*/*.txt - name: cache mypy - uses: actions/cache@v4.0.0 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 with: path: ./.mypy_cache - key: mypy|${{ matrix.python }}|${{ hashFiles('pyproject.toml') }} - if: matrix.tox == 'typing' + key: mypy|${{ hashFiles('pyproject.toml') }} - run: pip install tox - - run: tox -e ${{ matrix.tox }} + - run: tox run -e typing diff --git a/.gitignore b/.gitignore index a8c7343d..d8520fad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,11 @@ -*~ -venv/ +.idea/ +.vscode/ +.venv*/ +venv*/ __pycache__/ -Quart.egg-info/ -.cache/ +dist/ +.coverage* +htmlcov/ .tox/ -TODO -.mypy_cache/ -.pytest_cache/ -.hypothesis/ +docs/reference/source docs/_build/ -docs/reference/source/ -dist/ -.coverage -poetry.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 766101ee..7db182a0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,32 +1,14 @@ repos: - - repo: https://github.com/asottile/pyupgrade - rev: v3.9.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.7.3 hooks: - - id: pyupgrade - args: ["--py38-plus"] - - repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - - repo: https://github.com/psf/black - rev: 23.7.0 - hooks: - - id: black - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 - hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear - - flake8-implicit-str-concat - - repo: https://github.com/peterdemin/pip-compile-multi - rev: v2.6.3 - hooks: - - id: pip-compile-multi-verify + - id: ruff + - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: + - id: check-merge-conflict + - id: debug-statements - id: fix-byte-order-marker - id: trailing-whitespace - id: end-of-file-fixer - exclude: "^tests/.*.http$" diff --git a/.readthedocs.yaml b/.readthedocs.yaml index e20d0c2c..36673e8c 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,16 +1,13 @@ version: 2 - build: os: ubuntu-22.04 tools: - python: "3.12" - + python: '3.12' python: install: + - requirements: requirements/docs.txt - method: pip path: . - extra_requirements: - - docs - sphinx: - configuration: docs/conf.py + builder: html + fail_on_warning: false diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index f4ba197d..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,76 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at report@palletsprojects.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..f79d5e81 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,108 @@ +# Contributing Quick Reference + +This document assumes you have some familiarity with Git, GitHub, and Python +virutalenvs. We are working on a more thorough guide about the different ways to +contribute with in depth explanations, which will be available soon. + +These instructions will work with at least Bash and PowerShell, and should work +on other shells. On Windows, use PowerShell, not CMD. + +You need Python and Git installed, as well as the [GitHub CLI]. Log in with +`gh auth login`. Choose and install an editor; we suggest [PyCharm] or +[VS Code]. + +[GitHub CLI]: https://cli.github.com/ +[PyCharm]: https://www.jetbrains.com/pycharm/ +[VS Code]: https://code.visualstudio.com/ + +## Set Up the Repository + +Fork and clone the project's repository ("pallets/flask" for example). To work +on a bug or documentation fix, switch to the "stable" branch (if the project has +it), otherwise switch to the "main" branch. To update this target branch, pull +from "upstream". Create a work branch with a short descriptive name. + +``` +$ gh repo fork --clone pallets/flask +$ cd flask +$ git switch stable +$ git pull upstream +$ git switch -c short-descriptive-name +``` + +## Install Development Dependencies + +Create a virtualenv and activate it. Install the dev dependencies, and the +project in editable mode. Install the pre-commit hooks. + +Create a virtualenv (Mac and Linux): + +``` +$ python3 -m venv .venv +$ . .venv/bin/activate +``` + +Create a virtualenv (Windows): + +``` +> py -m venv .venv +> .\.venv\Scripts\activate +``` + +Install (all platforms): + +``` +$ pip install -r requirements/dev.txt && pip install -e . +$ pre-commit install --install-hooks +``` + +Any time you open a new terminal, you need to activate the virtualenv again. If +you've pulled from upstream recently, you can re-run the `pip` command above to +get the current dev dependencies. + +## Run Tests + +These are the essential test commands you can run while developing: + +* `pytest` - Run the unit tests. +* `mypy` - Run the main type checker. +* `tox run -e docs` - Build the documentation. + +These are some more specific commands if you need them: + +* `tox parallel` - Run all test environments that will be run in CI, in + parallel. Python versions that are not installed are skipped. +* `pre-commit` - Run the linter and formatter tools. Only runs against changed + files that have been staged with `git add -u`. This will run automatically + before each commit. +* `pre-commit run --all-files` - Run the pre-commit hooks against all files, + including unchanged and unstaged. +* `tox run -e py3.11` - Run unit tests with a specific Python version. The + version must be installed. `-e pypy` will run against PyPy. +* `pyright` - A second type checker. +* `tox run -e typing` - Run all typing checks. This includes `pyright` and its + export check as well. +* `python -m http.server -b 127.0.0.1 -d docs/_build/html` - Serve the + documentation. + +## Create a Pull Request + +Make your changes and commit them. Add tests that demonstrate that your code +works, and ensure all tests pass. Change documentation if needed to reflect your +change. Adding a changelog entry is optional, a maintainer will write one if +you're not sure how to. Add the entry to the end of the relevant section, match +the writing and formatting style of existing entries. Don't add an entry for +changes that only affect documentation or project internals. + +Use the GitHub CLI to start creating your pull request. Specify the target +branch with `-B`. The "stable" branch is the target for bug and documentation +fixes, otherwise the target is "main". + +``` +$ gh pr create --web --base stable +``` + +CI will run after you create the PR. If CI fails, you can click to see the logs +and address those failures, pushing new commits. Once you feel your PR is ready, +click the "Ready for review" button. A maintainer will review and merge the PR +when they are available. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index 7997c055..00000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,186 +0,0 @@ -How to contribute to Quart -========================== - -Thank you for considering contributing to Quart! - - -Support questions ------------------ - -Please don't use the issue tracker for this. The issue tracker is a -tool to address bugs and feature requests in Quart itself. Use one of -the following resources for questions about using Quart or issues -with your own code: - -- The ``#get-help`` channel on our Discord chat: - https://discord.gg/pallets -- Ask on `Stack Overflow`_. Search with Google first using: - ``site:stackoverflow.com quart {search term, exception message, etc.}`` - -.. _Stack Overflow: https://stackoverflow.com/questions/tagged/quart?tab=Frequent - - -Reporting issues ----------------- - -Include the following information in your post: - -- Describe what you expected to happen. -- If possible, include a `minimal reproducible example`_ to help us - identify the issue. This also helps check that the issue is not with - your own code. -- Describe what actually happened. Include the full traceback if there - was an exception. -- List your Python and Quart versions. If possible, check if this - issue is already fixed in the latest releases or the latest code in - the repository. - -.. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example - - -Submitting patches ------------------- - -If there is not an open issue for what you want to submit, prefer -opening one for discussion before working on a PR. You can work on any -issue that doesn't have an open PR linked to it or a maintainer assigned -to it. These show up in the sidebar. No need to ask if you can work on -an issue that interests you. - -Include the following in your patch: - -- Use `Black`_ to format your code. This and other tools will run - automatically if you install `pre-commit`_ using the instructions - below. -- Include tests if your patch adds or changes code. Make sure the test - fails without your patch. -- Update any relevant docs pages and docstrings. Docs pages and - docstrings should be wrapped at 72 characters. -- Add an entry in ``CHANGES.rst``. Use the same style as other - entries. Also include ``.. versionchanged::`` inline changelogs in - relevant docstrings. - -.. _Black: https://black.readthedocs.io -.. _pre-commit: https://pre-commit.com - - -First time setup -~~~~~~~~~~~~~~~~ - -- Download and install the `latest version of git`_. -- Configure git with your `username`_ and `email`_. - - .. code-block:: text - - $ git config --global user.name 'your name' - $ git config --global user.email 'your email' - -- Make sure you have a `GitHub account`_. -- Fork Quart to your GitHub account by clicking the `Fork`_ button. -- `Clone`_ the main repository locally. - - .. code-block:: text - - $ git clone https://github.com/pallets/quart - $ cd quart - -- Add your fork as a remote to push your work to. Replace - ``{username}`` with your username. This names the remote "fork", the - default Pallets remote is "origin". - - .. code-block:: text - - $ git remote add fork https://github.com/{username}/quart - -- Create a virtualenv. - - .. code-block:: text - - $ python3 -m venv env - $ . env/bin/activate - - On Windows, activating is different. - - .. code-block:: text - - > env\Scripts\activate - -- Install tox. - - .. code-block:: text - - $ python -m pip install --upgrade tox - -- Install the pre-commit hooks. - - .. code-block:: text - - $ pre-commit install - -.. _latest version of git: https://git-scm.com/downloads -.. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git -.. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address -.. _GitHub account: https://github.com/join -.. _Fork: https://github.com/pallets/quart/fork -.. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork - - -Start coding -~~~~~~~~~~~~ - -- Create a branch to identify the issue you would like to work - on. Branch off of the "main" branch. - - .. code-block:: text - - $ git fetch origin - $ git checkout -b your-branch-name origin/main - -- Using your favorite editor, make your changes, - `committing as you go`_. -- Include tests that cover any code changes you make. Make sure the - test fails without your patch. Run the tests as described below. -- Push your commits to your fork on GitHub and - `create a pull request`_. Link to the issue being addressed with - ``fixes #123`` in the pull request. - - .. code-block:: text - - $ git push --set-upstream fork your-branch-name - -.. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes -.. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request - - -Running the tests -~~~~~~~~~~~~~~~~~ - -Run the basic test suite with pytest. - -.. code-block:: text - - $ pytest - -This runs the tests for the current environment, which is usually -sufficient. CI will run the full suite when you submit your pull -request. You can run the full test suite with tox if you don't want to -wait. - -.. code-block:: text - - $ tox - - -Building the docs -~~~~~~~~~~~~~~~~~ - -Build the docs in the ``docs`` directory using Sphinx. - -.. code-block:: text - - $ cd docs - $ make html - -Open ``_build/html/index.html`` in your browser to view the docs. - -Read more about `Sphinx `__. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 64177813..00000000 --- a/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -Copyright P G Jones 2017. - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..45dc5800 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +MIT License + +Copyright 2017 Pallets + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..7426c592 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# Quart + +![](https://raw.githubusercontent.com/pallets/quart/main/artwork/logo.png) + +Quart is an async Python web application framework. Using Quart you can, + +- render and serve HTML templates, +- write (RESTful) JSON APIs, +- serve WebSockets, +- stream request and response data, +- do pretty much anything over the HTTP or WebSocket protocols. + +## Quickstart + +Install from PyPI using an installer such as pip. + +``` +$ pip install quart +``` + +Save the following as `app.py`. This shows off rendering a template, returning +JSON data, and using a WebSocket. + +```python +from quart import Quart, render_template, websocket + +app = Quart(__name__) + +@app.route("/") +async def hello(): + return await render_template("index.html") + +@app.route("/api") +async def json(): + return {"hello": "world"} + +@app.websocket("/ws") +async def ws(): + while True: + await websocket.send("hello") + await websocket.send_json({"hello": "world"}) +``` + +``` +$ quart run + * Running on http://127.0.0.1:5000 (CTRL + C to quit) +``` + +To deploy this app in a production setting see the [deployment] documentation. + +[deployment]: https://quart.palletsprojects.com/en/latest/tutorials/deployment.html + +## Contributing + +Quart is developed on [GitHub]. If you come across a bug, or have a feature +request, please open an [issue]. To contribute a fix or implement a feature, +follow our [contributing guide]. + +[GitHub]: https://github.com/pallets/quart +[issue]: https://github.com/pallets/quart/issues +[contributing guide]: https://github.com/pallets/quart/CONTRIBUTING.rst + +## Help + +If you need help with your code, the Quart [documentation] and [cheatsheet] are +the best places to start. You can ask for help on the [Discussions tab] or on +our [Discord chat]. + +[documentation]: https://quart.palletsprojects.com +[cheatsheet]: https://quart.palletsprojects.com/en/latest/reference/cheatsheet.html +[Discussions tab]: https://github.com/pallets/quart/discussions +[Discord chat]: https://discord.gg + +## Relationship with Flask + +Quart is an asyncio reimplementation of the popular [Flask] web application +framework. This means that if you understand Flask you understand Quart. + +Like Flask, Quart has an ecosystem of extensions for more specific needs. In +addition, a number of the Flask extensions work with Quart. + +[Flask]: https://flask.palletsprojects.com + +### Migrating from Flask + +It should be possible to migrate to Quart from Flask by a find and replace of +`flask` to `quart` and then adding `async` and `await` keywords. See the +[migration] documentation for more help. + +[migration]: https://quart.palletsprojects.com/en/latest/how_to_guides/flask_migration.html diff --git a/README.rst b/README.rst deleted file mode 100644 index 80d86ea0..00000000 --- a/README.rst +++ /dev/null @@ -1,137 +0,0 @@ -Quart -===== - -.. image:: https://raw.githubusercontent.com/pallets/quart/main/artwork/logo.png - :alt: Quart logo - -|Build Status| |docs| |pypi| |python| |license| |chat| - -Quart is an async Python web microframework. Using Quart you can, - -* render and serve HTML templates, -* write (RESTful) JSON APIs, -* serve WebSockets, -* stream request and response data, -* do pretty much anything over the HTTP or WebSocket protocols. - -Quickstart ----------- - -Quart can be installed via `pip -`_, - -.. code-block:: console - - $ pip install quart - -and requires Python 3.9.0 or higher (see `python version support -`_ -for reasoning). - -A minimal Quart example is, - -.. code-block:: python - - from quart import Quart, render_template, websocket - - app = Quart(__name__) - - @app.route("/") - async def hello(): - return await render_template("index.html") - - @app.route("/api") - async def json(): - return {"hello": "world"} - - @app.websocket("/ws") - async def ws(): - while True: - await websocket.send("hello") - await websocket.send_json({"hello": "world"}) - - if __name__ == "__main__": - app.run() - -if the above is in a file called ``app.py`` it can be run as, - -.. code-block:: console - - $ python app.py - -To deploy this app in a production setting see the `deployment -`_ -documentation. - -Contributing ------------- - -Quart is developed on `GitHub `_. If -you come across an issue, or have a feature request please open an -`issue `_. If you want to -contribute a fix or the feature-implementation please do (typo fixes -welcome), by proposing a `merge request -`_. - -Testing -^^^^^^^ - -The best way to test Quart is with `Tox -`_, - -.. code-block:: console - - $ pip install tox - $ tox - -this will check the code style and run the tests. - -Help ----- - -The Quart `documentation `_ or -`cheatsheet -`_ -are the best places to start, after that try searching `stack overflow -`_ or ask for help -`on discord `_. If you still -can't find an answer please `open an issue -`_. - -Relationship with Flask ------------------------ - -Quart is an asyncio reimplementation of the popular `Flask -`_ microframework API. This means that if you -understand Flask you understand Quart. - -Like Flask, Quart has an ecosystem of extensions for more specific -needs. In addition a number of the Flask extensions work with Quart. - -Migrating from Flask -^^^^^^^^^^^^^^^^^^^^ - -It should be possible to migrate to Quart from Flask by a find and -replace of ``flask`` to ``quart`` and then adding ``async`` and -``await`` keywords. See the `docs -`_ -for more help. - - -.. |Build Status| image:: https://github.com/pallets/quart/actions/workflows/tests.yaml/badge.svg - :target: https://github.com/pallets/quart/commits/main - -.. |docs| image:: https://img.shields.io/badge/docs-passing-brightgreen.svg - :target: https://quart.palletsprojects.com - -.. |pypi| image:: https://img.shields.io/pypi/v/quart.svg - :target: https://pypi.python.org/pypi/Quart/ - -.. |python| image:: https://img.shields.io/pypi/pyversions/quart.svg - :target: https://pypi.python.org/pypi/Quart/ - -.. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg - :target: https://github.com/pallets/quart/blob/main/LICENSE - -.. |chat| image:: https://img.shields.io/badge/chat-join_now-brightgreen.svg - :target: https://discord.gg/pallets diff --git a/artwork/LICENSE b/artwork/LICENSE deleted file mode 100644 index dbb57d05..00000000 --- a/artwork/LICENSE +++ /dev/null @@ -1,12 +0,0 @@ -CC0 1.0 Universal (CC0 1.0) - -The Quart logo is Copyright © 2017 Vic Shóstak - -CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE -LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN -ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS -INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES -REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS -PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM -THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED -HEREUNDER. diff --git a/docs/changes.rst b/docs/changes.rst new file mode 100644 index 00000000..955deaf2 --- /dev/null +++ b/docs/changes.rst @@ -0,0 +1,4 @@ +Changes +======= + +.. include:: ../CHANGES.rst diff --git a/docs/conf.py b/docs/conf.py index ee34c93f..eb5a59f7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,96 +1,33 @@ -#!/usr/bin/env python3 -# -# Quart documentation build configuration file, created by -# sphinx-quickstart on Sun May 21 14:18:44 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# +import importlib.metadata import os -import sys -from importlib.metadata import version as meta_version from sphinx.ext import apidoc -sys.path.insert(0, os.path.abspath("../")) +# Project -------------------------------------------------------------- -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon"] - -# Add any paths that contain templates here, relative to this directory. -# templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = ".rst" - -# The master toctree document. -master_doc = "index" - -# General information about the project. project = "Quart" -copyright = "2017-2022 Philip Jones" -author = "Philip Jones" - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = meta_version("quart") -# The full version, including alpha/beta/rc tags. -release = version - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = "en" - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" +copyright = "2017 Pallets" +version = release = importlib.metadata.version("quart").partition(".dev")[0] -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False +# General -------------------------------------------------------------- -# -- Options for HTML output ---------------------------------------------- +default_role = "code" +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "myst_parser", +] +autodoc_member_order = "bysource" +autodoc_typehints = "description" +autodoc_preserve_defaults = True +myst_enable_extensions = [ + "fieldlist", +] +myst_heading_anchors = 2 -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# +# HTML ----------------------------------------------------------------- html_theme = "pydata_sphinx_theme" -html_logo = "_static/logo_short.png" - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = { "external_links": [ {"name": "Source code", "url": "https://github.com/pallets/quart"}, @@ -104,92 +41,25 @@ }, ], } - -html_sidebars = { - "index": [], -} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +html_logo = "_static/logo_short.png" -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = "Quartdoc" - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, "Quart.tex", "Quart Documentation", "Philip Jones", "manual"), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "quart", "Quart Documentation", [author], 1)] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - "Quart", - "Quart Documentation", - author, - "Quart", - "One line description of project.", - "Miscellaneous", - ), -] - - -# generate API documentation via sphinx-apidoc -# see: https://www.sphinx-doc.org/en/master/ext/apidoc.html def run_apidoc(_): + # generate API documentation via sphinx-apidoc + # https://www.sphinx-doc.org/en/master/man/sphinx-apidoc.html base_path = os.path.abspath(os.path.dirname(__file__)) - - argv = [ - "-f", - "-e", - "-o", - f"{base_path}/reference/source", - f"{base_path}/../src/quart", - f"{base_path}/../src/quart/datastructures.py", - ] - - apidoc.main(argv) + apidoc.main( + [ + "-f", + "-e", + "-o", + f"{base_path}/reference/source", + f"{base_path}/../src/quart", + f"{base_path}/../src/quart/datastructures.py", + ] + ) def setup(app): - app.add_css_file("css/quart.css") - - # generate API documentation app.connect("builder-inited", run_apidoc) diff --git a/docs/index.rst b/docs/index.rst index 9dc65282..44080441 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -89,6 +89,8 @@ References ---------- .. toctree:: - :maxdepth: 2 + :maxdepth: 2 - reference/index.rst + reference/index + license + changes diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 00000000..4e69731d --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,5 @@ +MIT License +=========== + +.. literalinclude:: ../LICENSE.txt + :language: text diff --git a/pyproject.toml b/pyproject.toml index 73ef1a2e..6016a8f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,96 +1,109 @@ -[tool.poetry] +[project] name = "Quart" version = "0.20.0.dev" -description = "A Python ASGI web microframework with the same API as Flask" -authors = ["pgjones "] +description = "A Python ASGI web framework with the same API as Flask" +readme = "README.md" +license = {text = "MIT"} +authors = [{name = "pgjones", email = "philip.graham.jones@googlemail.com"}] +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Web Environment", + "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", - "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Typing :: Typed", +] +requires-python = ">=3.9" +dependencies = [ + "aiofiles", + "blinker>=1.6", + "click>=8.0", + "flask>=3.0", + "hypercorn>=0.11.2", + "importlib-metadata; python_version < '3.10'", + "itsdangerous", + "jinja2", + "markupsafe", + "typing-extensions; python_version < '3.10'", + "werkzeug>=3.0", ] -include = ["src/quart_auth/py.typed"] -license = "MIT" -readme = "README.rst" -repository = "https://github.com/pallets/quart/" -documentation = "https://quart.palletsprojects.com" -[tool.poetry.dependencies] -python = ">=3.9" -aiofiles = "*" -blinker = ">=1.6" -click = ">=8.0.0" -flask = ">=3.0.0" -hypercorn = ">=0.11.2" -importlib_metadata = { version = "*", python = "<3.10" } -itsdangerous = "*" -jinja2 = "*" -markupsafe = "*" -pydata_sphinx_theme = { version = "*", optional = true } -python-dotenv = { version = "*", optional = true } -typing_extensions = { version = "*", python = "<3.10" } -werkzeug = ">=3.0.0" +[project.urls] +Donate = "https://palletsprojects.com/donate" +Documentation = "https://quart.palletsprojects.com/" +Changes = "https://quart.palletsprojects.com/en/latest/changes.html" +Source = "https://github.com/pallets/quart/" +Chat = "https://discord.gg/pallets" -[tool.poetry.dev-dependencies] -hypothesis = "*" -pytest = "*" -pytest-asyncio = "*" +[project.optional-dependencies] +dotenv = ["python-dotenv"] -[tool.poetry.scripts] +[project.scripts] quart = "quart.cli:main" -[tool.poetry.extras] -docs = ["pydata_sphinx_theme"] -dotenv = ["python-dotenv"] +[build-system] +requires = ["flit-core<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "quart" + +[tool.pytest.ini_options] +addopts = "--no-cov-on-fail --showlocals --strict-markers" +asyncio_default_fixture_loop_scope = "session" +asyncio_mode = "auto" +testpaths = ["tests"] -[tool.black] -line-length = 100 -target-version = ["py39"] +[tool.coverage.run] +branch = true +source = ["quart", "tests"] -[tool.isort] -combine_as_imports = true -force_grid_wrap = 0 -include_trailing_comma = true -known_first_party = "quart, tests" -line_length = 100 -multi_line_output = 3 -no_lines_before = "LOCALFOLDER" -order_by_type = false -reverse_relative = true +[tool.coverage.paths] +source = ["src", "*/site-packages"] [tool.mypy] +python_version = "3.9" +files = ["src/quart", "tests"] +show_error_codes = true +pretty = true +strict = true +# TODO fully satisfy strict mode and remove these customizations allow_redefinition = true disallow_any_generics = false -disallow_subclassing_any = true disallow_untyped_calls = false -disallow_untyped_defs = true implicit_reexport = true no_implicit_optional = true -show_error_codes = true -strict = true -strict_equality = true strict_optional = false -warn_redundant_casts = true warn_return_any = false -warn_unused_configs = true -warn_unused_ignores = true -[tool.pytest.ini_options] -addopts = "--no-cov-on-fail --showlocals --strict-markers" -asyncio_default_fixture_loop_scope = "session" -asyncio_mode = "auto" -testpaths = ["tests"] +[tool.pyright] +pythonVersion = "3.9" +include = ["src/quart", "tests"] +typeCheckingMode = "basic" -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.ruff] +src = ["src"] +fix = true +show-fixes = true +output-format = "full" + +[tool.ruff.lint] +select = [ + "B", # flake8-bugbear + "E", # pycodestyle error + "F", # pyflakes + "FA", # flake8-future-annotations + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "W", # pycodestyle warning +] + +[tool.ruff.lint.isort] +force-single-line = true +order-by-type = false diff --git a/requirements/build.in b/requirements/build.in new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/requirements/build.in @@ -0,0 +1 @@ +build diff --git a/requirements/build.txt b/requirements/build.txt new file mode 100644 index 00000000..0079156a --- /dev/null +++ b/requirements/build.txt @@ -0,0 +1,8 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile build.in -o build.txt +build==1.2.2.post1 + # via -r build.in +packaging==24.2 + # via build +pyproject-hooks==1.2.0 + # via build diff --git a/requirements/dev.in b/requirements/dev.in new file mode 100644 index 00000000..4f74ca47 --- /dev/null +++ b/requirements/dev.in @@ -0,0 +1,6 @@ +-r docs.txt +-r tests.txt +-r typing.txt +pre-commit +tox +tox-uv diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 00000000..de7b77f8 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,244 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile dev.in -o dev.txt +accessible-pygments==0.0.5 + # via + # -r docs.txt + # pydata-sphinx-theme +alabaster==1.0.0 + # via + # -r docs.txt + # sphinx +attrs==24.2.0 + # via + # -r tests.txt + # -r typing.txt + # hypothesis +babel==2.16.0 + # via + # -r docs.txt + # pydata-sphinx-theme + # sphinx +beautifulsoup4==4.12.3 + # via + # -r docs.txt + # pydata-sphinx-theme +cachetools==5.5.0 + # via tox +certifi==2024.8.30 + # via + # -r docs.txt + # requests +cfgv==3.4.0 + # via pre-commit +chardet==5.2.0 + # via tox +charset-normalizer==3.4.0 + # via + # -r docs.txt + # requests +colorama==0.4.6 + # via tox +coverage==7.6.5 + # via + # -r tests.txt + # pytest-cov +distlib==0.3.9 + # via virtualenv +docutils==0.21.2 + # via + # -r docs.txt + # myst-parser + # pydata-sphinx-theme + # sphinx +filelock==3.16.1 + # via + # tox + # virtualenv +hypothesis==6.118.8 + # via + # -r tests.txt + # -r typing.txt +identify==2.6.2 + # via pre-commit +idna==3.10 + # via + # -r docs.txt + # requests +imagesize==1.4.1 + # via + # -r docs.txt + # sphinx +iniconfig==2.0.0 + # via + # -r tests.txt + # -r typing.txt + # pytest +jinja2==3.1.4 + # via + # -r docs.txt + # myst-parser + # sphinx +markdown-it-py==3.0.0 + # via + # -r docs.txt + # mdit-py-plugins + # myst-parser +markupsafe==3.0.2 + # via + # -r docs.txt + # jinja2 +mdit-py-plugins==0.4.2 + # via + # -r docs.txt + # myst-parser +mdurl==0.1.2 + # via + # -r docs.txt + # markdown-it-py +mypy==1.13.0 + # via -r typing.txt +mypy-extensions==1.0.0 + # via + # -r typing.txt + # mypy +myst-parser==4.0.0 + # via -r docs.txt +nodeenv==1.9.1 + # via + # -r typing.txt + # pre-commit + # pyright +packaging==24.2 + # via + # -r docs.txt + # -r tests.txt + # -r typing.txt + # pyproject-api + # pytest + # pytest-sugar + # sphinx + # tox + # tox-uv +platformdirs==4.3.6 + # via + # tox + # virtualenv +pluggy==1.5.0 + # via + # -r tests.txt + # -r typing.txt + # pytest + # tox +pre-commit==4.0.1 + # via -r dev.in +pydata-sphinx-theme==0.16.0 + # via -r docs.txt +pygments==2.18.0 + # via + # -r docs.txt + # accessible-pygments + # pydata-sphinx-theme + # sphinx +pyproject-api==1.8.0 + # via tox +pyright==1.1.389 + # via -r typing.txt +pytest==8.3.3 + # via + # -r tests.txt + # -r typing.txt + # pytest-asyncio + # pytest-cov + # pytest-sugar +pytest-asyncio==0.24.0 + # via + # -r tests.txt + # -r typing.txt +pytest-cov==6.0.0 + # via -r tests.txt +pytest-sugar==1.0.0 + # via -r tests.txt +python-dotenv==1.0.1 + # via + # -r tests.txt + # -r typing.txt +pyyaml==6.0.2 + # via + # -r docs.txt + # myst-parser + # pre-commit +requests==2.32.3 + # via + # -r docs.txt + # sphinx +snowballstemmer==2.2.0 + # via + # -r docs.txt + # sphinx +sortedcontainers==2.4.0 + # via + # -r tests.txt + # -r typing.txt + # hypothesis +soupsieve==2.6 + # via + # -r docs.txt + # beautifulsoup4 +sphinx==8.1.3 + # via + # -r docs.txt + # myst-parser + # pydata-sphinx-theme +sphinxcontrib-applehelp==2.0.0 + # via + # -r docs.txt + # sphinx +sphinxcontrib-devhelp==2.0.0 + # via + # -r docs.txt + # sphinx +sphinxcontrib-htmlhelp==2.1.0 + # via + # -r docs.txt + # sphinx +sphinxcontrib-jsmath==1.0.1 + # via + # -r docs.txt + # sphinx +sphinxcontrib-qthelp==2.0.0 + # via + # -r docs.txt + # sphinx +sphinxcontrib-serializinghtml==2.0.0 + # via + # -r docs.txt + # sphinx +termcolor==2.5.0 + # via + # -r tests.txt + # pytest-sugar +tox==4.23.2 + # via + # -r dev.in + # tox-uv +tox-uv==1.16.0 + # via -r dev.in +types-aiofiles==24.1.0.20240626 + # via -r typing.txt +typing-extensions==4.12.2 + # via + # -r docs.txt + # -r typing.txt + # mypy + # pydata-sphinx-theme + # pyright +urllib3==2.2.3 + # via + # -r docs.txt + # requests +uv==0.5.1 + # via tox-uv +virtualenv==20.27.1 + # via + # pre-commit + # tox diff --git a/requirements/docs.in b/requirements/docs.in new file mode 100644 index 00000000..59cc7cd4 --- /dev/null +++ b/requirements/docs.in @@ -0,0 +1,3 @@ +myst-parser +pydata-sphinx-theme +sphinx diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 00000000..861a95b4 --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1,79 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile docs.in -o docs.txt +accessible-pygments==0.0.5 + # via pydata-sphinx-theme +alabaster==1.0.0 + # via sphinx +babel==2.16.0 + # via + # pydata-sphinx-theme + # sphinx +beautifulsoup4==4.12.3 + # via pydata-sphinx-theme +certifi==2024.8.30 + # via requests +charset-normalizer==3.4.0 + # via requests +docutils==0.21.2 + # via + # myst-parser + # pydata-sphinx-theme + # sphinx +idna==3.10 + # via requests +imagesize==1.4.1 + # via sphinx +jinja2==3.1.4 + # via + # myst-parser + # sphinx +markdown-it-py==3.0.0 + # via + # mdit-py-plugins + # myst-parser +markupsafe==3.0.2 + # via jinja2 +mdit-py-plugins==0.4.2 + # via myst-parser +mdurl==0.1.2 + # via markdown-it-py +myst-parser==4.0.0 + # via -r docs.in +packaging==24.2 + # via sphinx +pydata-sphinx-theme==0.16.0 + # via -r docs.in +pygments==2.18.0 + # via + # accessible-pygments + # pydata-sphinx-theme + # sphinx +pyyaml==6.0.2 + # via myst-parser +requests==2.32.3 + # via sphinx +snowballstemmer==2.2.0 + # via sphinx +soupsieve==2.6 + # via beautifulsoup4 +sphinx==8.1.3 + # via + # -r docs.in + # myst-parser + # pydata-sphinx-theme +sphinxcontrib-applehelp==2.0.0 + # via sphinx +sphinxcontrib-devhelp==2.0.0 + # via sphinx +sphinxcontrib-htmlhelp==2.1.0 + # via sphinx +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==2.0.0 + # via sphinx +sphinxcontrib-serializinghtml==2.0.0 + # via sphinx +typing-extensions==4.12.2 + # via pydata-sphinx-theme +urllib3==2.2.3 + # via requests diff --git a/requirements/tests.in b/requirements/tests.in new file mode 100644 index 00000000..097552f8 --- /dev/null +++ b/requirements/tests.in @@ -0,0 +1,6 @@ +hypothesis +pytest +pytest-asyncio +pytest-cov +pytest-sugar +python-dotenv diff --git a/requirements/tests.txt b/requirements/tests.txt new file mode 100644 index 00000000..7a97bacb --- /dev/null +++ b/requirements/tests.txt @@ -0,0 +1,34 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile tests.in -o tests.txt +attrs==24.2.0 + # via hypothesis +coverage==7.6.5 + # via pytest-cov +hypothesis==6.118.8 + # via -r tests.in +iniconfig==2.0.0 + # via pytest +packaging==24.2 + # via + # pytest + # pytest-sugar +pluggy==1.5.0 + # via pytest +pytest==8.3.3 + # via + # -r tests.in + # pytest-asyncio + # pytest-cov + # pytest-sugar +pytest-asyncio==0.24.0 + # via -r tests.in +pytest-cov==6.0.0 + # via -r tests.in +pytest-sugar==1.0.0 + # via -r tests.in +python-dotenv==1.0.1 + # via -r tests.in +sortedcontainers==2.4.0 + # via hypothesis +termcolor==2.5.0 + # via pytest-sugar diff --git a/requirements/typing.in b/requirements/typing.in new file mode 100644 index 00000000..c1d6c5ad --- /dev/null +++ b/requirements/typing.in @@ -0,0 +1,7 @@ +mypy +hypothesis +pyright +pytest +pytest-asyncio +types-aiofiles +python-dotenv diff --git a/requirements/typing.txt b/requirements/typing.txt new file mode 100644 index 00000000..3b6f24cb --- /dev/null +++ b/requirements/typing.txt @@ -0,0 +1,36 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile typing.in -o typing.txt +attrs==24.2.0 + # via hypothesis +hypothesis==6.118.8 + # via -r typing.in +iniconfig==2.0.0 + # via pytest +mypy==1.13.0 + # via -r typing.in +mypy-extensions==1.0.0 + # via mypy +nodeenv==1.9.1 + # via pyright +packaging==24.2 + # via pytest +pluggy==1.5.0 + # via pytest +pyright==1.1.389 + # via -r typing.in +pytest==8.3.3 + # via + # -r typing.in + # pytest-asyncio +pytest-asyncio==0.24.0 + # via -r typing.in +python-dotenv==1.0.1 + # via -r typing.in +sortedcontainers==2.4.0 + # via hypothesis +types-aiofiles==24.1.0.20240626 + # via -r typing.in +typing-extensions==4.12.2 + # via + # mypy + # pyright diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 1cec8c49..00000000 --- a/setup.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[flake8] -ignore = E203, E252, E704, FI58, W503, W504 -max_line_length = 100 -min_version = 3.9 -per-file-ignores = - src/quart/__init__.py:F401 -require_code = True diff --git a/tox.ini b/tox.ini index 333b62d9..900984bd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,76 +1,53 @@ [tox] envlist = + py3{13,12,11,10,9} + style + typing docs - format - mypy - package - pep8 - py39 - py310 - py311 - py312 - py313 -minversion = 3.3 -isolated_build = true [testenv] -deps = - hypothesis - pytest - pytest-asyncio - pytest-cov - pytest-sugar - python-dotenv -commands = pytest --cov=quart {posargs} +package = wheel +wheel_build_env = .pkg +constrain_package_deps = true +use_frozen_constraints = true +deps = -r requirements/tests.txt +commands = pytest -v --tb=short --basetemp={envtmpdir} --cov=quart {posargs} + +[testenv:style] +deps = pre-commit +skip_install = true +commands = pre-commit run --all-files + +[testenv:typing] +deps = -r requirements/typing.txt +# TODO test with pyright as well +commands = mypy [testenv:docs] -basepython = python3.13 -deps = - pydata-sphinx-theme - sphinx -commands = - sphinx-build -b html -d {envtmpdir}/doctrees docs/ docs/_build/html/ +deps = -r requirements/docs.txt +# TODO enable -W and fix warnings +commands = sphinx-build -E -b html docs docs/_build/html -[testenv:format] -basepython = python3.13 -deps = - black - isort -commands = - black --check --diff src/quart/ tests/ - isort --check --diff src/quart tests +[testenv:update-actions] +labels = update +deps = gha-update skip_install = true +commands = gha-update -[testenv:pep8] -basepython = python3.13 -deps = - flake8 - pep8-naming - flake8-future-import - flake8-print -commands = flake8 src/quart/ tests/ +[testenv:update-pre_commit] +labels = update +deps = pre-commit skip_install = true +commands = pre-commit autoupdate -j4 -[testenv:mypy] -basepython = python3.13 -deps = - flask - mypy - hypothesis - pytest - pytest-asyncio - pytest-cov - pytest-sugar - types-aiofiles - python-dotenv -commands = - mypy src/quart/ tests/ - -[testenv:package] -basepython = python3.13 -deps = - poetry - twine +[testenv:update-requirements] +labels = update +deps = uv +skip_install = true +change_dir = requirements commands = - poetry build - twine check dist/* + uv pip compile build.in -o build.txt -q {posargs:-U} + uv pip compile docs.in -o docs.txt -q {posargs:-U} + uv pip compile tests.in -o tests.txt -q {posargs:-U} + uv pip compile typing.in -o typing.txt -q {posargs:-U} + uv pip compile dev.in -o dev.txt -q {posargs:-U} From 506e402e86db8a5ba54e80a20cb9bae2400f2ea2 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 15 Nov 2024 11:22:24 -0800 Subject: [PATCH 2/3] apply ruff lint and format --- examples/api/src/api/__init__.py | 9 +- examples/api/tests/test_api.py | 3 +- examples/blog/src/blog/__init__.py | 7 +- examples/blog/tests/conftest.py | 3 +- examples/blog/tests/test_blog.py | 4 +- examples/chat/src/chat/__init__.py | 5 +- examples/chat/src/chat/broker.py | 2 +- examples/chat/tests/test_chat.py | 4 +- examples/video/src/video/__init__.py | 4 +- src/quart/__init__.py | 101 ++++---- src/quart/app.py | 340 ++++++++++++++++----------- src/quart/asgi.py | 129 +++++++--- src/quart/blueprints.py | 44 ++-- src/quart/cli.py | 46 +++- src/quart/config.py | 6 +- src/quart/ctx.py | 56 +++-- src/quart/datastructures.py | 3 +- src/quart/debug.py | 3 +- src/quart/formparser.py | 38 +-- src/quart/globals.py | 10 +- src/quart/helpers.py | 47 +++- src/quart/json/__init__.py | 4 +- src/quart/json/provider.py | 6 +- src/quart/json/tag.py | 22 +- src/quart/logging.py | 16 +- src/quart/routing.py | 15 +- src/quart/sessions.py | 34 ++- src/quart/templating.py | 53 +++-- src/quart/testing/__init__.py | 17 +- src/quart/testing/app.py | 21 +- src/quart/testing/client.py | 49 ++-- src/quart/testing/connections.py | 51 +++- src/quart/testing/utils.py | 39 ++- src/quart/typing.py | 88 +++---- src/quart/utils.py | 38 +-- src/quart/views.py | 19 +- src/quart/wrappers/__init__.py | 3 +- src/quart/wrappers/base.py | 3 +- src/quart/wrappers/request.py | 43 +++- src/quart/wrappers/response.py | 53 +++-- src/quart/wrappers/websocket.py | 12 +- tests/conftest.py | 3 +- tests/test_app.py | 52 +++- tests/test_asgi.py | 30 ++- tests/test_background_tasks.py | 3 +- tests/test_basic.py | 12 +- tests/test_blueprints.py | 26 +- tests/test_cli.py | 25 +- tests/test_ctx.py | 25 +- tests/test_exceptions.py | 3 +- tests/test_helpers.py | 52 ++-- tests/test_routing.py | 4 +- tests/test_sessions.py | 20 +- tests/test_sync.py | 6 +- tests/test_templating.py | 22 +- tests/test_testing.py | 58 +++-- tests/test_utils.py | 3 +- tests/test_views.py | 18 +- tests/wrappers/test_base.py | 30 ++- tests/wrappers/test_request.py | 14 +- tests/wrappers/test_response.py | 28 ++- 61 files changed, 1219 insertions(+), 665 deletions(-) diff --git a/examples/api/src/api/__init__.py b/examples/api/src/api/__init__.py index 905cc258..53d9ad81 100644 --- a/examples/api/src/api/__init__.py +++ b/examples/api/src/api/__init__.py @@ -1,9 +1,14 @@ +from __future__ import annotations + from dataclasses import dataclass from datetime import datetime -from quart_schema import QuartSchema, validate_request, validate_response +from quart_schema import QuartSchema +from quart_schema import validate_request +from quart_schema import validate_response -from quart import Quart, request +from quart import Quart +from quart import request app = Quart(__name__) QuartSchema(app) diff --git a/examples/api/tests/test_api.py b/examples/api/tests/test_api.py index 3ab1c2c3..709de0ea 100644 --- a/examples/api/tests/test_api.py +++ b/examples/api/tests/test_api.py @@ -1,4 +1,5 @@ -from api import app, TodoIn +from api import app +from api import TodoIn async def test_echo() -> None: diff --git a/examples/blog/src/blog/__init__.py b/examples/blog/src/blog/__init__.py index 9e5ae906..47cd8368 100644 --- a/examples/blog/src/blog/__init__.py +++ b/examples/blog/src/blog/__init__.py @@ -1,6 +1,11 @@ from sqlite3 import dbapi2 as sqlite3 -from quart import g, Quart, redirect, render_template, request, url_for +from quart import g +from quart import Quart +from quart import redirect +from quart import render_template +from quart import request +from quart import url_for app = Quart(__name__) diff --git a/examples/blog/tests/conftest.py b/examples/blog/tests/conftest.py index dfe3db50..bc449204 100644 --- a/examples/blog/tests/conftest.py +++ b/examples/blog/tests/conftest.py @@ -1,5 +1,6 @@ import pytest -from blog import app, init_db +from blog import app +from blog import init_db @pytest.fixture(autouse=True) diff --git a/examples/blog/tests/test_blog.py b/examples/blog/tests/test_blog.py index 5917057d..6f3e14fe 100644 --- a/examples/blog/tests/test_blog.py +++ b/examples/blog/tests/test_blog.py @@ -3,7 +3,9 @@ async def test_create_post(): test_client = app.test_client() - response = await test_client.post("/create/", form={"title": "Post", "text": "Text"}) + response = await test_client.post( + "/create/", form={"title": "Post", "text": "Text"} + ) assert response.status_code == 302 response = await test_client.get("/") text = await response.get_data() diff --git a/examples/chat/src/chat/__init__.py b/examples/chat/src/chat/__init__.py index 22df47b3..0d853d3c 100644 --- a/examples/chat/src/chat/__init__.py +++ b/examples/chat/src/chat/__init__.py @@ -1,8 +1,9 @@ import asyncio from chat.broker import Broker - -from quart import Quart, render_template, websocket +from quart import Quart +from quart import render_template +from quart import websocket app = Quart(__name__) broker = Broker() diff --git a/examples/chat/src/chat/broker.py b/examples/chat/src/chat/broker.py index c217bbc4..1db44996 100644 --- a/examples/chat/src/chat/broker.py +++ b/examples/chat/src/chat/broker.py @@ -1,5 +1,5 @@ import asyncio -from typing import AsyncGenerator +from collections.abc import AsyncGenerator class Broker: diff --git a/examples/chat/tests/test_chat.py b/examples/chat/tests/test_chat.py index 7124a3a1..496fcb8b 100644 --- a/examples/chat/tests/test_chat.py +++ b/examples/chat/tests/test_chat.py @@ -2,7 +2,9 @@ from chat import app -from quart.testing.connections import TestWebsocketConnection as _TestWebsocketConnection +from quart.testing.connections import ( + TestWebsocketConnection as _TestWebsocketConnection, +) async def _receive(test_websocket: _TestWebsocketConnection) -> str: diff --git a/examples/video/src/video/__init__.py b/examples/video/src/video/__init__.py index 0528409c..c58be49a 100644 --- a/examples/video/src/video/__init__.py +++ b/examples/video/src/video/__init__.py @@ -1,4 +1,6 @@ -from quart import Quart, render_template, send_file +from quart import Quart +from quart import render_template +from quart import send_file app = Quart(__name__) diff --git a/src/quart/__init__.py b/src/quart/__init__.py index 67d2dd7f..971f13b2 100644 --- a/src/quart/__init__.py +++ b/src/quart/__init__.py @@ -1,62 +1,55 @@ from __future__ import annotations -from markupsafe import escape as escape, Markup as Markup +from markupsafe import escape as escape +from markupsafe import Markup as Markup from .app import Quart as Quart from .blueprints import Blueprint as Blueprint from .config import Config as Config -from .ctx import ( - after_this_request as after_this_request, - copy_current_app_context as copy_current_app_context, - copy_current_request_context as copy_current_request_context, - copy_current_websocket_context as copy_current_websocket_context, - has_app_context as has_app_context, - has_request_context as has_request_context, - has_websocket_context as has_websocket_context, -) -from .globals import ( - current_app as current_app, - g as g, - request as request, - session as session, - websocket as websocket, -) -from .helpers import ( - abort as abort, - flash as flash, - get_flashed_messages as get_flashed_messages, - get_template_attribute as get_template_attribute, - make_push_promise as make_push_promise, - make_response as make_response, - redirect as redirect, - send_file as send_file, - send_from_directory as send_from_directory, - stream_with_context as stream_with_context, - url_for as url_for, -) +from .ctx import after_this_request as after_this_request +from .ctx import copy_current_app_context as copy_current_app_context +from .ctx import copy_current_request_context as copy_current_request_context +from .ctx import copy_current_websocket_context as copy_current_websocket_context +from .ctx import has_app_context as has_app_context +from .ctx import has_request_context as has_request_context +from .ctx import has_websocket_context as has_websocket_context +from .globals import current_app as current_app +from .globals import g as g +from .globals import request as request +from .globals import session as session +from .globals import websocket as websocket +from .helpers import abort as abort +from .helpers import flash as flash +from .helpers import get_flashed_messages as get_flashed_messages +from .helpers import get_template_attribute as get_template_attribute +from .helpers import make_push_promise as make_push_promise +from .helpers import make_response as make_response +from .helpers import redirect as redirect +from .helpers import send_file as send_file +from .helpers import send_from_directory as send_from_directory +from .helpers import stream_with_context as stream_with_context +from .helpers import url_for as url_for from .json import jsonify as jsonify -from .signals import ( - appcontext_popped as appcontext_popped, - appcontext_pushed as appcontext_pushed, - appcontext_tearing_down as appcontext_tearing_down, - before_render_template as before_render_template, - got_request_exception as got_request_exception, - got_websocket_exception as got_websocket_exception, - message_flashed as message_flashed, - request_finished as request_finished, - request_started as request_started, - request_tearing_down as request_tearing_down, - signals_available as signals_available, - template_rendered as template_rendered, - websocket_finished as websocket_finished, - websocket_started as websocket_started, - websocket_tearing_down as websocket_tearing_down, -) -from .templating import ( - render_template as render_template, - render_template_string as render_template_string, - stream_template as stream_template, - stream_template_string as stream_template_string, -) +from .signals import appcontext_popped as appcontext_popped +from .signals import appcontext_pushed as appcontext_pushed +from .signals import appcontext_tearing_down as appcontext_tearing_down +from .signals import before_render_template as before_render_template +from .signals import got_request_exception as got_request_exception +from .signals import got_websocket_exception as got_websocket_exception +from .signals import message_flashed as message_flashed +from .signals import request_finished as request_finished +from .signals import request_started as request_started +from .signals import request_tearing_down as request_tearing_down +from .signals import signals_available as signals_available +from .signals import template_rendered as template_rendered +from .signals import websocket_finished as websocket_finished +from .signals import websocket_started as websocket_started +from .signals import websocket_tearing_down as websocket_tearing_down +from .templating import render_template as render_template +from .templating import render_template_string as render_template_string +from .templating import stream_template as stream_template +from .templating import stream_template_string as stream_template_string from .typing import ResponseReturnValue as ResponseReturnValue -from .wrappers import Request as Request, Response as Response, Websocket as Websocket +from .wrappers import Request as Request +from .wrappers import Response as Response +from .wrappers import Websocket as Websocket diff --git a/src/quart/app.py b/src/quart/app.py index c14d30b8..6407dde0 100644 --- a/src/quart/app.py +++ b/src/quart/app.py @@ -6,24 +6,22 @@ import sys import warnings from collections import defaultdict +from collections.abc import AsyncGenerator +from collections.abc import Awaitable +from collections.abc import Coroutine from datetime import timedelta -from inspect import isasyncgen, iscoroutinefunction as _inspect_iscoroutinefunction, isgenerator +from inspect import isasyncgen +from inspect import iscoroutinefunction as _inspect_iscoroutinefunction +from inspect import isgenerator from types import TracebackType -from typing import ( - Any, - AnyStr, - AsyncGenerator, - Awaitable, - Callable, - cast, - Coroutine, - NoReturn, - Optional, - overload, - Set, - TypeVar, - Union, -) +from typing import Any +from typing import AnyStr +from typing import Callable +from typing import cast +from typing import NoReturn +from typing import Optional +from typing import overload +from typing import TypeVar from urllib.parse import quote from aiofiles import open as async_open @@ -32,94 +30,100 @@ from flask.sansio.scaffold import setupmethod from hypercorn.asyncio import serve from hypercorn.config import Config as HyperConfig -from hypercorn.typing import ASGIReceiveCallable, ASGISendCallable, Scope -from werkzeug.datastructures import Authorization, Headers, ImmutableDict -from werkzeug.exceptions import Aborter, BadRequestKeyError, HTTPException, InternalServerError -from werkzeug.routing import BuildError, MapAdapter, RoutingException +from hypercorn.typing import ASGIReceiveCallable +from hypercorn.typing import ASGISendCallable +from hypercorn.typing import Scope +from werkzeug.datastructures import Authorization +from werkzeug.datastructures import Headers +from werkzeug.datastructures import ImmutableDict +from werkzeug.exceptions import Aborter +from werkzeug.exceptions import BadRequestKeyError +from werkzeug.exceptions import HTTPException +from werkzeug.exceptions import InternalServerError +from werkzeug.routing import BuildError +from werkzeug.routing import MapAdapter +from werkzeug.routing import RoutingException from werkzeug.wrappers import Response as WerkzeugResponse -from .asgi import ASGIHTTPConnection, ASGILifespan, ASGIWebsocketConnection +from .asgi import ASGIHTTPConnection +from .asgi import ASGILifespan +from .asgi import ASGIWebsocketConnection from .cli import AppGroup from .config import Config -from .ctx import ( - _AppCtxGlobals, - AppContext, - has_request_context, - has_websocket_context, - RequestContext, - WebsocketContext, -) -from .globals import ( - _cv_app, - _cv_request, - _cv_websocket, - g, - request, - request_ctx, - session, - websocket, - websocket_ctx, -) -from .helpers import get_debug_flag, get_flashed_messages, send_from_directory -from .routing import QuartMap, QuartRule +from .ctx import _AppCtxGlobals +from .ctx import AppContext +from .ctx import has_request_context +from .ctx import has_websocket_context +from .ctx import RequestContext +from .ctx import WebsocketContext +from .globals import _cv_app +from .globals import _cv_request +from .globals import _cv_websocket +from .globals import g +from .globals import request +from .globals import request_ctx +from .globals import session +from .globals import websocket +from .globals import websocket_ctx +from .helpers import get_debug_flag +from .helpers import get_flashed_messages +from .helpers import send_from_directory +from .routing import QuartMap +from .routing import QuartRule from .sessions import SecureCookieSessionInterface -from .signals import ( - appcontext_tearing_down, - got_background_exception, - got_request_exception, - got_serving_exception, - got_websocket_exception, - request_finished, - request_started, - request_tearing_down, - websocket_finished, - websocket_started, - websocket_tearing_down, -) -from .templating import _default_template_ctx_processor, Environment -from .testing import ( - make_test_body_with_headers, - make_test_headers_path_and_query_string, - make_test_scope, - no_op_push, - QuartClient, - QuartCliRunner, - sentinel, - TestApp, -) -from .typing import ( - AfterServingCallable, - AfterWebsocketCallable, - ASGIHTTPProtocol, - ASGILifespanProtocol, - ASGIWebsocketProtocol, - BeforeServingCallable, - BeforeWebsocketCallable, - Event, - FilePath, - HeadersValue, - ResponseReturnValue, - ResponseTypes, - ShellContextProcessorCallable, - StatusCode, - TeardownCallable, - TemplateFilterCallable, - TemplateGlobalCallable, - TemplateTestCallable, - TestAppProtocol, - TestClientProtocol, - WebsocketCallable, - WhileServingCallable, -) -from .utils import ( - cancel_tasks, - file_path_to_path, - MustReloadError, - observe_changes, - restart, - run_sync, -) -from .wrappers import BaseRequestWebsocket, Request, Response, Websocket +from .signals import appcontext_tearing_down +from .signals import got_background_exception +from .signals import got_request_exception +from .signals import got_serving_exception +from .signals import got_websocket_exception +from .signals import request_finished +from .signals import request_started +from .signals import request_tearing_down +from .signals import websocket_finished +from .signals import websocket_started +from .signals import websocket_tearing_down +from .templating import _default_template_ctx_processor +from .templating import Environment +from .testing import make_test_body_with_headers +from .testing import make_test_headers_path_and_query_string +from .testing import make_test_scope +from .testing import no_op_push +from .testing import QuartClient +from .testing import QuartCliRunner +from .testing import sentinel +from .testing import TestApp +from .typing import AfterServingCallable +from .typing import AfterWebsocketCallable +from .typing import ASGIHTTPProtocol +from .typing import ASGILifespanProtocol +from .typing import ASGIWebsocketProtocol +from .typing import BeforeServingCallable +from .typing import BeforeWebsocketCallable +from .typing import Event +from .typing import FilePath +from .typing import HeadersValue +from .typing import ResponseReturnValue +from .typing import ResponseTypes +from .typing import ShellContextProcessorCallable +from .typing import StatusCode +from .typing import TeardownCallable +from .typing import TemplateFilterCallable +from .typing import TemplateGlobalCallable +from .typing import TemplateTestCallable +from .typing import TestAppProtocol +from .typing import TestClientProtocol +from .typing import WebsocketCallable +from .typing import WhileServingCallable +from .utils import cancel_tasks +from .utils import file_path_to_path +from .utils import MustReloadError +from .utils import observe_changes +from .utils import restart +from .utils import run_sync +from .wrappers import BaseRequestWebsocket +from .wrappers import Request +from .wrappers import Response +from .wrappers import Websocket try: from typing import ParamSpec @@ -321,17 +325,17 @@ def __init__( ) self.after_serving_funcs: list[Callable[[], Awaitable[None]]] = [] - self.after_websocket_funcs: dict[AppOrBlueprintKey, list[AfterWebsocketCallable]] = ( - defaultdict(list) - ) - self.background_tasks: Set[asyncio.Task] = set() + self.after_websocket_funcs: dict[ + AppOrBlueprintKey, list[AfterWebsocketCallable] + ] = defaultdict(list) + self.background_tasks: set[asyncio.Task] = set() self.before_serving_funcs: list[Callable[[], Awaitable[None]]] = [] - self.before_websocket_funcs: dict[AppOrBlueprintKey, list[BeforeWebsocketCallable]] = ( - defaultdict(list) - ) - self.teardown_websocket_funcs: dict[AppOrBlueprintKey, list[TeardownCallable]] = ( - defaultdict(list) - ) + self.before_websocket_funcs: dict[ + AppOrBlueprintKey, list[BeforeWebsocketCallable] + ] = defaultdict(list) + self.teardown_websocket_funcs: dict[ + AppOrBlueprintKey, list[TeardownCallable] + ] = defaultdict(list) self.while_serving_gens: list[AsyncGenerator[None, None]] = [] self.template_context_processors[None] = [_default_template_ctx_processor] @@ -544,7 +548,9 @@ async def func(): self.after_serving_funcs.append(func) return func - def create_url_adapter(self, request: BaseRequestWebsocket | None) -> MapAdapter | None: + def create_url_adapter( + self, request: BaseRequestWebsocket | None + ) -> MapAdapter | None: """Create and return a URL adapter. This will create the adapter based on the request if present @@ -552,7 +558,9 @@ def create_url_adapter(self, request: BaseRequestWebsocket | None) -> MapAdapter """ if request is not None: subdomain = ( - (self.url_map.default_subdomain or None) if not self.subdomain_matching else None + (self.url_map.default_subdomain or None) + if not self.subdomain_matching + else None ) return self.url_map.bind_to_request( # type: ignore[attr-defined] @@ -725,7 +733,8 @@ def url_for( if url_adapter is None: raise RuntimeError( - "Unable to create a url adapter, try setting the SERVER_NAME config variable." + "Unable to create a url adapter, try setting the SERVER_NAME" + " config variable." ) if _scheme is not None and not _external: raise ValueError("External must be True for scheme usage") @@ -738,7 +747,9 @@ def url_for( url_adapter.url_scheme = _scheme try: - url = url_adapter.build(endpoint, values, method=_method, force_external=_external) + url = url_adapter.build( + endpoint, values, method=_method, force_external=_external + ) except BuildError as error: return self.handle_url_build_error(error, endpoint, values) finally: @@ -819,7 +830,9 @@ def _signal_handler(*_: Any) -> None: for signal_name in {"SIGINT", "SIGTERM", "SIGBREAK"}: if hasattr(signal, signal_name): try: - loop.add_signal_handler(getattr(signal, signal_name), _signal_handler) + loop.add_signal_handler( + getattr(signal, signal_name), _signal_handler + ) except NotImplementedError: # Add signal handler may not be implemented on Windows signal.signal(getattr(signal, signal_name), _signal_handler) @@ -854,7 +867,9 @@ def _signal_handler(*_: Any) -> None: tasks = [loop.create_task(task)] if use_reloader: - tasks.append(loop.create_task(observe_changes(asyncio.sleep, shutdown_event))) + tasks.append( + loop.create_task(observe_changes(asyncio.sleep, shutdown_event)) + ) reload_ = False try: @@ -910,7 +925,9 @@ def run_task( return serve(self, config, shutdown_trigger=shutdown_trigger) - def test_client(self, use_cookies: bool = True, **kwargs: Any) -> TestClientProtocol: + def test_client( + self, use_cookies: bool = True, **kwargs: Any + ) -> TestClientProtocol: """Creates and returns a test client.""" return self.test_client_class(self, use_cookies=use_cookies, **kwargs) @@ -1012,7 +1029,9 @@ async def handle_http_exception( else: return await self.ensure_async(handler)(error) # type: ignore - async def handle_user_exception(self, error: Exception) -> HTTPException | ResponseReturnValue: + async def handle_user_exception( + self, error: Exception + ) -> HTTPException | ResponseReturnValue: """Handle an exception that has been raised. This should forward :class:`~quart.exception.HTTPException` to @@ -1046,7 +1065,9 @@ async def handle_exception(self, error: Exception) -> ResponseTypes: """ exc_info = sys.exc_info() await got_request_exception.send_async( - self, _sync_wrapper=self.ensure_async, exception=error # type: ignore + self, + _sync_wrapper=self.ensure_async, + exception=error, # type: ignore ) propagate = self.config["PROPAGATE_EXCEPTIONS"] @@ -1071,14 +1092,18 @@ async def handle_exception(self, error: Exception) -> ResponseTypes: return await self.finalize_request(server_error, from_error_handler=True) - async def handle_websocket_exception(self, error: Exception) -> ResponseTypes | None: + async def handle_websocket_exception( + self, error: Exception + ) -> ResponseTypes | None: """Handle an uncaught exception. By default this logs the exception and then re-raises it. """ exc_info = sys.exc_info() await got_websocket_exception.send_async( - self, _sync_wrapper=self.ensure_async, exception=error # type: ignore + self, + _sync_wrapper=self.ensure_async, + exception=error, # type: ignore ) propagate = self.config["PROPAGATE_EXCEPTIONS"] @@ -1105,7 +1130,8 @@ async def handle_websocket_exception(self, error: Exception) -> ResponseTypes | def log_exception( self, - exception_info: tuple[type, BaseException, TracebackType] | tuple[None, None, None], + exception_info: tuple[type, BaseException, TracebackType] + | tuple[None, None, None], ) -> None: """Log a exception to the :attr:`logger`. @@ -1114,22 +1140,27 @@ def log_exception( if has_request_context(): request_ = request_ctx.request self.logger.error( - f"Exception on request {request_.method} {request_.path}", exc_info=exception_info + f"Exception on request {request_.method} {request_.path}", + exc_info=exception_info, ) elif has_websocket_context(): websocket_ = websocket_ctx.websocket - self.logger.error(f"Exception on websocket {websocket_.path}", exc_info=exception_info) + self.logger.error( + f"Exception on websocket {websocket_.path}", exc_info=exception_info + ) else: self.logger.error("Exception", exc_info=exception_info) @overload - def ensure_async(self, func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]: ... + def ensure_async( + self, func: Callable[P, Awaitable[T]] + ) -> Callable[P, Awaitable[T]]: ... @overload def ensure_async(self, func: Callable[P, T]) -> Callable[P, Awaitable[T]]: ... def ensure_async( - self, func: Union[Callable[P, Awaitable[T]], Callable[P, T]] + self, func: Callable[P, Awaitable[T]] | Callable[P, T] ) -> Callable[P, Awaitable[T]]: """Ensure that the returned func is async and calls the func. @@ -1173,11 +1204,15 @@ async def do_teardown_request( await self.ensure_async(function)(exc) await request_tearing_down.send_async( - self, _sync_wrapper=self.ensure_async, exc=exc # type: ignore + self, + _sync_wrapper=self.ensure_async, + exc=exc, # type: ignore ) async def do_teardown_websocket( - self, exc: BaseException | None, websocket_context: WebsocketContext | None = None + self, + exc: BaseException | None, + websocket_context: WebsocketContext | None = None, ) -> None: """Teardown the websocket, calling the teardown functions. @@ -1193,7 +1228,9 @@ async def do_teardown_websocket( await self.ensure_async(function)(exc) await websocket_tearing_down.send_async( - self, _sync_wrapper=self.ensure_async, exc=exc # type: ignore + self, + _sync_wrapper=self.ensure_async, + exc=exc, # type: ignore ) async def do_teardown_appcontext(self, exc: BaseException | None) -> None: @@ -1201,7 +1238,9 @@ async def do_teardown_appcontext(self, exc: BaseException | None) -> None: for function in self.teardown_appcontext_funcs: await self.ensure_async(function)(exc) await appcontext_tearing_down.send_async( - self, _sync_wrapper=self.ensure_async, exc=exc # type: ignore + self, + _sync_wrapper=self.ensure_async, + exc=exc, # type: ignore ) def app_context(self) -> AppContext: @@ -1296,7 +1335,9 @@ def test_request_context( auth, subdomain, ) - request_body, body_headers = make_test_body_with_headers(data=data, form=form, json=json) + request_body, body_headers = make_test_body_with_headers( + data=data, form=form, json=json + ) headers.update(**body_headers) scope = make_test_scope( "http", @@ -1337,7 +1378,9 @@ async def _wrapper() -> None: async def handle_background_exception(self, error: Exception) -> None: await got_background_exception.send_async( - self, _sync_wrapper=self.ensure_async, exception=error # type: ignore + self, + _sync_wrapper=self.ensure_async, + exception=error, # type: ignore ) self.log_exception(sys.exc_info()) @@ -1347,7 +1390,9 @@ async def make_default_options_response(self) -> Response: methods = request_ctx.url_adapter.allowed_methods() return self.response_class("", headers={"Allow": ", ".join(methods)}) - async def make_response(self, result: ResponseReturnValue | HTTPException) -> ResponseTypes: + async def make_response( + self, result: ResponseReturnValue | HTTPException + ) -> ResponseTypes: """Make a Response from the result of the route handler. The result itself can either be: @@ -1379,7 +1424,9 @@ async def make_response(self, result: ResponseReturnValue | HTTPException) -> Re value = result # type: ignore[assignment] if value is None: - raise TypeError("The response value returned by the view function cannot be None") + raise TypeError( + "The response value returned by the view function cannot be None" + ) response: ResponseTypes if isinstance(value, HTTPException): @@ -1394,7 +1441,9 @@ async def make_response(self, result: ResponseReturnValue | HTTPException) -> Re elif isinstance(value, (list, dict)): response = self.json.response(value) # type: ignore[assignment] else: - raise TypeError(f"The response value type ({type(value).__name__}) is not valid") + raise TypeError( + f"The response value type ({type(value).__name__}) is not valid" + ) else: response = value @@ -1461,7 +1510,8 @@ async def full_dispatch_websocket( """ try: await websocket_started.send_async( - self, _sync_wrapper=self.ensure_async # type: ignore + self, + _sync_wrapper=self.ensure_async, # type: ignore ) result: ResponseReturnValue | HTTPException | None @@ -1576,7 +1626,9 @@ async def finalize_request( try: response = await self.process_response(response, request_context) await request_finished.send_async( - self, _sync_wrapper=self.ensure_async, response=response # type: ignore + self, + _sync_wrapper=self.ensure_async, + response=response, # type: ignore ) except Exception: if not from_error_handler: @@ -1604,7 +1656,9 @@ async def finalize_websocket( try: response = await self.postprocess_websocket(response, websocket_context) await websocket_finished.send_async( - self, _sync_wrapper=self.ensure_async, response=response # type: ignore + self, + _sync_wrapper=self.ensure_async, + response=response, # type: ignore ) except Exception: if not from_error_handler: @@ -1635,7 +1689,9 @@ async def process_response( session_ = (request_context or request_ctx).session if not self.session_interface.is_null_session(session_): - await self.ensure_async(self.session_interface.save_session)(self, session_, response) + await self.ensure_async(self.session_interface.save_session)( + self, session_, response + ) return response async def postprocess_websocket( @@ -1711,7 +1767,9 @@ async def startup(self) -> None: await gen.__anext__() except Exception as error: await got_serving_exception.send_async( - self, _sync_wrapper=self.ensure_async, exception=error # type: ignore + self, + _sync_wrapper=self.ensure_async, + exception=error, # type: ignore ) self.log_exception(sys.exc_info()) raise @@ -1739,7 +1797,9 @@ async def shutdown(self) -> None: raise RuntimeError("While serving generator didn't terminate") except Exception as error: await got_serving_exception.send_async( - self, _sync_wrapper=self.ensure_async, exception=error # type: ignore + self, + _sync_wrapper=self.ensure_async, + exception=error, # type: ignore ) self.log_exception(sys.exc_info()) raise diff --git a/src/quart/asgi.py b/src/quart/asgi.py index 856b55a4..7221d48a 100644 --- a/src/quart/asgi.py +++ b/src/quart/asgi.py @@ -3,34 +3,40 @@ import asyncio import warnings from functools import partial -from typing import AnyStr, cast, Optional, TYPE_CHECKING +from typing import AnyStr +from typing import cast +from typing import Optional +from typing import TYPE_CHECKING from urllib.parse import urlparse -from hypercorn.typing import ( - ASGIReceiveCallable, - ASGISendCallable, - HTTPResponseBodyEvent, - HTTPResponseStartEvent, - HTTPScope, - LifespanScope, - LifespanShutdownCompleteEvent, - LifespanShutdownFailedEvent, - LifespanStartupCompleteEvent, - LifespanStartupFailedEvent, - WebsocketAcceptEvent, - WebsocketCloseEvent, - WebsocketResponseBodyEvent, - WebsocketResponseStartEvent, - WebsocketScope, -) +from hypercorn.typing import ASGIReceiveCallable +from hypercorn.typing import ASGISendCallable +from hypercorn.typing import HTTPResponseBodyEvent +from hypercorn.typing import HTTPResponseStartEvent +from hypercorn.typing import HTTPScope +from hypercorn.typing import LifespanScope +from hypercorn.typing import LifespanShutdownCompleteEvent +from hypercorn.typing import LifespanShutdownFailedEvent +from hypercorn.typing import LifespanStartupCompleteEvent +from hypercorn.typing import LifespanStartupFailedEvent +from hypercorn.typing import WebsocketAcceptEvent +from hypercorn.typing import WebsocketCloseEvent +from hypercorn.typing import WebsocketResponseBodyEvent +from hypercorn.typing import WebsocketResponseStartEvent +from hypercorn.typing import WebsocketScope from werkzeug.datastructures import Headers from werkzeug.wrappers import Response as WerkzeugResponse from .debug import traceback_response -from .signals import websocket_received, websocket_sent +from .signals import websocket_received +from .signals import websocket_sent from .typing import ResponseTypes -from .utils import cancel_tasks, encode_headers, raise_task_exceptions -from .wrappers import Request, Response, Websocket # noqa: F401 +from .utils import cancel_tasks +from .utils import encode_headers +from .utils import raise_task_exceptions +from .wrappers import Request # noqa: F401 +from .wrappers import Response # noqa: F401 +from .wrappers import Websocket # noqa: F401 if TYPE_CHECKING: from .app import Quart # noqa: F401 @@ -41,7 +47,9 @@ def __init__(self, app: Quart, scope: HTTPScope) -> None: self.app = app self.scope = scope - async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: + async def __call__( + self, receive: ASGIReceiveCallable, send: ASGISendCallable + ) -> None: request = self._create_request_from_scope(send) receiver_task = asyncio.ensure_future(self.handle_messages(request, receive)) handler_task = asyncio.ensure_future(self.handle_request(request, send)) @@ -51,7 +59,9 @@ async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) - await cancel_tasks(pending) raise_task_exceptions(done) - async def handle_messages(self, request: Request, receive: ASGIReceiveCallable) -> None: + async def handle_messages( + self, request: Request, receive: ASGIReceiveCallable + ) -> None: while True: message = await receive() if message["type"] == "http.request": @@ -108,7 +118,9 @@ async def handle_request(self, request: Request, send: ASGISendCallable) -> None except asyncio.TimeoutError: pass - async def _send_response(self, send: ASGISendCallable, response: ResponseTypes) -> None: + async def _send_response( + self, send: ASGISendCallable, response: ResponseTypes + ) -> None: await send( cast( HTTPResponseStartEvent, @@ -136,7 +148,11 @@ async def _send_response(self, send: ASGISendCallable, response: ResponseTypes) await send( cast( HTTPResponseBodyEvent, - {"type": "http.response.body", "body": body, "more_body": True}, + { + "type": "http.response.body", + "body": body, + "more_body": True, + }, ) ) await send( @@ -146,11 +162,17 @@ async def _send_response(self, send: ASGISendCallable, response: ResponseTypes) ) ) - async def _send_push_promise(self, send: ASGISendCallable, path: str, headers: Headers) -> None: + async def _send_push_promise( + self, send: ASGISendCallable, path: str, headers: Headers + ) -> None: extensions = self.scope.get("extensions", {}) or {} if "http.response.push" in extensions: await send( - {"type": "http.response.push", "path": path, "headers": encode_headers(headers)} + { + "type": "http.response.push", + "path": path, + "headers": encode_headers(headers), + } ) @@ -162,7 +184,9 @@ def __init__(self, app: Quart, scope: WebsocketScope) -> None: self._accepted = False self._closed = False - async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: + async def __call__( + self, receive: ASGIReceiveCallable, send: ASGISendCallable + ) -> None: websocket = self._create_websocket_from_scope(send) receiver_task = asyncio.ensure_future(self.handle_messages(receive)) handler_task = asyncio.ensure_future(self.handle_websocket(websocket, send)) @@ -213,7 +237,9 @@ def _create_websocket_from_scope(self, send: ASGISendCallable) -> Websocket: scope=self.scope, ) - async def handle_websocket(self, websocket: Websocket, send: ASGISendCallable) -> None: + async def handle_websocket( + self, websocket: Websocket, send: ASGISendCallable + ) -> None: try: response = await self.app.handle_websocket(websocket) except Exception as error: @@ -264,13 +290,21 @@ async def handle_websocket(self, websocket: Websocket, send: ASGISendCallable) - await send( cast( WebsocketResponseBodyEvent, - {"type": "websocket.http.response.body", "body": b"", "more_body": False}, + { + "type": "websocket.http.response.body", + "body": b"", + "more_body": False, + }, ) ) elif not self._closed: - await send(cast(WebsocketCloseEvent, {"type": "websocket.close", "code": 1000})) + await send( + cast(WebsocketCloseEvent, {"type": "websocket.close", "code": 1000}) + ) elif self._accepted and not self._closed: - await send(cast(WebsocketCloseEvent, {"type": "websocket.close", "code": 1000})) + await send( + cast(WebsocketCloseEvent, {"type": "websocket.close", "code": 1000}) + ) async def send_data(self, send: ASGISendCallable, data: AnyStr) -> None: if isinstance(data, str): @@ -288,19 +322,28 @@ async def accept_connection( "subprotocol": subprotocol, "type": "websocket.accept", } - spec_version = _convert_version(self.scope.get("asgi", {}).get("spec_version", "2.0")) + spec_version = _convert_version( + self.scope.get("asgi", {}).get("spec_version", "2.0") + ) if spec_version > [2, 0]: message["headers"] = encode_headers(headers) elif headers: - warnings.warn("The ASGI Server does not support accept headers, headers not sent") + warnings.warn( + "The ASGI Server does not support accept headers, headers not sent", + stacklevel=1, + ) self._accepted = True await send(message) - async def close_connection(self, send: ASGISendCallable, code: int, reason: str) -> None: + async def close_connection( + self, send: ASGISendCallable, code: int, reason: str + ) -> None: if self._closed: raise RuntimeError("Cannot close websocket multiple times") - spec_version = _convert_version(self.scope.get("asgi", {}).get("spec_version", "2.0")) + spec_version = _convert_version( + self.scope.get("asgi", {}).get("spec_version", "2.0") + ) if spec_version >= [2, 3]: await send({"type": "websocket.close", "code": code, "reason": reason}) else: @@ -312,7 +355,9 @@ class ASGILifespan: def __init__(self, app: Quart, scope: LifespanScope) -> None: self.app = app - async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: + async def __call__( + self, receive: ASGIReceiveCallable, send: ASGISendCallable + ) -> None: while True: event = await receive() if event["type"] == "lifespan.startup": @@ -327,7 +372,10 @@ async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) - ) else: await send( - cast(LifespanStartupCompleteEvent, {"type": "lifespan.startup.complete"}) + cast( + LifespanStartupCompleteEvent, + {"type": "lifespan.startup.complete"}, + ) ) elif event["type"] == "lifespan.shutdown": try: @@ -341,7 +389,10 @@ async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) - ) else: await send( - cast(LifespanShutdownCompleteEvent, {"type": "lifespan.shutdown.complete"}), + cast( + LifespanShutdownCompleteEvent, + {"type": "lifespan.shutdown.complete"}, + ), ) break diff --git a/src/quart/blueprints.py b/src/quart/blueprints.py index c40d3f57..56d88bad 100644 --- a/src/quart/blueprints.py +++ b/src/quart/blueprints.py @@ -8,26 +8,22 @@ from aiofiles import open as async_open from aiofiles.base import AiofilesContextManager from flask.sansio.app import App -from flask.sansio.blueprints import ( # noqa - Blueprint as SansioBlueprint, - BlueprintSetupState as BlueprintSetupState, -) +from flask.sansio.blueprints import Blueprint as SansioBlueprint # noqa +from flask.sansio.blueprints import BlueprintSetupState as BlueprintSetupState # noqa from flask.sansio.scaffold import setupmethod from .cli import AppGroup from .globals import current_app from .helpers import send_from_directory -from .typing import ( - AfterServingCallable, - AfterWebsocketCallable, - AppOrBlueprintKey, - BeforeServingCallable, - BeforeWebsocketCallable, - FilePath, - TeardownCallable, - WebsocketCallable, - WhileServingCallable, -) +from .typing import AfterServingCallable +from .typing import AfterWebsocketCallable +from .typing import AppOrBlueprintKey +from .typing import BeforeServingCallable +from .typing import BeforeWebsocketCallable +from .typing import FilePath +from .typing import TeardownCallable +from .typing import WebsocketCallable +from .typing import WhileServingCallable if t.TYPE_CHECKING: from .wrappers import Response @@ -57,15 +53,15 @@ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: self.cli = AppGroup() self.cli.name = self.name - self.after_websocket_funcs: t.Dict[AppOrBlueprintKey, t.List[AfterWebsocketCallable]] = ( - defaultdict(list) - ) - self.before_websocket_funcs: t.Dict[AppOrBlueprintKey, t.List[BeforeWebsocketCallable]] = ( - defaultdict(list) - ) - self.teardown_websocket_funcs: dict[AppOrBlueprintKey, list[TeardownCallable]] = ( - defaultdict(list) - ) + self.after_websocket_funcs: dict[ + AppOrBlueprintKey, list[AfterWebsocketCallable] + ] = defaultdict(list) + self.before_websocket_funcs: dict[ + AppOrBlueprintKey, list[BeforeWebsocketCallable] + ] = defaultdict(list) + self.teardown_websocket_funcs: dict[ + AppOrBlueprintKey, list[TeardownCallable] + ] = defaultdict(list) def get_send_file_max_age(self, filename: str | None) -> int | None: """Used by :func:`send_file` to determine the ``max_age`` cache diff --git a/src/quart/cli.py b/src/quart/cli.py index f99e2341..7d66fc3b 100644 --- a/src/quart/cli.py +++ b/src/quart/cli.py @@ -13,13 +13,16 @@ from importlib import import_module from operator import attrgetter from types import ModuleType -from typing import Any, Callable, TYPE_CHECKING +from typing import Any +from typing import Callable +from typing import TYPE_CHECKING import click from click.core import ParameterSource from .globals import current_app -from .helpers import get_debug_flag, get_load_dotenv +from .helpers import get_debug_flag +from .helpers import get_load_dotenv try: from importlib.metadata import version @@ -123,7 +126,9 @@ def find_app_by_string(module: ModuleType, app_name: str) -> Quart: elif isinstance(expr, ast.Call): # Ensure the function name is an attribute name only. if not isinstance(expr.func, ast.Name): - raise NoAppException(f"Function reference must be a simple name: {app_name!r}.") + raise NoAppException( + f"Function reference must be a simple name: {app_name!r}." + ) name = expr.func.id @@ -138,12 +143,16 @@ def find_app_by_string(module: ModuleType, app_name: str) -> Quart: f"Failed to parse arguments as literal values: {app_name!r}." ) from None else: - raise NoAppException(f"Failed to parse {app_name!r} as an attribute name or function call.") + raise NoAppException( + f"Failed to parse {app_name!r} as an attribute name or function call." + ) try: attr = getattr(module, name) except AttributeError as e: - raise NoAppException(f"Failed to find attribute {name!r} in {module.__name__!r}.") from e + raise NoAppException( + f"Failed to find attribute {name!r} in {module.__name__!r}." + ) from e # If the attribute is a function, call it with any args and kwargs # to get the real application. @@ -166,7 +175,8 @@ def find_app_by_string(module: ModuleType, app_name: str) -> Quart: return app raise NoAppException( - f"A valid Quart application was not obtained from '{module.__name__}:{app_name}'." + "A valid Quart application was not obtained from" + f" '{module.__name__}:{app_name}'." ) @@ -240,7 +250,9 @@ def load_app(self) -> Quart: app = self.create_app() else: if self.app_import_path: - path, name = (re.split(r":(?![\\/])", self.app_import_path, 1) + [None])[:2] + path, name = ( + re.split(r":(?![\\/])", self.app_import_path, maxsplit=1) + [None] + )[:2] import_name = prepare_import(path) app = locate_app(import_name, name) else: @@ -279,10 +291,13 @@ async def _inner() -> Any: try: return __ctx.invoke(fn, *args, **kwargs) except RuntimeError as error: - if error.args[0] == "Cannot run the event loop while another loop is running": + if ( + error.args[0] + == "Cannot run the event loop while another loop is running" + ): click.echo( - "The appcontext cannot be used with a command that runs an event loop. " - "See quart#361 for more details" + "The appcontext cannot be used with a command that" + " runs an event loop. See quart#361 for more details" ) raise @@ -397,7 +412,9 @@ def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | N ) -def _env_file_callback(ctx: click.Context, param: click.Option, value: str | None) -> str | None: +def _env_file_callback( + ctx: click.Context, param: click.Option, value: str | None +) -> str | None: if value is None: return None @@ -637,7 +654,12 @@ def run_command( reload = debug app.run( - debug=debug, host=host, port=port, certfile=certfile, keyfile=keyfile, use_reloader=reload + debug=debug, + host=host, + port=port, + certfile=certfile, + keyfile=keyfile, + use_reloader=reload, ) diff --git a/src/quart/config.py b/src/quart/config.py index 28190f33..cb0d03f0 100644 --- a/src/quart/config.py +++ b/src/quart/config.py @@ -1,9 +1,11 @@ from __future__ import annotations import json -from typing import Any, Callable +from typing import Any +from typing import Callable -from flask.config import Config as FlaskConfig, ConfigAttribute as ConfigAttribute # noqa: F401 +from flask.config import Config as FlaskConfig # noqa: F401 +from flask.config import ConfigAttribute as ConfigAttribute # noqa: F401 class Config(FlaskConfig): diff --git a/src/quart/ctx.py b/src/quart/ctx.py index 1915be1b..65085cc4 100644 --- a/src/quart/ctx.py +++ b/src/quart/ctx.py @@ -4,16 +4,25 @@ from contextvars import Token from functools import wraps from types import TracebackType -from typing import Any, Callable, cast, Iterator, List, Optional, Tuple, TYPE_CHECKING # noqa: F401 +from typing import Any +from typing import Callable +from typing import cast +from typing import TYPE_CHECKING from flask.ctx import _AppCtxGlobals as _AppCtxGlobals # noqa: F401 from werkzeug.exceptions import HTTPException -from .globals import _cv_app, _cv_request, _cv_websocket +from .globals import _cv_app +from .globals import _cv_request +from .globals import _cv_websocket from .sessions import SessionMixin # noqa -from .signals import appcontext_popped, appcontext_pushed -from .typing import AfterRequestCallable, AfterWebsocketCallable -from .wrappers import BaseRequestWebsocket, Request, Websocket +from .signals import appcontext_popped +from .signals import appcontext_pushed +from .typing import AfterRequestCallable +from .typing import AfterWebsocketCallable +from .wrappers import BaseRequestWebsocket +from .wrappers import Request +from .wrappers import Websocket if TYPE_CHECKING: from .app import Quart # noqa @@ -22,7 +31,8 @@ class _BaseRequestWebsocketContext: - """A base context relating to either request or websockets, bound to the current task. + """A base context relating to either request or websockets, bound to the + current task. Attributes: app: The app itself. @@ -84,7 +94,9 @@ async def __aenter__(self) -> _BaseRequestWebsocketContext: await self.push() return self - async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: + async def __aexit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: await self.auto_pop(exc_value) async def _push_appctx(self, token: Token) -> None: @@ -160,7 +172,9 @@ async def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ign await app_ctx.pop(exc) if ctx is not self: - raise AssertionError(f"Popped wrong request context. ({ctx!r} instead of {self!r})") + raise AssertionError( + f"Popped wrong request context. ({ctx!r} instead of {self!r})" + ) async def __aenter__(self) -> RequestContext: await self.push() @@ -211,7 +225,9 @@ async def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ign await app_ctx.pop(exc) if ctx is not self: - raise AssertionError(f"Popped wrong request context. ({ctx!r} instead of {self!r})") + raise AssertionError( + f"Popped wrong request context. ({ctx!r} instead of {self!r})" + ) async def __aenter__(self) -> WebsocketContext: await self.push() @@ -245,7 +261,8 @@ def copy(self) -> AppContext: async def push(self) -> None: self._cv_tokens.append(_cv_app.set(self)) await appcontext_pushed.send_async( - self.app, _sync_wrapper=self.app.ensure_async # type: ignore + self.app, + _sync_wrapper=self.app.ensure_async, # type: ignore ) async def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore @@ -259,17 +276,22 @@ async def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ign _cv_app.reset(self._cv_tokens.pop()) if ctx is not self: - raise AssertionError(f"Popped wrong app context. ({ctx!r} instead of {self!r})") + raise AssertionError( + f"Popped wrong app context. ({ctx!r} instead of {self!r})" + ) await appcontext_popped.send_async( - self.app, _sync_wrapper=self.app.ensure_async # type: ignore + self.app, + _sync_wrapper=self.app.ensure_async, # type: ignore ) async def __aenter__(self) -> AppContext: await self.push() return self - async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: + async def __aexit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: await self.pop(exc_value) @@ -369,7 +391,9 @@ async def within_context() -> None: """ if not has_request_context(): - raise RuntimeError("Attempt to copy request context outside of a request context") + raise RuntimeError( + "Attempt to copy request context outside of a request context" + ) request_context = _cv_request.get().copy() @@ -397,7 +421,9 @@ async def within_context() -> None: """ if not has_websocket_context(): - raise RuntimeError("Attempt to copy websocket context outside of a websocket context") + raise RuntimeError( + "Attempt to copy websocket context outside of a websocket context" + ) websocket_context = _cv_websocket.get().copy() diff --git a/src/quart/datastructures.py b/src/quart/datastructures.py index 17fe27be..a0c1e7d8 100644 --- a/src/quart/datastructures.py +++ b/src/quart/datastructures.py @@ -5,7 +5,8 @@ from typing import IO from aiofiles import open as async_open -from werkzeug.datastructures import FileStorage as WerkzeugFileStorage, Headers +from werkzeug.datastructures import FileStorage as WerkzeugFileStorage +from werkzeug.datastructures import Headers class FileStorage(WerkzeugFileStorage): diff --git a/src/quart/debug.py b/src/quart/debug.py index 9b0cb3ee..0353fe73 100644 --- a/src/quart/debug.py +++ b/src/quart/debug.py @@ -66,7 +66,8 @@
{% for line in frame.code[0] %} -
+
{{ loop.index + frame.code[1] }}
{{ line }}
diff --git a/src/quart/formparser.py b/src/quart/formparser.py index 8b0c0abc..e8ea6e16 100644 --- a/src/quart/formparser.py +++ b/src/quart/formparser.py @@ -1,24 +1,26 @@ from __future__ import annotations -from typing import ( - Any, - Awaitable, - Callable, - cast, - Dict, - IO, - NoReturn, - Optional, - Tuple, - TYPE_CHECKING, -) +from collections.abc import Awaitable +from typing import Any +from typing import Callable +from typing import cast +from typing import IO +from typing import NoReturn +from typing import Optional +from typing import TYPE_CHECKING from urllib.parse import parse_qsl -from werkzeug.datastructures import Headers, MultiDict +from werkzeug.datastructures import Headers +from werkzeug.datastructures import MultiDict from werkzeug.exceptions import RequestEntityTooLarge from werkzeug.formparser import default_stream_factory from werkzeug.http import parse_options_header -from werkzeug.sansio.multipart import Data, Epilogue, Field, File, MultipartDecoder, NeedData +from werkzeug.sansio.multipart import Data +from werkzeug.sansio.multipart import Epilogue +from werkzeug.sansio.multipart import Field +from werkzeug.sansio.multipart import File +from werkzeug.sansio.multipart import MultipartDecoder +from werkzeug.sansio.multipart import NeedData from .datastructures import FileStorage @@ -31,8 +33,8 @@ ] ParserFunc = Callable[ - ["FormDataParser", "Body", str, Optional[int], Dict[str, str]], - Awaitable[Tuple[MultiDict, MultiDict]], + ["FormDataParser", "Body", str, Optional[int], dict[str, str]], + Awaitable[tuple[MultiDict, MultiDict]], ] @@ -51,7 +53,9 @@ def __init__( self.cls = cls self.silent = silent - def get_parse_func(self, mimetype: str, options: dict[str, str]) -> ParserFunc | None: + def get_parse_func( + self, mimetype: str, options: dict[str, str] + ) -> ParserFunc | None: return self.parse_functions.get(mimetype) async def parse( diff --git a/src/quart/globals.py b/src/quart/globals.py index af2c7906..55456b03 100644 --- a/src/quart/globals.py +++ b/src/quart/globals.py @@ -7,9 +7,13 @@ if TYPE_CHECKING: from .app import Quart - from .ctx import _AppCtxGlobals, AppContext, RequestContext, WebsocketContext + from .ctx import _AppCtxGlobals + from .ctx import AppContext + from .ctx import RequestContext + from .ctx import WebsocketContext from .sessions import SessionMixin - from .wrappers import Request, Websocket + from .wrappers import Request + from .wrappers import Websocket _no_app_msg = "Not within an app context" _cv_app: ContextVar[AppContext] = ContextVar("quart.app_ctx") @@ -49,7 +53,7 @@ def _session_lookup() -> RequestContext | WebsocketContext: try: return _cv_websocket.get() except LookupError: - raise RuntimeError("Not within a request nor websocket context") + raise RuntimeError("Not within a request nor websocket context") from None session: SessionMixin = LocalProxy(_session_lookup, "session") # type: ignore[assignment] diff --git a/src/quart/helpers.py b/src/quart/helpers.py index f3df7002..43032098 100644 --- a/src/quart/helpers.py +++ b/src/quart/helpers.py @@ -4,21 +4,36 @@ import os import pkgutil import sys -from datetime import datetime, timedelta, timezone -from functools import lru_cache, wraps +from collections.abc import Iterable +from datetime import datetime +from datetime import timedelta +from datetime import timezone +from functools import cache +from functools import wraps from io import BytesIO from pathlib import Path -from typing import Any, Callable, cast, Iterable, NoReturn +from typing import Any +from typing import Callable +from typing import cast +from typing import NoReturn from zlib import adler32 from flask.helpers import get_root_path as get_root_path # noqa: F401 -from werkzeug.exceptions import abort as werkzeug_abort, NotFound -from werkzeug.utils import redirect as werkzeug_redirect, safe_join +from werkzeug.exceptions import abort as werkzeug_abort +from werkzeug.exceptions import NotFound +from werkzeug.utils import redirect as werkzeug_redirect +from werkzeug.utils import safe_join from werkzeug.wrappers import Response as WerkzeugResponse -from .globals import _cv_request, current_app, request, request_ctx, session +from .globals import _cv_request +from .globals import current_app +from .globals import request +from .globals import request_ctx +from .globals import session from .signals import message_flashed -from .typing import FilePath, ResponseReturnValue, ResponseTypes +from .typing import FilePath +from .typing import ResponseReturnValue +from .typing import ResponseTypes from .utils import file_path_to_path from .wrappers import Response from .wrappers.response import ResponseBody @@ -325,22 +340,26 @@ async def send_file( last_modified = file_path.stat().st_mtime # type: ignore if cache_timeout is None: cache_timeout = current_app.get_send_file_max_age(str(file_path)) - etag = "{}-{}-{}".format( - file_path.stat().st_mtime, file_path.stat().st_size, adler32(bytes(file_path)) + etag = ( + f"{file_path.stat().st_mtime}-{file_path.stat().st_size}" + f"-{adler32(bytes(file_path))}" ) if mimetype is None and attachment_filename is not None: mimetype = mimetypes.guess_type(attachment_filename)[0] or DEFAULT_MIMETYPE if mimetype is None: raise ValueError( - "The mime type cannot be inferred, please set it manually via the mimetype argument." + "The mime type cannot be inferred, please set it manually via the" + " mimetype argument." ) response = current_app.response_class(file_body, mimetype=mimetype) response.content_length = file_size if as_attachment: - response.headers.add("Content-Disposition", "attachment", filename=attachment_filename) + response.headers.add( + "Content-Disposition", "attachment", filename=attachment_filename + ) if last_modified is not None: response.last_modified = last_modified @@ -354,11 +373,13 @@ async def send_file( response.set_etag(etag) if conditional: - await response.make_conditional(request, accept_ranges=True, complete_length=file_size) + await response.make_conditional( + request, accept_ranges=True, complete_length=file_size + ) return response -@lru_cache(maxsize=None) +@cache def _split_blueprint_path(name: str) -> list[str]: bps = [name] while "." in bps[-1]: diff --git a/src/quart/json/__init__.py b/src/quart/json/__init__.py index 9753dd91..354a8cae 100644 --- a/src/quart/json/__init__.py +++ b/src/quart/json/__init__.py @@ -1,7 +1,9 @@ from __future__ import annotations import json -from typing import Any, IO, TYPE_CHECKING +from typing import Any +from typing import IO +from typing import TYPE_CHECKING from flask.json.provider import _default diff --git a/src/quart/json/provider.py b/src/quart/json/provider.py index 17fbea4c..379c6891 100644 --- a/src/quart/json/provider.py +++ b/src/quart/json/provider.py @@ -1,4 +1,2 @@ -from flask.json.provider import ( # noqa: F401 - DefaultJSONProvider as DefaultJSONProvider, - JSONProvider as JSONProvider, -) +from flask.json.provider import DefaultJSONProvider as DefaultJSONProvider # noqa: F401 +from flask.json.provider import JSONProvider as JSONProvider # noqa: F401 diff --git a/src/quart/json/tag.py b/src/quart/json/tag.py index 0ff177b1..d6a21802 100644 --- a/src/quart/json/tag.py +++ b/src/quart/json/tag.py @@ -1,12 +1,10 @@ -from flask.json.tag import ( # noqa: F401 - JSONTag as JSONTag, - PassDict as PassDict, - PassList as PassList, - TagBytes as TagBytes, - TagDateTime as TagDateTime, - TagDict as TagDict, - TaggedJSONSerializer as TaggedJSONSerializer, - TagMarkup as TagMarkup, - TagTuple as TagTuple, - TagUUID as TagUUID, -) +from flask.json.tag import JSONTag as JSONTag # noqa: F401 +from flask.json.tag import PassDict as PassDict # noqa: F401 +from flask.json.tag import PassList as PassList # noqa: F401 +from flask.json.tag import TagBytes as TagBytes # noqa: F401 +from flask.json.tag import TagDateTime as TagDateTime # noqa: F401 +from flask.json.tag import TagDict as TagDict # noqa: F401 +from flask.json.tag import TaggedJSONSerializer as TaggedJSONSerializer # noqa: F401 +from flask.json.tag import TagMarkup as TagMarkup # noqa: F401 +from flask.json.tag import TagTuple as TagTuple # noqa: F401 +from flask.json.tag import TagUUID as TagUUID # noqa: F401 diff --git a/src/quart/logging.py b/src/quart/logging.py index 699a143b..f4cf0ecb 100644 --- a/src/quart/logging.py +++ b/src/quart/logging.py @@ -1,8 +1,16 @@ from __future__ import annotations import sys -from logging import DEBUG, Formatter, getLogger, Handler, Logger, LogRecord, NOTSET, StreamHandler -from logging.handlers import QueueHandler, QueueListener +from logging import DEBUG +from logging import Formatter +from logging import getLogger +from logging import Handler +from logging import Logger +from logging import LogRecord +from logging import NOTSET +from logging import StreamHandler +from logging.handlers import QueueHandler +from logging.handlers import QueueListener from queue import SimpleQueue as Queue from typing import TYPE_CHECKING @@ -10,7 +18,9 @@ from .app import Quart # noqa default_handler = StreamHandler(sys.stderr) -default_handler.setFormatter(Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")) +default_handler.setFormatter( + Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s") +) class LocalQueueHandler(QueueHandler): diff --git a/src/quart/routing.py b/src/quart/routing.py index e1b40478..7c272c40 100644 --- a/src/quart/routing.py +++ b/src/quart/routing.py @@ -1,9 +1,11 @@ from __future__ import annotations import warnings -from typing import Iterable +from collections.abc import Iterable -from werkzeug.routing import Map, MapAdapter, Rule +from werkzeug.routing import Map +from werkzeug.routing import MapAdapter +from werkzeug.routing import Rule from .wrappers.base import BaseRequestWebsocket @@ -38,7 +40,10 @@ def __init__( class QuartMap(Map): def bind_to_request( - self, request: BaseRequestWebsocket, subdomain: str | None, server_name: str | None + self, + request: BaseRequestWebsocket, + subdomain: str | None, + server_name: str | None, ) -> MapAdapter: host: str if server_name is None: @@ -49,7 +54,9 @@ def bind_to_request( host = _normalise_host(request.scheme, host) if subdomain is None and not self.host_matching: - request_host_parts = _normalise_host(request.scheme, request.host.lower()).split(".") + request_host_parts = _normalise_host( + request.scheme, request.host.lower() + ).split(".") config_host_parts = host.split(".") offset = -len(config_host_parts) diff --git a/src/quart/sessions.py b/src/quart/sessions.py index 9df230b8..4bb1fed2 100644 --- a/src/quart/sessions.py +++ b/src/quart/sessions.py @@ -1,19 +1,22 @@ from __future__ import annotations import hashlib -from datetime import datetime, timezone +from datetime import datetime +from datetime import timezone from typing import TYPE_CHECKING +from flask.sessions import NullSession as NullSession # noqa: F401 +from flask.sessions import SecureCookieSession as SecureCookieSession # noqa: F401 from flask.sessions import ( # noqa: F401 - NullSession as NullSession, - SecureCookieSession as SecureCookieSession, session_json_serializer as session_json_serializer, - SessionMixin as SessionMixin, ) -from itsdangerous import BadSignature, URLSafeTimedSerializer +from flask.sessions import SessionMixin as SessionMixin # noqa: F401 +from itsdangerous import BadSignature +from itsdangerous import URLSafeTimedSerializer from werkzeug.wrappers import Response as WerkzeugResponse -from .wrappers import BaseRequestWebsocket, Response +from .wrappers import BaseRequestWebsocket +from .wrappers import Response if TYPE_CHECKING: from .app import Quart # noqa @@ -90,7 +93,9 @@ def should_set_cookie(self, app: Quart, session: SessionMixin) -> bool: save_each = app.config["SESSION_REFRESH_EACH_REQUEST"] return save_each and session.permanent - async def open_session(self, app: Quart, request: BaseRequestWebsocket) -> SessionMixin | None: + async def open_session( + self, app: Quart, request: BaseRequestWebsocket + ) -> SessionMixin | None: """Open an existing session from the request or create one. Returns: @@ -101,7 +106,10 @@ async def open_session(self, app: Quart, request: BaseRequestWebsocket) -> Sessi raise NotImplementedError() async def save_session( - self, app: Quart, session: SessionMixin, response: Response | WerkzeugResponse | None + self, + app: Quart, + session: SessionMixin, + response: Response | WerkzeugResponse | None, ) -> None: """Save the session argument to the response. @@ -137,9 +145,15 @@ def get_signing_serializer(self, app: Quart) -> URLSafeTimedSerializer | None: if not app.secret_key: return None - options = {"key_derivation": self.key_derivation, "digest_method": self.digest_method} + options = { + "key_derivation": self.key_derivation, + "digest_method": self.digest_method, + } return URLSafeTimedSerializer( - app.secret_key, salt=self.salt, serializer=self.serializer, signer_kwargs=options + app.secret_key, + salt=self.salt, + serializer=self.serializer, + signer_kwargs=options, ) async def open_session( diff --git a/src/quart/templating.py b/src/quart/templating.py index c53874b3..6e5cbcc7 100644 --- a/src/quart/templating.py +++ b/src/quart/templating.py @@ -1,14 +1,23 @@ from __future__ import annotations -from typing import Any, AsyncIterator, TYPE_CHECKING - -from flask.templating import DispatchingJinjaLoader as DispatchingJinjaLoader # noqa: F401 -from jinja2 import Environment as BaseEnvironment, Template - -from .ctx import has_app_context, has_request_context -from .globals import app_ctx, current_app, request_ctx +from collections.abc import AsyncIterator +from typing import Any +from typing import TYPE_CHECKING + +from flask.templating import ( + DispatchingJinjaLoader as DispatchingJinjaLoader, # noqa: F401 +) +from jinja2 import Environment as BaseEnvironment +from jinja2 import Template + +from .ctx import has_app_context +from .ctx import has_request_context +from .globals import app_ctx +from .globals import current_app +from .globals import request_ctx from .helpers import stream_with_context -from .signals import before_render_template, template_rendered +from .signals import before_render_template +from .signals import template_rendered if TYPE_CHECKING: from .app import Quart # noqa @@ -34,7 +43,9 @@ def __init__(self, app: Quart, **options: Any) -> None: super().__init__(**options) -async def render_template(template_name_or_list: str | list[str], **context: Any) -> str: +async def render_template( + template_name_or_list: str | list[str], **context: Any +) -> str: """Render the template with the context given. Arguments: @@ -61,11 +72,17 @@ async def render_template_string(source: str, **context: Any) -> str: async def _render(template: Template, context: dict, app: Quart) -> str: await before_render_template.send_async( - app, _sync_wrapper=app.ensure_async, template=template, context=context # type: ignore + app, + _sync_wrapper=app.ensure_async, + template=template, + context=context, # type: ignore ) rendered_template = await template.render_async(context) await template_rendered.send_async( - app, _sync_wrapper=app.ensure_async, template=template, context=context # type: ignore + app, + _sync_wrapper=app.ensure_async, + template=template, + context=context, # type: ignore ) return rendered_template @@ -113,16 +130,24 @@ async def stream_template_string(source: str, **context: Any) -> AsyncIterator[s return await _stream(current_app._get_current_object(), template, context) # type: ignore -async def _stream(app: Quart, template: Template, context: dict[str, Any]) -> AsyncIterator[str]: +async def _stream( + app: Quart, template: Template, context: dict[str, Any] +) -> AsyncIterator[str]: await before_render_template.send_async( - app, _sync_wrapper=app.ensure_async, template=template, context=context # type: ignore + app, + _sync_wrapper=app.ensure_async, + template=template, + context=context, # type: ignore ) async def generate() -> AsyncIterator[str]: async for chunk in template.generate_async(context): yield chunk await template_rendered.send_async( - app, _sync_wrapper=app.ensure_async, template=template, context=context # type: ignore + app, + _sync_wrapper=app.ensure_async, + template=template, + context=context, # type: ignore ) # If a request context is active, keep it while generating. diff --git a/src/quart/testing/__init__.py b/src/quart/testing/__init__.py index 3cc18bbb..9b280bdb 100644 --- a/src/quart/testing/__init__.py +++ b/src/quart/testing/__init__.py @@ -1,20 +1,19 @@ from __future__ import annotations -from typing import Any, TYPE_CHECKING +from typing import Any +from typing import TYPE_CHECKING from click.testing import CliRunner +from ..cli import ScriptInfo from .app import TestApp from .client import QuartClient from .connections import WebsocketResponseError -from .utils import ( - make_test_body_with_headers, - make_test_headers_path_and_query_string, - make_test_scope, - no_op_push, - sentinel, -) -from ..cli import ScriptInfo +from .utils import make_test_body_with_headers +from .utils import make_test_headers_path_and_query_string +from .utils import make_test_scope +from .utils import no_op_push +from .utils import sentinel if TYPE_CHECKING: from ..app import Quart diff --git a/src/quart/testing/app.py b/src/quart/testing/app.py index 2ed04e23..ccbcdc70 100644 --- a/src/quart/testing/app.py +++ b/src/quart/testing/app.py @@ -1,10 +1,13 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable from types import TracebackType -from typing import Awaitable, TYPE_CHECKING +from typing import TYPE_CHECKING -from hypercorn.typing import ASGIReceiveEvent, ASGISendEvent, LifespanScope +from hypercorn.typing import ASGIReceiveEvent +from hypercorn.typing import ASGISendEvent +from hypercorn.typing import LifespanScope from ..typing import TestClientProtocol @@ -37,8 +40,14 @@ def test_client(self) -> TestClientProtocol: return self.app.test_client() async def startup(self) -> None: - scope: LifespanScope = {"type": "lifespan", "asgi": {"spec_version": "2.0"}, "state": {}} - self._task = asyncio.ensure_future(self.app(scope, self._asgi_receive, self._asgi_send)) + scope: LifespanScope = { + "type": "lifespan", + "asgi": {"spec_version": "2.0"}, + "state": {}, + } + self._task = asyncio.ensure_future( + self.app(scope, self._asgi_receive, self._asgi_send) + ) await self._app_queue.put({"type": "lifespan.startup"}) await asyncio.wait_for(self._startup.wait(), timeout=self.startup_timeout) if self._task.done(): @@ -54,7 +63,9 @@ async def __aenter__(self) -> TestApp: await self.startup() return self - async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: + async def __aexit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: await self.shutdown() async def _asgi_receive(self) -> ASGIReceiveEvent: diff --git a/src/quart/testing/client.py b/src/quart/testing/client.py index 066b15c2..f576cbad 100644 --- a/src/quart/testing/client.py +++ b/src/quart/testing/client.py @@ -1,27 +1,32 @@ from __future__ import annotations +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from datetime import datetime, timedelta +from datetime import datetime +from datetime import timedelta from http.cookiejar import CookieJar from types import TracebackType -from typing import Any, AnyStr, AsyncGenerator, TYPE_CHECKING +from typing import Any +from typing import AnyStr +from typing import TYPE_CHECKING from urllib.request import Request as U2Request -from werkzeug.datastructures import Authorization, Headers +from werkzeug.datastructures import Authorization +from werkzeug.datastructures import Headers from werkzeug.http import dump_cookie -from .connections import TestHTTPConnection, TestWebsocketConnection -from .utils import ( - make_test_body_with_headers, - make_test_headers_path_and_query_string, - make_test_scope, - sentinel, -) from ..datastructures import FileStorage from ..globals import _cv_request from ..sessions import SessionMixin -from ..typing import TestHTTPConnectionProtocol, TestWebsocketConnectionProtocol +from ..typing import TestHTTPConnectionProtocol +from ..typing import TestWebsocketConnectionProtocol from ..wrappers import Response +from .connections import TestHTTPConnection +from .connections import TestWebsocketConnection +from .utils import make_test_body_with_headers +from .utils import make_test_headers_path_and_query_string +from .utils import make_test_scope +from .utils import sentinel if TYPE_CHECKING: from ..app import Quart # noqa @@ -103,9 +108,9 @@ async def open( ) if follow_redirects: while response.status_code >= 300 and response.status_code <= 399: - # Most browsers respond to an HTTP 302 with a GET request to the new location, - # despite what the HTTP spec says. HTTP 303 should always be responded to with - # a GET request. + # Most browsers respond to an HTTP 302 with a GET request to the + # new location, despite what the HTTP spec says. HTTP 303 should + # always be responded to with a GET request. if response.status_code == 302 or response.status_code == 303: method = "GET" response = await self._make_request( @@ -165,7 +170,9 @@ def request( scope_base, _preserve_context=self.preserve_context, ) - return self.http_connection_class(self.app, scope, _preserve_context=self.preserve_context) + return self.http_connection_class( + self.app, scope, _preserve_context=self.preserve_context + ) def websocket( self, @@ -308,7 +315,9 @@ def delete_cookie( self, server_name: str, key: str, path: str = "/", domain: str | None = None ) -> None: """Delete a cookie (set to expire immediately).""" - self.set_cookie(server_name, key, expires=0, max_age=0, path=path, domain=domain) + self.set_cookie( + server_name, key, expires=0, max_age=0, path=path, domain=domain + ) @asynccontextmanager async def session_transaction( @@ -327,7 +336,9 @@ async def session_transaction( auth: Authorization | tuple[str, str] | None = None, ) -> AsyncGenerator[SessionMixin, None]: if self.cookie_jar is None: - raise RuntimeError("Session transactions only make sense with cookies enabled.") + raise RuntimeError( + "Session transactions only make sense with cookies enabled." + ) if headers is None: headers = Headers() @@ -377,7 +388,9 @@ async def __aenter__(self) -> QuartClient: self.preserve_context = True return self - async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: + async def __aexit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: self.preserve_context = False while True: diff --git a/src/quart/testing/connections.py b/src/quart/testing/connections.py index d2e9a939..be9adcf7 100644 --- a/src/quart/testing/connections.py +++ b/src/quart/testing/connections.py @@ -1,13 +1,20 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable from types import TracebackType -from typing import Any, AnyStr, Awaitable, TYPE_CHECKING - -from hypercorn.typing import ASGIReceiveEvent, ASGISendEvent, HTTPScope, WebsocketScope +from typing import Any +from typing import AnyStr +from typing import TYPE_CHECKING + +from hypercorn.typing import ASGIReceiveEvent +from hypercorn.typing import ASGISendEvent +from hypercorn.typing import HTTPScope +from hypercorn.typing import WebsocketScope from werkzeug.datastructures import Headers -from ..json import dumps, loads +from ..json import dumps +from ..json import loads from ..utils import decode_headers from ..wrappers import Response @@ -30,7 +37,9 @@ def __init__(self, response: Response) -> None: class TestHTTPConnection: - def __init__(self, app: Quart, scope: HTTPScope, _preserve_context: bool = False) -> None: + def __init__( + self, app: Quart, scope: HTTPScope, _preserve_context: bool = False + ) -> None: self.app = app self.headers: Headers | None = None self.push_promises: list[tuple[str, Headers]] = [] @@ -43,10 +52,14 @@ def __init__(self, app: Quart, scope: HTTPScope, _preserve_context: bool = False self._task: Awaitable[None] = None async def send(self, data: bytes) -> None: - await self._send_queue.put({"type": "http.request", "body": data, "more_body": True}) + await self._send_queue.put( + {"type": "http.request", "body": data, "more_body": True} + ) async def send_complete(self) -> None: - await self._send_queue.put({"type": "http.request", "body": b"", "more_body": False}) + await self._send_queue.put( + {"type": "http.request", "body": b"", "more_body": False} + ) async def receive(self) -> bytes: data = await self._receive_queue.get() @@ -64,7 +77,9 @@ async def __aenter__(self) -> TestHTTPConnection: ) return self - async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: + async def __aexit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: if exc_type is not None: await self.disconnect() await self._task @@ -80,7 +95,9 @@ async def as_response(self) -> Response: data = await self._receive_queue.get() if isinstance(data, bytes): self.response_data.extend(data) - return self.app.response_class(bytes(self.response_data), self.status_code, self.headers) + return self.app.response_class( + bytes(self.response_data), self.status_code, self.headers + ) async def _asgi_receive(self) -> ASGIReceiveEvent: return await self._send_queue.get() @@ -92,7 +109,9 @@ async def _asgi_send(self, message: ASGISendEvent) -> None: elif message["type"] == "http.response.body": await self._receive_queue.put(message["body"]) elif message["type"] == "http.response.push": - self.push_promises.append((message["path"], decode_headers(message["headers"]))) + self.push_promises.append( + (message["path"], decode_headers(message["headers"])) + ) elif message["type"] == "http.disconnect": await self._receive_queue.put(HTTPDisconnectError()) @@ -115,12 +134,16 @@ async def __aenter__(self) -> TestWebsocketConnection: ) return self - async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: + async def __aexit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: await self.disconnect() await self._task while not self._receive_queue.empty(): data = await self._receive_queue.get() - if isinstance(data, Exception) and not isinstance(data, WebsocketDisconnectError): + if isinstance(data, Exception) and not isinstance( + data, WebsocketDisconnectError + ): raise data async def receive(self) -> AnyStr: @@ -172,4 +195,6 @@ async def _asgi_send(self, message: ASGISendEvent) -> None: ) ) elif message["type"] == "websocket.close": - await self._receive_queue.put(WebsocketDisconnectError(message.get("code", 1000))) + await self._receive_queue.put( + WebsocketDisconnectError(message.get("code", 1000)) + ) diff --git a/src/quart/testing/utils.py b/src/quart/testing/utils.py index baf193a5..3f09748a 100644 --- a/src/quart/testing/utils.py +++ b/src/quart/testing/utils.py @@ -1,11 +1,24 @@ from __future__ import annotations -from typing import Any, AnyStr, cast, overload, TYPE_CHECKING -from urllib.parse import unquote, urlencode - -from hypercorn.typing import HTTPScope, Scope, WebsocketScope -from werkzeug.datastructures import Authorization, Headers -from werkzeug.sansio.multipart import Data, Epilogue, Field, File, MultipartEncoder, Preamble +from typing import Any +from typing import AnyStr +from typing import cast +from typing import overload +from typing import TYPE_CHECKING +from urllib.parse import unquote +from urllib.parse import urlencode + +from hypercorn.typing import HTTPScope +from hypercorn.typing import Scope +from hypercorn.typing import WebsocketScope +from werkzeug.datastructures import Authorization +from werkzeug.datastructures import Headers +from werkzeug.sansio.multipart import Data +from werkzeug.sansio.multipart import Epilogue +from werkzeug.sansio.multipart import Field +from werkzeug.sansio.multipart import File +from werkzeug.sansio.multipart import MultipartEncoder +from werkzeug.sansio.multipart import Preamble from werkzeug.urls import iri_to_uri from ..datastructures import FileStorage @@ -89,9 +102,13 @@ def make_test_body_with_headers( """ if [json is not sentinel, form is not None, data is not None].count(True) > 1: - raise ValueError("Quart test args 'json', 'form', and 'data' are mutually exclusive") + raise ValueError( + "Quart test args 'json', 'form', and 'data' are mutually exclusive" + ) if [json is not sentinel, files is not None, data is not None].count(True) > 1: - raise ValueError("Quart test args 'files', 'json', and 'data' are mutually exclusive") + raise ValueError( + "Quart test args 'files', 'json', and 'data' are mutually exclusive" + ) request_data = b"" @@ -112,7 +129,11 @@ def make_test_body_with_headers( request_data += encoder.send_event(Preamble(data=b"")) for key, file_storage in files.items(): request_data += encoder.send_event( - File(name=key, filename=file_storage.filename, headers=file_storage.headers) + File( + name=key, + filename=file_storage.filename, + headers=file_storage.headers, + ) ) chunk = file_storage.read(16384) while chunk != b"": diff --git a/src/quart/typing.py b/src/quart/typing.py index 97b46e72..397f2b37 100644 --- a/src/quart/typing.py +++ b/src/quart/typing.py @@ -1,34 +1,28 @@ from __future__ import annotations import os -from datetime import datetime, timedelta +from collections.abc import AsyncGenerator +from collections.abc import Awaitable +from collections.abc import Iterator +from collections.abc import Mapping +from collections.abc import Sequence +from contextlib import AbstractAsyncContextManager +from datetime import datetime +from datetime import timedelta from http.cookiejar import CookieJar from types import TracebackType -from typing import ( - Any, - AnyStr, - AsyncContextManager, - AsyncGenerator, - Awaitable, - Callable, - Dict, - Iterator, - List, - Mapping, - Optional, - Sequence, - Tuple, - TYPE_CHECKING, - Union, -) - -from hypercorn.typing import ( - ASGIReceiveCallable, - ASGISendCallable, - HTTPScope, - LifespanScope, - WebsocketScope, -) +from typing import Any +from typing import AnyStr +from typing import Callable +from typing import Optional +from typing import TYPE_CHECKING +from typing import Union + +from hypercorn.typing import ASGIReceiveCallable +from hypercorn.typing import ASGISendCallable +from hypercorn.typing import HTTPScope +from hypercorn.typing import LifespanScope +from hypercorn.typing import WebsocketScope from .datastructures import FileStorage @@ -38,7 +32,8 @@ from typing_extensions import Protocol # type: ignore if TYPE_CHECKING: - from werkzeug.datastructures import Authorization, Headers # noqa: F401 + from werkzeug.datastructures import Authorization # noqa: F401 + from werkzeug.datastructures import Headers # noqa: F401 from werkzeug.wrappers import Response as WerkzeugResponse from .app import Quart @@ -54,7 +49,7 @@ bytes, str, Mapping[str, Any], # any jsonify-able dict - List[Any], # any jsonify-able list + list[Any], # any jsonify-able list Iterator[bytes], Iterator[str], ] @@ -62,26 +57,29 @@ # the possible types for an individual HTTP header HeaderName = str -HeaderValue = Union[str, List[str], Tuple[str, ...]] +HeaderValue = Union[str, list[str], tuple[str, ...]] # the possible types for HTTP headers HeadersValue = Union[ - "Headers", Mapping[HeaderName, HeaderValue], Sequence[Tuple[HeaderName, HeaderValue]] + "Headers", + Mapping[HeaderName, HeaderValue], + Sequence[tuple[HeaderName, HeaderValue]], ] # The possible types returned by a route function. ResponseReturnValue = Union[ ResponseValue, - Tuple[ResponseValue, HeadersValue], - Tuple[ResponseValue, StatusCode], - Tuple[ResponseValue, StatusCode, HeadersValue], + tuple[ResponseValue, HeadersValue], + tuple[ResponseValue, StatusCode], + tuple[ResponseValue, StatusCode, HeadersValue], ] ResponseTypes = Union["Response", "WerkzeugResponse"] AppOrBlueprintKey = Optional[str] # The App key is None, whereas blueprints are named AfterRequestCallable = Union[ - Callable[[ResponseTypes], ResponseTypes], Callable[[ResponseTypes], Awaitable[ResponseTypes]] + Callable[[ResponseTypes], ResponseTypes], + Callable[[ResponseTypes], Awaitable[ResponseTypes]], ] AfterServingCallable = Union[Callable[[], None], Callable[[], Awaitable[None]]] AfterWebsocketCallable = Union[ @@ -101,13 +99,13 @@ Callable[[Any], ResponseReturnValue], Callable[[Any], Awaitable[ResponseReturnValue]], ] -ShellContextProcessorCallable = Callable[[], Dict[str, Any]] +ShellContextProcessorCallable = Callable[[], dict[str, Any]] TeardownCallable = Union[ Callable[[Optional[BaseException]], None], Callable[[Optional[BaseException]], Awaitable[None]], ] TemplateContextProcessorCallable = Union[ - Callable[[], Dict[str, Any]], Callable[[], Awaitable[Dict[str, Any]]] + Callable[[], dict[str, Any]], Callable[[], Awaitable[dict[str, Any]]] ] TemplateFilterCallable = Callable[[Any], Any] TemplateGlobalCallable = Callable[[Any], Any] @@ -129,25 +127,33 @@ class ASGIHTTPProtocol(Protocol): def __init__(self, app: Quart, scope: HTTPScope) -> None: ... - async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... + async def __call__( + self, receive: ASGIReceiveCallable, send: ASGISendCallable + ) -> None: ... class ASGILifespanProtocol(Protocol): def __init__(self, app: Quart, scope: LifespanScope) -> None: ... - async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... + async def __call__( + self, receive: ASGIReceiveCallable, send: ASGISendCallable + ) -> None: ... class ASGIWebsocketProtocol(Protocol): def __init__(self, app: Quart, scope: WebsocketScope) -> None: ... - async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... + async def __call__( + self, receive: ASGIReceiveCallable, send: ASGISendCallable + ) -> None: ... class TestHTTPConnectionProtocol(Protocol): push_promises: list[tuple[str, Headers]] - def __init__(self, app: Quart, scope: HTTPScope, _preserve_context: bool = False) -> None: ... + def __init__( + self, app: Quart, scope: HTTPScope, _preserve_context: bool = False + ) -> None: ... async def send(self, data: bytes) -> None: ... @@ -295,7 +301,7 @@ def session_transaction( json: Any = None, root_path: str = "", http_version: str = "1.1", - ) -> AsyncContextManager[SessionMixin]: ... + ) -> AbstractAsyncContextManager[SessionMixin]: ... async def __aenter__(self) -> TestClientProtocol: ... diff --git a/src/quart/utils.py b/src/quart/utils.py index a92fc032..36692958 100644 --- a/src/quart/utils.py +++ b/src/quart/utils.py @@ -5,24 +5,24 @@ import os import platform import sys +from collections.abc import AsyncIterator +from collections.abc import Awaitable +from collections.abc import Coroutine +from collections.abc import Iterable +from collections.abc import Iterator from contextvars import copy_context -from functools import partial, wraps +from functools import partial +from functools import wraps from pathlib import Path -from typing import ( - Any, - AsyncIterator, - Awaitable, - Callable, - Coroutine, - Iterable, - Iterator, - TYPE_CHECKING, - TypeVar, -) +from typing import Any +from typing import Callable +from typing import TYPE_CHECKING +from typing import TypeVar from werkzeug.datastructures import Headers -from .typing import Event, FilePath +from .typing import Event +from .typing import FilePath if TYPE_CHECKING: from .wrappers.response import Response # noqa: F401 @@ -81,8 +81,8 @@ def _inner() -> T: # run_in_exector method try: return next(iterable) - except StopIteration: - raise StopAsyncIteration() + except StopIteration as e: + raise StopAsyncIteration() from e loop = asyncio.get_running_loop() while True: @@ -102,7 +102,9 @@ def decode_headers(headers: Iterable[tuple[bytes, bytes]]) -> Headers: return Headers([(key.decode(), value.decode()) for key, value in headers]) -async def observe_changes(sleep: Callable[[float], Awaitable[Any]], shutdown_event: Event) -> None: +async def observe_changes( + sleep: Callable[[float], Awaitable[Any]], shutdown_event: Event +) -> None: last_updates: dict[Path, float] = {} for module in list(sys.modules.values()): filename = getattr(module, "__file__", None) @@ -124,9 +126,9 @@ async def observe_changes(sleep: Callable[[float], Awaitable[Any]], shutdown_eve try: mtime = path.stat().st_mtime - except FileNotFoundError: + except FileNotFoundError as e: # File deleted - raise MustReloadError() + raise MustReloadError() from e else: if mtime > last_mtime: raise MustReloadError() diff --git a/src/quart/views.py b/src/quart/views.py index 9df77b0c..e6645a7d 100644 --- a/src/quart/views.py +++ b/src/quart/views.py @@ -1,11 +1,18 @@ from __future__ import annotations -from typing import Any, Callable, ClassVar, Collection - -from .globals import current_app, request -from .typing import ResponseReturnValue, RouteCallable - -http_method_funcs = frozenset(["get", "post", "head", "options", "delete", "put", "trace", "patch"]) +from collections.abc import Collection +from typing import Any +from typing import Callable +from typing import ClassVar + +from .globals import current_app +from .globals import request +from .typing import ResponseReturnValue +from .typing import RouteCallable + +http_method_funcs = frozenset( + ["get", "post", "head", "options", "delete", "put", "trace", "patch"] +) class View: diff --git a/src/quart/wrappers/__init__.py b/src/quart/wrappers/__init__.py index bbb32323..f7749934 100644 --- a/src/quart/wrappers/__init__.py +++ b/src/quart/wrappers/__init__.py @@ -1,7 +1,8 @@ from __future__ import annotations from .base import BaseRequestWebsocket -from .request import Body, Request +from .request import Body +from .request import Request from .response import Response from .websocket import Websocket diff --git a/src/quart/wrappers/base.py b/src/quart/wrappers/base.py index 7b7971e2..6f458ce1 100644 --- a/src/quart/wrappers/base.py +++ b/src/quart/wrappers/base.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, TYPE_CHECKING +from typing import Any +from typing import TYPE_CHECKING from hypercorn.typing import WWWScope from werkzeug.datastructures import Headers diff --git a/src/quart/wrappers/request.py b/src/quart/wrappers/request.py index 8b4ed8de..82459422 100644 --- a/src/quart/wrappers/request.py +++ b/src/quart/wrappers/request.py @@ -1,15 +1,25 @@ from __future__ import annotations import asyncio -from typing import Any, Awaitable, Callable, Generator, NoReturn, overload +from collections.abc import Awaitable +from collections.abc import Generator +from typing import Any +from typing import Callable +from typing import NoReturn +from typing import overload from hypercorn.typing import HTTPScope -from werkzeug.datastructures import CombinedMultiDict, Headers, iter_multi_items, MultiDict -from werkzeug.exceptions import BadRequest, RequestEntityTooLarge, RequestTimeout +from werkzeug.datastructures import CombinedMultiDict +from werkzeug.datastructures import Headers +from werkzeug.datastructures import iter_multi_items +from werkzeug.datastructures import MultiDict +from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import RequestEntityTooLarge +from werkzeug.exceptions import RequestTimeout -from .base import BaseRequestWebsocket from ..formparser import FormDataParser from ..globals import current_app +from .base import BaseRequestWebsocket try: from typing import Literal @@ -42,7 +52,9 @@ class Body: it. """ - def __init__(self, expected_content_length: int | None, max_content_length: int | None) -> None: + def __init__( + self, expected_content_length: int | None, max_content_length: int | None + ) -> None: self._data = bytearray() self._complete: asyncio.Event = asyncio.Event() self._has_data: asyncio.Event = asyncio.Event() @@ -97,7 +109,10 @@ def append(self, data: bytes) -> None: return self._data.extend(data) self._has_data.set() - if self._max_content_length is not None and len(self._data) > self._max_content_length: + if ( + self._max_content_length is not None + and len(self._data) > self._max_content_length + ): self._must_raise = RequestEntityTooLarge() self.set_complete() @@ -192,7 +207,9 @@ async def get_data( ) -> bytes: ... @overload - async def get_data(self, cache: bool, as_text: Literal[True], parse_form_data: bool) -> str: ... + async def get_data( + self, cache: bool, as_text: Literal[True], parse_form_data: bool + ) -> str: ... @overload async def get_data( @@ -218,8 +235,8 @@ async def get_data( try: raw_data = await asyncio.wait_for(self.body, timeout=self.body_timeout) - except asyncio.TimeoutError: - raise RequestTimeout() + except asyncio.TimeoutError as e: + raise RequestTimeout() from e else: if not cache: self.body.clear() @@ -288,14 +305,16 @@ async def _load_form_data(self) -> None: ), timeout=self.body_timeout, ) - except asyncio.TimeoutError: - raise RequestTimeout() + except asyncio.TimeoutError as e: + raise RequestTimeout() from e @property async def json(self) -> Any: return await self.get_json() - async def get_json(self, force: bool = False, silent: bool = False, cache: bool = True) -> Any: + async def get_json( + self, force: bool = False, silent: bool = False, cache: bool = True + ) -> Any: """Parses the body data as JSON and returns it. Arguments: diff --git a/src/quart/wrappers/response.py b/src/quart/wrappers/response.py index 01bf4c02..0d04965a 100644 --- a/src/quart/wrappers/response.py +++ b/src/quart/wrappers/response.py @@ -1,24 +1,24 @@ from __future__ import annotations -from abc import ABC, abstractmethod +from abc import ABC +from abc import abstractmethod +from collections.abc import AsyncGenerator +from collections.abc import AsyncIterable +from collections.abc import AsyncIterator +from collections.abc import Iterable from hashlib import md5 from io import BytesIO from os import PathLike from types import TracebackType -from typing import ( - Any, - AsyncGenerator, - AsyncIterable, - AsyncIterator, - Iterable, - overload, - TYPE_CHECKING, -) +from typing import Any +from typing import overload +from typing import TYPE_CHECKING from aiofiles import open as async_open from aiofiles.base import AiofilesContextManager from aiofiles.threadpool.binary import AsyncBufferedIOBase -from werkzeug.datastructures import ContentRange, Headers +from werkzeug.datastructures import ContentRange +from werkzeug.datastructures import Headers from werkzeug.exceptions import RequestedRangeNotSatisfiable from werkzeug.http import parse_etags from werkzeug.sansio.http import is_resource_modified @@ -26,7 +26,8 @@ from .. import json from ..globals import current_app -from ..utils import file_path_to_path, run_sync_iterable +from ..utils import file_path_to_path +from ..utils import run_sync_iterable if TYPE_CHECKING: from .request import Request @@ -54,7 +55,9 @@ async def __aenter__(self) -> AsyncIterable: pass @abstractmethod - async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: + async def __aexit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: pass @@ -72,7 +75,9 @@ def __init__(self, data: bytes) -> None: async def __aenter__(self) -> DataBody: return self - async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: + async def __aexit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: pass def __aiter__(self) -> AsyncIterator[bytes]: @@ -110,7 +115,9 @@ def __init__(self, iterable: AsyncIterable[Any] | Iterable[Any]) -> None: async def __aenter__(self) -> IterableBody: return self - async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: + async def __aexit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: if hasattr(self.iter, "aclose"): await self.iter.aclose() @@ -132,7 +139,9 @@ class FileBody(ResponseBody): buffer_size = 8192 - def __init__(self, file_path: str | PathLike, *, buffer_size: int | None = None) -> None: + def __init__( + self, file_path: str | PathLike, *, buffer_size: int | None = None + ) -> None: self.file_path = file_path_to_path(file_path) self.size = self.file_path.stat().st_size self.begin = 0 @@ -148,7 +157,9 @@ async def __aenter__(self) -> FileBody: await self.file.seek(self.begin) return self - async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: + async def __aexit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: await self.file_manager.__aexit__(exc_type, exc_value, tb) def __aiter__(self) -> FileBody: @@ -200,7 +211,9 @@ async def __aenter__(self) -> IOBody: self.io_stream.seek(self.begin) return self - async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None: + async def __aexit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: return None def __aiter__(self) -> IOBody: @@ -431,7 +444,9 @@ async def make_conditional( ) -> Response: if request.method in {"GET", "HEAD"}: accept_ranges = _clean_accept_ranges(accept_ranges) - is206 = await self._process_range_request(request, complete_length, accept_ranges) + is206 = await self._process_range_request( + request, complete_length, accept_ranges + ) if not is206 and not is_resource_modified( http_range=request.headers.get("Range"), http_if_range=request.headers.get("If-Range"), diff --git a/src/quart/wrappers/websocket.py b/src/quart/wrappers/websocket.py index ff8d72a4..f2aad87b 100644 --- a/src/quart/wrappers/websocket.py +++ b/src/quart/wrappers/websocket.py @@ -1,7 +1,9 @@ from __future__ import annotations import asyncio -from typing import Any, AnyStr, Callable +from typing import Any +from typing import AnyStr +from typing import Callable from hypercorn.typing import WebsocketScope from werkzeug.datastructures import Headers @@ -40,7 +42,9 @@ def __init__( accept: Idempotent callable to accept the websocket connection. scope: Underlying ASGI scope dictionary. """ - super().__init__("GET", scheme, path, query_string, headers, root_path, http_version, scope) + super().__init__( + "GET", scheme, path, query_string, headers, root_path, http_version, scope + ) self._accept = accept self._close = close self._receive = receive @@ -69,7 +73,9 @@ async def receive_json(self) -> Any: async def send_json(self, *args: Any, **kwargs: Any) -> None: if args and kwargs: - raise TypeError("jsonify() behavior undefined when passed both args and kwargs") + raise TypeError( + "jsonify() behavior undefined when passed both args and kwargs" + ) elif len(args) == 1: data = args[0] else: diff --git a/tests/conftest.py b/tests/conftest.py index e14c22c8..cb29c305 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,8 @@ from __future__ import annotations import pytest -from hypercorn.typing import HTTPScope, WebsocketScope +from hypercorn.typing import HTTPScope +from hypercorn.typing import WebsocketScope @pytest.fixture(name="http_scope") diff --git a/tests/test_app.py b/tests/test_app.py index 02f012da..f5f2dcb5 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,21 +1,28 @@ from __future__ import annotations import asyncio -from typing import AsyncGenerator, NoReturn +from collections.abc import AsyncGenerator +from typing import NoReturn from unittest.mock import AsyncMock import pytest -from hypercorn.typing import HTTPScope, WebsocketScope +from hypercorn.typing import HTTPScope +from hypercorn.typing import WebsocketScope from werkzeug.datastructures import Headers from werkzeug.exceptions import InternalServerError from werkzeug.wrappers import Response as WerkzeugResponse from quart.app import Quart -from quart.globals import session, websocket -from quart.sessions import SecureCookieSession, SessionInterface -from quart.testing import no_op_push, WebsocketResponseError -from quart.typing import ResponseReturnValue, ResponseTypes -from quart.wrappers import Request, Response +from quart.globals import session +from quart.globals import websocket +from quart.sessions import SecureCookieSession +from quart.sessions import SessionInterface +from quart.testing import no_op_push +from quart.testing import WebsocketResponseError +from quart.typing import ResponseReturnValue +from quart.typing import ResponseTypes +from quart.wrappers import Request +from quart.wrappers import Response TEST_RESPONSE = Response("") @@ -74,7 +81,11 @@ def route() -> str: non_func_methods = {"PATCH"} if not methods else None app.add_url_rule( - "/", "end", route, methods=non_func_methods, provide_automatic_options=automatic_options + "/", + "end", + route, + methods=non_func_methods, + provide_automatic_options=automatic_options, ) result = {"PATCH"} if not methods else set() result.update(methods) @@ -109,7 +120,9 @@ def route() -> str: route.provide_automatic_options = func_automatic # type: ignore - app.add_url_rule("/", "end", route, methods=methods, provide_automatic_options=arg_automatic) + app.add_url_rule( + "/", "end", route, methods=methods, provide_automatic_options=arg_automatic + ) assert app.url_map._rules_by_endpoint["end"][0].methods == expected_methods assert ( app.url_map._rules_by_endpoint["end"][0].provide_automatic_options # type: ignore @@ -151,7 +164,11 @@ async def route(subdomain: str) -> str: (None, None, True), ((None, 201), None, True), (TEST_RESPONSE, TEST_RESPONSE, False), - (("hello", {"X-Header": "bob"}), Response("hello", headers={"X-Header": "bob"}), False), + ( + ("hello", {"X-Header": "bob"}), + Response("hello", headers={"X-Header": "bob"}), + False, + ), (("hello", 201), Response("hello", 201), False), ( ("hello", 201, {"X-Header": "bob"}), @@ -238,7 +255,9 @@ def after(_: ResponseTypes) -> None: assert response.status_code == 500 -async def test_app_handle_request_asyncio_cancelled_error(http_scope: HTTPScope) -> None: +async def test_app_handle_request_asyncio_cancelled_error( + http_scope: HTTPScope, +) -> None: app = Quart(__name__) @app.route("/") @@ -339,9 +358,16 @@ async def test_app_session_websocket_return(session_app: Quart) -> None: @pytest.mark.parametrize( "debug, testing, raises", - [(False, False, False), (True, False, True), (False, True, True), (True, True, True)], + [ + (False, False, False), + (True, False, True), + (False, True, True), + (True, True, True), + ], ) -async def test_propagation(debug: bool, testing: bool, raises: bool, http_scope: HTTPScope) -> None: +async def test_propagation( + debug: bool, testing: bool, raises: bool, http_scope: HTTPScope +) -> None: app = Quart(__name__) @app.route("/") diff --git a/tests/test_asgi.py b/tests/test_asgi.py index 91f4dc09..a491fa03 100644 --- a/tests/test_asgi.py +++ b/tests/test_asgi.py @@ -1,23 +1,27 @@ from __future__ import annotations import asyncio -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock +from unittest.mock import Mock import pytest -from hypercorn.typing import ASGIReceiveEvent, ASGISendEvent, HTTPScope, WebsocketScope +from hypercorn.typing import ASGIReceiveEvent +from hypercorn.typing import ASGISendEvent +from hypercorn.typing import HTTPScope +from hypercorn.typing import WebsocketScope from werkzeug.datastructures import Headers from quart import Quart -from quart.asgi import ( - _convert_version, - _handle_exception, - ASGIHTTPConnection, - ASGIWebsocketConnection, -) +from quart.asgi import _convert_version +from quart.asgi import _handle_exception +from quart.asgi import ASGIHTTPConnection +from quart.asgi import ASGIWebsocketConnection from quart.utils import encode_headers -@pytest.mark.parametrize("headers, expected", [([(b"host", b"quart")], "quart"), ([], "")]) +@pytest.mark.parametrize( + "headers, expected", [([(b"host", b"quart")], "quart"), ([], "")] +) async def test_http_1_0_host_header(headers: list, expected: str) -> None: app = Quart(__name__) scope: HTTPScope = { @@ -286,7 +290,9 @@ async def test_websocket_accept_connection( ) -async def test_websocket_accept_connection_warns(websocket_scope: WebsocketScope) -> None: +async def test_websocket_accept_connection_warns( + websocket_scope: WebsocketScope, +) -> None: connection = ASGIWebsocketConnection(Quart(__name__), websocket_scope) async def mock_send(message: ASGISendEvent) -> None: @@ -325,7 +331,9 @@ def test_http_asgi_scope_from_request() -> None: (False, False, True), ], ) -async def test__handle_exception(propagate_exceptions: bool, testing: bool, raises: bool) -> None: +async def test__handle_exception( + propagate_exceptions: bool, testing: bool, raises: bool +) -> None: app = Mock() app.config = {} app.config["PROPAGATE_EXCEPTIONS"] = propagate_exceptions diff --git a/tests/test_background_tasks.py b/tests/test_background_tasks.py index 26452ad1..ead10c86 100644 --- a/tests/test_background_tasks.py +++ b/tests/test_background_tasks.py @@ -3,7 +3,8 @@ import asyncio import time -from quart import current_app, Quart +from quart import current_app +from quart import Quart async def test_background_task() -> None: diff --git a/tests/test_basic.py b/tests/test_basic.py index cc2208e4..aa2fa07b 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,11 +1,19 @@ from __future__ import annotations -from typing import AsyncGenerator, cast +from collections.abc import AsyncGenerator +from typing import cast import pytest from werkzeug.wrappers import Response as WerkzeugResponse -from quart import abort, jsonify, Quart, request, Response, ResponseReturnValue, url_for, websocket +from quart import abort +from quart import jsonify +from quart import Quart +from quart import request +from quart import Response +from quart import ResponseReturnValue +from quart import url_for +from quart import websocket from quart.testing import WebsocketResponseError diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 53caa91f..16f77e47 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -5,16 +5,14 @@ import click import pytest -from quart import ( - abort, - Blueprint, - g, - Quart, - render_template_string, - request, - ResponseReturnValue, - websocket, -) +from quart import abort +from quart import Blueprint +from quart import g +from quart import Quart +from quart import render_template_string +from quart import request +from quart import ResponseReturnValue +from quart import websocket from quart.views import MethodView @@ -332,10 +330,14 @@ async def sibling_index() -> ResponseReturnValue: assert (await (await client.get("/parent/child/")).get_data()) == b"Child yes" assert (await (await client.get("/parent/sibling")).get_data()) == b"Sibling yes" assert (await (await client.get("/alt/sibling")).get_data()) == b"Sibling yes" - assert (await (await client.get("/parent/child/grandchild/")).get_data()) == b"Grandchild yes" + assert ( + await (await client.get("/parent/child/grandchild/")).get_data() + ) == b"Grandchild yes" assert (await (await client.get("/parent/no")).get_data()) == b"Parent no" assert (await (await client.get("/parent/child/no")).get_data()) == b"Parent no" - assert (await (await client.get("/parent/child/grandchild/no")).get_data()) == b"Grandchild no" + assert ( + await (await client.get("/parent/child/grandchild/no")).get_data() + ) == b"Grandchild no" async def test_blueprint_renaming() -> None: diff --git a/tests/test_cli.py b/tests/test_cli.py index 5fe0e7a9..aca9c444 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,8 +2,8 @@ import os import tempfile +from collections.abc import Generator from pathlib import Path -from typing import Generator from unittest.mock import Mock import pytest @@ -12,7 +12,10 @@ import quart.cli from quart.app import Quart -from quart.cli import AppGroup, cli, load_dotenv, ScriptInfo +from quart.cli import AppGroup +from quart.cli import cli +from quart.cli import load_dotenv +from quart.cli import ScriptInfo @pytest.fixture(scope="module") @@ -73,15 +76,27 @@ def test_run_command(app: Mock) -> None: runner = CliRunner() runner.invoke(cli, ["--app", "module:app", "run"]) app.run.assert_called_once_with( - debug=False, host="127.0.0.1", port=5000, certfile=None, keyfile=None, use_reloader=False + debug=False, + host="127.0.0.1", + port=5000, + certfile=None, + keyfile=None, + use_reloader=False, ) -def test_run_command_development_debug_disabled(dev_app: Mock, no_debug_env: None) -> None: +def test_run_command_development_debug_disabled( + dev_app: Mock, no_debug_env: None +) -> None: runner = CliRunner() runner.invoke(cli, ["--app", "module:app", "run"]) dev_app.run.assert_called_once_with( - debug=False, host="127.0.0.1", port=5000, certfile=None, keyfile=None, use_reloader=False + debug=False, + host="127.0.0.1", + port=5000, + certfile=None, + keyfile=None, + use_reloader=False, ) diff --git a/tests/test_ctx.py b/tests/test_ctx.py index 0bccabf8..7450b31e 100644 --- a/tests/test_ctx.py +++ b/tests/test_ctx.py @@ -10,19 +10,20 @@ from werkzeug.exceptions import BadRequest from quart.app import Quart -from quart.ctx import ( - after_this_request, - AppContext, - copy_current_app_context, - copy_current_request_context, - copy_current_websocket_context, - has_app_context, - has_request_context, - RequestContext, -) -from quart.globals import g, request, websocket +from quart.ctx import after_this_request +from quart.ctx import AppContext +from quart.ctx import copy_current_app_context +from quart.ctx import copy_current_request_context +from quart.ctx import copy_current_websocket_context +from quart.ctx import has_app_context +from quart.ctx import has_request_context +from quart.ctx import RequestContext +from quart.globals import g +from quart.globals import request +from quart.globals import websocket from quart.routing import QuartRule -from quart.testing import make_test_headers_path_and_query_string, no_op_push +from quart.testing import make_test_headers_path_and_query_string +from quart.testing import no_op_push from quart.wrappers import Request diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 4a7eda41..71939f0a 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -3,7 +3,8 @@ from http import HTTPStatus import pytest -from werkzeug.exceptions import abort, HTTPException +from werkzeug.exceptions import abort +from werkzeug.exceptions import HTTPException from quart import Response diff --git a/tests/test_helpers.py b/tests/test_helpers.py index d286b010..cbf539a2 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,23 +1,24 @@ from __future__ import annotations -from datetime import datetime, timezone +from collections.abc import AsyncGenerator +from datetime import datetime +from datetime import timezone from io import BytesIO from pathlib import Path -from typing import AsyncGenerator import pytest from werkzeug.exceptions import NotFound -from quart import Blueprint, Quart, request -from quart.helpers import ( - flash, - get_flashed_messages, - make_response, - send_file, - send_from_directory, - stream_with_context, - url_for, -) +from quart import Blueprint +from quart import Quart +from quart import request +from quart.helpers import flash +from quart.helpers import get_flashed_messages +from quart.helpers import make_response +from quart.helpers import send_file +from quart.helpers import send_from_directory +from quart.helpers import stream_with_context +from quart.helpers import url_for SERVER_NAME = "localhost" @@ -37,7 +38,9 @@ async def index() -> str: async def index_post() -> str: return "" - app.add_url_rule("/post", view_func=index_post, methods=["POST"], endpoint="index_post") + app.add_url_rule( + "/post", view_func=index_post, methods=["POST"], endpoint="index_post" + ) @app.route("/resource/") async def resource(id: int) -> str: @@ -80,8 +83,14 @@ async def test_flash_category(app: Quart) -> None: async with app.test_request_context("/"): await flash("bar", "error") await flash("foo", "info") - assert get_flashed_messages(with_categories=True) == [("error", "bar"), ("info", "foo")] - assert get_flashed_messages(with_categories=True) == [("error", "bar"), ("info", "foo")] + assert get_flashed_messages(with_categories=True) == [ + ("error", "bar"), + ("info", "foo"), + ] + assert get_flashed_messages(with_categories=True) == [ + ("error", "bar"), + ("info", "foo"), + ] async def test_flash_category_filter(app: Quart) -> None: @@ -109,7 +118,9 @@ async def test_url_for_external(app: Quart) -> None: async with app.test_request_context("/"): assert url_for("index") == "/" assert url_for("index", _external=True) == "http://localhost/" - assert url_for("resource", id=5, _external=True) == "http://localhost/resource/5" + assert ( + url_for("resource", id=5, _external=True) == "http://localhost/resource/5" + ) assert url_for("resource", id=5, _external=False) == "/resource/5" async with app.app_context(): @@ -216,7 +227,9 @@ async def test_send_file_as_attachment_name(tmp_path: Path) -> None: file_ = tmp_path / "send.img" file_.write_text("something") async with app.app_context(): - response = await send_file(Path(file_), as_attachment=True, attachment_filename="send.html") + response = await send_file( + Path(file_), as_attachment=True, attachment_filename="send.html" + ) assert response.headers["content-disposition"] == "attachment; filename=send.html" @@ -257,4 +270,7 @@ async def test_send_file_max_age(tmp_path: Path) -> None: file_.write_text("something") async with app.app_context(): response = await send_file(str(file_)) - assert response.cache_control.max_age == app.config["SEND_FILE_MAX_AGE_DEFAULT"].total_seconds() + assert ( + response.cache_control.max_age + == app.config["SEND_FILE_MAX_AGE_DEFAULT"].total_seconds() + ) diff --git a/tests/test_routing.py b/tests/test_routing.py index c01ecf93..9756599e 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -15,7 +15,9 @@ "server_name, warns", [("localhost", False), ("quart.com", True)], ) -async def test_bind_warning(server_name: str, warns: bool, http_scope: HTTPScope) -> None: +async def test_bind_warning( + server_name: str, warns: bool, http_scope: HTTPScope +) -> None: map_ = QuartMap(host_matching=False) request = Request( "GET", diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 1579731a..130b0e6b 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -6,12 +6,16 @@ from werkzeug.datastructures import Headers from quart.app import Quart -from quart.sessions import SecureCookieSession, SecureCookieSessionInterface +from quart.sessions import SecureCookieSession +from quart.sessions import SecureCookieSessionInterface from quart.testing import no_op_push -from quart.wrappers import Request, Response +from quart.wrappers import Request +from quart.wrappers import Response -async def test_secure_cookie_session_interface_open_session(http_scope: HTTPScope) -> None: +async def test_secure_cookie_session_interface_open_session( + http_scope: HTTPScope, +) -> None: session = SecureCookieSession() session["something"] = "else" interface = SecureCookieSessionInterface() @@ -20,7 +24,15 @@ async def test_secure_cookie_session_interface_open_session(http_scope: HTTPScop response = Response("") await interface.save_session(app, session, response) request = Request( - "GET", "http", "/", b"", Headers(), "", "1.1", http_scope, send_push_promise=no_op_push + "GET", + "http", + "/", + b"", + Headers(), + "", + "1.1", + http_scope, + send_push_promise=no_op_push, ) request.headers["Cookie"] = response.headers["Set-Cookie"] new_session = await interface.open_session(app, request) diff --git a/tests/test_sync.py b/tests/test_sync.py index 64173fa3..df06adbd 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -1,11 +1,13 @@ from __future__ import annotations import threading -from typing import Generator +from collections.abc import Generator import pytest -from quart import Quart, request, ResponseReturnValue +from quart import Quart +from quart import request +from quart import ResponseReturnValue @pytest.fixture(name="app") diff --git a/tests/test_templating.py b/tests/test_templating.py index 5df54033..50b767aa 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -4,16 +4,14 @@ import pytest -from quart import ( - Blueprint, - g, - Quart, - render_template_string, - Response, - ResponseReturnValue, - session, - stream_template_string, -) +from quart import Blueprint +from quart import g +from quart import Quart +from quart import render_template_string +from quart import Response +from quart import ResponseReturnValue +from quart import session +from quart import stream_template_string @pytest.fixture(scope="function") @@ -137,7 +135,9 @@ def app_test(value: int) -> bool: assert rendered == "foo" async with app.test_request_context("/"): - rendered = await render_template_string("{% if 5 is blueprint_test %}bar{% endif %}") + rendered = await render_template_string( + "{% if 5 is blueprint_test %}bar{% endif %}" + ) assert rendered == "bar" diff --git a/tests/test_testing.py b/tests/test_testing.py index d20a121b..242b468b 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -7,15 +7,19 @@ from werkzeug.datastructures import Headers from werkzeug.wrappers import Response as WerkzeugResponse -from quart import jsonify, Quart, redirect, request, Response, session, websocket +from quart import jsonify +from quart import Quart +from quart import redirect +from quart import request +from quart import Response +from quart import session +from quart import websocket from quart.datastructures import FileStorage -from quart.testing import ( - make_test_body_with_headers, - make_test_headers_path_and_query_string, - make_test_scope, - QuartClient as Client, - WebsocketResponseError, -) +from quart.testing import make_test_body_with_headers +from quart.testing import make_test_headers_path_and_query_string +from quart.testing import make_test_scope +from quart.testing import QuartClient as Client +from quart.testing import WebsocketResponseError async def test_methods() -> None: @@ -36,7 +40,14 @@ async def echo() -> str: @pytest.mark.parametrize( - "path, query_string, subdomain, expected_path, expected_query_string, expected_host", + ( + "path", + "query_string", + "subdomain", + "expected_path", + "expected_query_string", + "expected_host", + ), [ ("/path", {"a": "b"}, None, "/path", b"a=b", "localhost"), ("/path", {"a": ["b", "c"]}, None, "/path", b"a=b&a=c", "localhost"), @@ -64,7 +75,9 @@ def test_build_headers_path_and_query_string( def test_build_headers_path_and_query_string_with_query_string_error() -> None: with pytest.raises(ValueError): - make_test_headers_path_and_query_string(Quart(__name__), "/?a=b", None, {"c": "d"}) + make_test_headers_path_and_query_string( + Quart(__name__), "/?a=b", None, {"c": "d"} + ) def test_build_headers_path_and_query_string_with_auth() -> None: @@ -98,7 +111,9 @@ def test_make_test_body_with_headers_files() -> None: b'\r\n------QuartBoundary\r\nContent-Disposition: form-data; name="a"; ' b'filename="Quart"\r\n\r\nabc\r\n------QuartBoundary--\r\n' ) - assert headers == Headers({"Content-Type": "multipart/form-data; boundary=----QuartBoundary"}) + assert headers == Headers( + {"Content-Type": "multipart/form-data; boundary=----QuartBoundary"} + ) def test_make_test_body_with_headers_form_and_files() -> None: @@ -108,9 +123,12 @@ def test_make_test_body_with_headers_form_and_files() -> None: assert body == ( b'\r\n------QuartBoundary\r\nContent-Disposition: form-data; name="a"; ' b'filename="Quart"\r\n\r\nabc\r\n------QuartBoundary\r\n' - b'Content-Disposition: form-data; name="b"\r\n\r\nc\r\n------QuartBoundary--\r\n' + b'Content-Disposition: form-data; name="b"\r\n\r\nc\r\n' + b"------QuartBoundary--\r\n" + ) + assert headers == Headers( + {"Content-Type": "multipart/form-data; boundary=----QuartBoundary"} ) - assert headers == Headers({"Content-Type": "multipart/form-data; boundary=----QuartBoundary"}) def test_make_test_body_with_headers_json() -> None: @@ -134,7 +152,15 @@ def test_make_test_body_with_headers_argument_error() -> None: ) def test_make_test_scope_with_scope_base(path: str, expected_raw_path: bytes) -> None: scope = make_test_scope( - "http", path, "GET", Headers(), b"", "http", "", "1.1", {"client": ("127.0.0.2", "1234")} + "http", + path, + "GET", + Headers(), + b"", + "http", + "", + "1.1", + {"client": ("127.0.0.2", "1234")}, ) assert scope == { "type": "http", @@ -411,7 +437,9 @@ class OddMiddleware: def __init__(self, app: Callable) -> None: self.app = app - async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None: + async def __call__( + self, scope: dict, receive: Callable, send: Callable + ) -> None: if scope["path"] != "/": await send( { diff --git a/tests/test_utils.py b/tests/test_utils.py index a5d852c6..33ede9d5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,7 +2,8 @@ from werkzeug.datastructures import Headers -from quart.utils import decode_headers, encode_headers +from quart.utils import decode_headers +from quart.utils import encode_headers def test_encode_headers() -> None: diff --git a/tests/test_views.py b/tests/test_views.py index 9e997f88..fe8655b6 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,11 +1,15 @@ from __future__ import annotations -from typing import Any, Callable +from typing import Any +from typing import Callable import pytest -from quart import Quart, request, ResponseReturnValue -from quart.views import MethodView, View +from quart import Quart +from quart import request +from quart import ResponseReturnValue +from quart.views import MethodView +from quart.views import View @pytest.fixture @@ -18,7 +22,9 @@ async def test_view(app: Quart) -> None: class Views(View): methods = ["GET", "POST"] - async def dispatch_request(self, *args: Any, **kwargs: Any) -> ResponseReturnValue: + async def dispatch_request( + self, *args: Any, **kwargs: Any + ) -> ResponseReturnValue: return request.method app.add_url_rule("/", view_func=Views.as_view("simple")) @@ -59,7 +65,9 @@ class Views(View): decorators = [decorate_status_code] methods = ["GET"] - async def dispatch_request(self, *args: Any, **kwargs: Any) -> ResponseReturnValue: + async def dispatch_request( + self, *args: Any, **kwargs: Any + ) -> ResponseReturnValue: return request.method app.add_url_rule("/", view_func=Views.as_view("simple")) diff --git a/tests/wrappers/test_base.py b/tests/wrappers/test_base.py index 29e91146..6c2fa3a5 100644 --- a/tests/wrappers/test_base.py +++ b/tests/wrappers/test_base.py @@ -11,8 +11,12 @@ def test_basic_authorization(http_scope: HTTPScope) -> None: headers = Headers() - headers["Authorization"] = "Basic {}".format(b64encode(b"identity:secret").decode("ascii")) - request = BaseRequestWebsocket("GET", "http", "/", b"", headers, "", "1.1", http_scope) + headers["Authorization"] = "Basic {}".format( + b64encode(b"identity:secret").decode("ascii") + ) + request = BaseRequestWebsocket( + "GET", "http", "/", b"", headers, "", "1.1", http_scope + ) auth = request.authorization assert auth.username == "identity" assert auth.password == "secret" @@ -29,7 +33,9 @@ def test_digest_authorization(http_scope: HTTPScope) -> None: 'response="abcd1235", ' 'opaque="abcd1236"' ) - request = BaseRequestWebsocket("GET", "http", "/", b"", headers, "", "1.1", http_scope) + request = BaseRequestWebsocket( + "GET", "http", "/", b"", headers, "", "1.1", http_scope + ) auth = request.authorization assert auth.username == "identity" assert auth.realm == "realm@rea.lm" @@ -100,7 +106,14 @@ def test_url_structure( http_scope: HTTPScope, ) -> None: base_request_websocket = BaseRequestWebsocket( - method, scheme, path, query_string, Headers({"host": host}), "", "1.1", http_scope + method, + scheme, + path, + query_string, + Headers({"host": host}), + "", + "1.1", + http_scope, ) assert base_request_websocket.path == expected_path @@ -118,7 +131,14 @@ def test_url_structure( def test_query_string(http_scope: HTTPScope) -> None: base_request_websocket = BaseRequestWebsocket( - "GET", "http", "/", b"a=b&a=c&f", Headers({"host": "localhost"}), "", "1.1", http_scope + "GET", + "http", + "/", + b"a=b&a=c&f", + Headers({"host": "localhost"}), + "", + "1.1", + http_scope, ) assert base_request_websocket.query_string == b"a=b&a=c&f" assert base_request_websocket.args.getlist("a") == ["b", "c"] diff --git a/tests/wrappers/test_request.py b/tests/wrappers/test_request.py index 8cf00ab6..e2f02678 100644 --- a/tests/wrappers/test_request.py +++ b/tests/wrappers/test_request.py @@ -6,10 +6,12 @@ import pytest from hypercorn.typing import HTTPScope from werkzeug.datastructures import Headers -from werkzeug.exceptions import RequestEntityTooLarge, RequestTimeout +from werkzeug.exceptions import RequestEntityTooLarge +from werkzeug.exceptions import RequestTimeout from quart.testing import no_op_push -from quart.wrappers.request import Body, Request +from quart.wrappers.request import Body +from quart.wrappers.request import Request async def _fill_body(body: Body, semaphore: asyncio.Semaphore, limit: int) -> None: @@ -110,13 +112,17 @@ async def test_request_get_data_timeout(http_scope: HTTPScope) -> None: "method, expected", [("GET", ["b", "c"]), ("POST", ["b", "c", "d"])], ) -async def test_request_values(method: str, expected: list[str], http_scope: HTTPScope) -> None: +async def test_request_values( + method: str, expected: list[str], http_scope: HTTPScope +) -> None: request = Request( method, "http", "/", b"a=b&a=c", - Headers({"host": "quart.com", "Content-Type": "application/x-www-form-urlencoded"}), + Headers( + {"host": "quart.com", "Content-Type": "application/x-www-form-urlencoded"} + ), "", "1.1", http_scope, diff --git a/tests/wrappers/test_response.py b/tests/wrappers/test_response.py index bda590a2..15980ee2 100644 --- a/tests/wrappers/test_response.py +++ b/tests/wrappers/test_response.py @@ -1,20 +1,27 @@ from __future__ import annotations -from datetime import datetime, timezone +from collections.abc import AsyncGenerator +from datetime import datetime +from datetime import timezone from http import HTTPStatus from io import BytesIO from pathlib import Path -from typing import Any, AsyncGenerator +from typing import Any import pytest -from hypothesis import given, strategies as strategies +from hypothesis import given +from hypothesis import strategies as strategies from werkzeug.datastructures import Headers from werkzeug.exceptions import RequestedRangeNotSatisfiable from quart.testing import no_op_push from quart.typing import HTTPScope from quart.wrappers import Request -from quart.wrappers.response import DataBody, FileBody, IOBody, IterableBody, Response +from quart.wrappers.response import DataBody +from quart.wrappers.response import FileBody +from quart.wrappers.response import IOBody +from quart.wrappers.response import IterableBody +from quart.wrappers.response import Response async def test_data_wrapper() -> None: @@ -32,7 +39,8 @@ async def _simple_async_generator() -> AsyncGenerator[bytes, None]: @pytest.mark.parametrize( - "iterable", [[b"abc", b"def"], (data for data in [b"abc", b"def"]), _simple_async_generator()] + "iterable", + [[b"abc", b"def"], (data for data in [b"abc", b"def"]), _simple_async_generator()], ) async def test_iterable_wrapper(iterable: Any) -> None: wrapper = IterableBody(iterable) @@ -103,7 +111,15 @@ async def test_response_make_conditional(http_scope: HTTPScope) -> None: async def test_response_make_conditional_no_condition(http_scope: HTTPScope) -> None: request = Request( - "GET", "https", "/", b"", Headers(), "", "1.1", http_scope, send_push_promise=no_op_push + "GET", + "https", + "/", + b"", + Headers(), + "", + "1.1", + http_scope, + send_push_promise=no_op_push, ) response = Response(b"abcdef") await response.make_conditional(request, complete_length=6) From d6dd65d9ca4b81ce1a1c6507bd67c9bcdb76d0a8 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 15 Nov 2024 16:46:41 -0800 Subject: [PATCH 3/3] fix mypy findings --- src/quart/app.py | 72 +++++++++++++++++----------------- src/quart/ctx.py | 4 +- src/quart/templating.py | 16 ++++---- src/quart/testing/utils.py | 6 +-- src/quart/typing.py | 7 ++-- src/quart/wrappers/request.py | 6 +-- src/quart/wrappers/response.py | 6 +-- 7 files changed, 53 insertions(+), 64 deletions(-) diff --git a/src/quart/app.py b/src/quart/app.py index 6407dde0..03813e6c 100644 --- a/src/quart/app.py +++ b/src/quart/app.py @@ -125,9 +125,9 @@ from .wrappers import Response from .wrappers import Websocket -try: +if sys.version_info >= (3, 10): from typing import ParamSpec -except ImportError: +else: from typing_extensions import ParamSpec # Python 3.14 deprecated asyncio.iscoroutinefunction, but suggested @@ -154,7 +154,7 @@ T_websocket = TypeVar("T_websocket", bound=WebsocketCallable) T_while_serving = TypeVar("T_while_serving", bound=WhileServingCallable) -T = TypeVar("T") +T = TypeVar("T", bound=Any) P = ParamSpec("P") @@ -458,10 +458,10 @@ async def update_template_context(self, context: dict) -> None: elif has_websocket_context(): names.extend(reversed(websocket_ctx.websocket.blueprints)) # type: ignore - extra_context: dict = {} + extra_context: dict[str, Any] = {} for name in names: for processor in self.template_context_processors[name]: - extra_context.update(await self.ensure_async(processor)()) # type: ignore + extra_context.update(await self.ensure_async(processor)()) # type: ignore[call-overload] original = context.copy() context.update(extra_context) @@ -1027,7 +1027,7 @@ async def handle_http_exception( if handler is None: return error else: - return await self.ensure_async(handler)(error) # type: ignore + return await self.ensure_async(handler)(error) # type: ignore[return-value] async def handle_user_exception( self, error: Exception @@ -1055,7 +1055,7 @@ async def handle_user_exception( handler = self._find_error_handler(error, blueprints) if handler is None: raise error - return await self.ensure_async(handler)(error) # type: ignore + return await self.ensure_async(handler)(error) # type: ignore[return-value] async def handle_exception(self, error: Exception) -> ResponseTypes: """Handle an uncaught exception. @@ -1066,8 +1066,8 @@ async def handle_exception(self, error: Exception) -> ResponseTypes: exc_info = sys.exc_info() await got_request_exception.send_async( self, - _sync_wrapper=self.ensure_async, - exception=error, # type: ignore + _sync_wrapper=self.ensure_async, # type: ignore[arg-type] + exception=error, ) propagate = self.config["PROPAGATE_EXCEPTIONS"] @@ -1088,7 +1088,7 @@ async def handle_exception(self, error: Exception) -> ResponseTypes: handler = self._find_error_handler(server_error, request.blueprints) if handler is not None: - server_error = await self.ensure_async(handler)(server_error) # type: ignore + server_error = await self.ensure_async(handler)(server_error) # type: ignore[assignment] return await self.finalize_request(server_error, from_error_handler=True) @@ -1102,8 +1102,8 @@ async def handle_websocket_exception( exc_info = sys.exc_info() await got_websocket_exception.send_async( self, - _sync_wrapper=self.ensure_async, - exception=error, # type: ignore + _sync_wrapper=self.ensure_async, # type: ignore[arg-type] + exception=error, ) propagate = self.config["PROPAGATE_EXCEPTIONS"] @@ -1124,7 +1124,7 @@ async def handle_websocket_exception( handler = self._find_error_handler(server_error, websocket.blueprints) if handler is not None: - server_error = await self.ensure_async(handler)(server_error) # type: ignore + server_error = await self.ensure_async(handler)(server_error) # type: ignore[assignment] return await self.finalize_websocket(server_error, from_error_handler=True) @@ -1205,8 +1205,8 @@ async def do_teardown_request( await request_tearing_down.send_async( self, - _sync_wrapper=self.ensure_async, - exc=exc, # type: ignore + _sync_wrapper=self.ensure_async, # type: ignore[arg-type] + exc=exc, ) async def do_teardown_websocket( @@ -1229,8 +1229,8 @@ async def do_teardown_websocket( await websocket_tearing_down.send_async( self, - _sync_wrapper=self.ensure_async, - exc=exc, # type: ignore + _sync_wrapper=self.ensure_async, # type: ignore[arg-type] + exc=exc, ) async def do_teardown_appcontext(self, exc: BaseException | None) -> None: @@ -1239,8 +1239,8 @@ async def do_teardown_appcontext(self, exc: BaseException | None) -> None: await self.ensure_async(function)(exc) await appcontext_tearing_down.send_async( self, - _sync_wrapper=self.ensure_async, - exc=exc, # type: ignore + _sync_wrapper=self.ensure_async, # type: ignore[arg-type] + exc=exc, ) def app_context(self) -> AppContext: @@ -1379,8 +1379,8 @@ async def _wrapper() -> None: async def handle_background_exception(self, error: Exception) -> None: await got_background_exception.send_async( self, - _sync_wrapper=self.ensure_async, - exception=error, # type: ignore + _sync_wrapper=self.ensure_async, # type: ignore[arg-type] + exception=error, ) self.log_exception(sys.exc_info()) @@ -1541,7 +1541,7 @@ async def preprocess_request( for function in self.before_request_funcs[name]: result = await self.ensure_async(function)() if result is not None: - return result # type: ignore + return result # type: ignore[return-value] return None @@ -1567,7 +1567,7 @@ async def preprocess_websocket( for function in self.before_websocket_funcs[name]: result = await self.ensure_async(function)() if result is not None: - return result # type: ignore + return result # type: ignore[return-value] return None @@ -1591,7 +1591,7 @@ async def dispatch_request( return await self.make_default_options_response() handler = self.view_functions[request_.url_rule.endpoint] - return await self.ensure_async(handler)(**request_.view_args) # type: ignore + return await self.ensure_async(handler)(**request_.view_args) # type: ignore[return-value] async def dispatch_websocket( self, websocket_context: WebsocketContext | None = None @@ -1607,7 +1607,7 @@ async def dispatch_websocket( self.raise_routing_exception(websocket_) handler = self.view_functions[websocket_.url_rule.endpoint] - return await self.ensure_async(handler)(**websocket_.view_args) # type: ignore + return await self.ensure_async(handler)(**websocket_.view_args) # type: ignore[return-value] async def finalize_request( self, @@ -1627,8 +1627,8 @@ async def finalize_request( response = await self.process_response(response, request_context) await request_finished.send_async( self, - _sync_wrapper=self.ensure_async, - response=response, # type: ignore + _sync_wrapper=self.ensure_async, # type: ignore[arg-type] + response=response, ) except Exception: if not from_error_handler: @@ -1657,8 +1657,8 @@ async def finalize_websocket( response = await self.postprocess_websocket(response, websocket_context) await websocket_finished.send_async( self, - _sync_wrapper=self.ensure_async, - response=response, # type: ignore + _sync_wrapper=self.ensure_async, # type: ignore[arg-type] + response=response, ) except Exception: if not from_error_handler: @@ -1681,7 +1681,7 @@ async def process_response( names = [*(request_context or request_ctx).request.blueprints, None] for function in (request_context or request_ctx)._after_request_functions: - response = await self.ensure_async(function)(response) # type: ignore + response = await self.ensure_async(function)(response) # type: ignore[assignment] for name in names: for function in reversed(self.after_request_funcs[name]): @@ -1709,11 +1709,11 @@ async def postprocess_websocket( names = [*(websocket_context or websocket_ctx).websocket.blueprints, None] for function in (websocket_context or websocket_ctx)._after_websocket_functions: - response = await self.ensure_async(function)(response) # type: ignore + response = await self.ensure_async(function)(response) # type: ignore[assignment] for name in names: for function in reversed(self.after_websocket_funcs[name]): - response = await self.ensure_async(function)(response) # type: ignore + response = await self.ensure_async(function)(response) # type: ignore[assignment] session_ = (websocket_context or websocket_ctx).session if not self.session_interface.is_null_session(session_): @@ -1768,8 +1768,8 @@ async def startup(self) -> None: except Exception as error: await got_serving_exception.send_async( self, - _sync_wrapper=self.ensure_async, - exception=error, # type: ignore + _sync_wrapper=self.ensure_async, # type: ignore[arg-type] + exception=error, ) self.log_exception(sys.exc_info()) raise @@ -1798,8 +1798,8 @@ async def shutdown(self) -> None: except Exception as error: await got_serving_exception.send_async( self, - _sync_wrapper=self.ensure_async, - exception=error, # type: ignore + _sync_wrapper=self.ensure_async, # type: ignore[arg-type] + exception=error, ) self.log_exception(sys.exc_info()) raise diff --git a/src/quart/ctx.py b/src/quart/ctx.py index 65085cc4..cbf05b2d 100644 --- a/src/quart/ctx.py +++ b/src/quart/ctx.py @@ -262,7 +262,7 @@ async def push(self) -> None: self._cv_tokens.append(_cv_app.set(self)) await appcontext_pushed.send_async( self.app, - _sync_wrapper=self.app.ensure_async, # type: ignore + _sync_wrapper=self.app.ensure_async, # type: ignore[arg-type] ) async def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore @@ -282,7 +282,7 @@ async def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ign await appcontext_popped.send_async( self.app, - _sync_wrapper=self.app.ensure_async, # type: ignore + _sync_wrapper=self.app.ensure_async, # type: ignore[arg-type] ) async def __aenter__(self) -> AppContext: diff --git a/src/quart/templating.py b/src/quart/templating.py index 6e5cbcc7..b9644551 100644 --- a/src/quart/templating.py +++ b/src/quart/templating.py @@ -73,16 +73,16 @@ async def render_template_string(source: str, **context: Any) -> str: async def _render(template: Template, context: dict, app: Quart) -> str: await before_render_template.send_async( app, - _sync_wrapper=app.ensure_async, + _sync_wrapper=app.ensure_async, # type: ignore[arg-type] template=template, - context=context, # type: ignore + context=context, ) rendered_template = await template.render_async(context) await template_rendered.send_async( app, - _sync_wrapper=app.ensure_async, + _sync_wrapper=app.ensure_async, # type: ignore[arg-type] template=template, - context=context, # type: ignore + context=context, ) return rendered_template @@ -135,9 +135,9 @@ async def _stream( ) -> AsyncIterator[str]: await before_render_template.send_async( app, - _sync_wrapper=app.ensure_async, + _sync_wrapper=app.ensure_async, # type: ignore[arg-type] template=template, - context=context, # type: ignore + context=context, ) async def generate() -> AsyncIterator[str]: @@ -145,9 +145,9 @@ async def generate() -> AsyncIterator[str]: yield chunk await template_rendered.send_async( app, - _sync_wrapper=app.ensure_async, + _sync_wrapper=app.ensure_async, # type: ignore[arg-type] template=template, - context=context, # type: ignore + context=context, ) # If a request context is active, keep it while generating. diff --git a/src/quart/testing/utils.py b/src/quart/testing/utils.py index 3f09748a..a39e3efb 100644 --- a/src/quart/testing/utils.py +++ b/src/quart/testing/utils.py @@ -3,6 +3,7 @@ from typing import Any from typing import AnyStr from typing import cast +from typing import Literal from typing import overload from typing import TYPE_CHECKING from urllib.parse import unquote @@ -28,11 +29,6 @@ if TYPE_CHECKING: from ..app import Quart # noqa -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore - sentinel = object() diff --git a/src/quart/typing.py b/src/quart/typing.py index 397f2b37..f96bb122 100644 --- a/src/quart/typing.py +++ b/src/quart/typing.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import sys from collections.abc import AsyncGenerator from collections.abc import Awaitable from collections.abc import Iterator @@ -26,10 +27,10 @@ from .datastructures import FileStorage -try: +if sys.version_info >= (3, 10): from typing import Protocol -except ImportError: - from typing_extensions import Protocol # type: ignore +else: + from typing_extensions import Protocol if TYPE_CHECKING: from werkzeug.datastructures import Authorization # noqa: F401 diff --git a/src/quart/wrappers/request.py b/src/quart/wrappers/request.py index 82459422..cf50b6f5 100644 --- a/src/quart/wrappers/request.py +++ b/src/quart/wrappers/request.py @@ -5,6 +5,7 @@ from collections.abc import Generator from typing import Any from typing import Callable +from typing import Literal from typing import NoReturn from typing import overload @@ -21,11 +22,6 @@ from ..globals import current_app from .base import BaseRequestWebsocket -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore - SERVER_PUSH_HEADERS_TO_COPY = { "accept", "accept-encoding", diff --git a/src/quart/wrappers/response.py b/src/quart/wrappers/response.py index 0d04965a..df13ab09 100644 --- a/src/quart/wrappers/response.py +++ b/src/quart/wrappers/response.py @@ -11,6 +11,7 @@ from os import PathLike from types import TracebackType from typing import Any +from typing import Literal from typing import overload from typing import TYPE_CHECKING @@ -32,11 +33,6 @@ if TYPE_CHECKING: from .request import Request -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore - class ResponseBody(ABC): """Base class wrapper for response body data.