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/CHANGES.md b/CHANGES.md new file mode 100644 index 00000000..59341329 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,809 @@ +## Version 0.20.0 + +Unreleased + +- Drop support for Python 3.8. +- Fix deprecated `asyncio.iscoroutinefunction` for Python 3.14. +- Allow `AsyncIterable` to be passed to `Response`. + +## Version 0.19.9 + +Released 2024-11-14 + +- Fix missing `PROVIDE_AUTOMATIC_OPTIONS` config for compatibility with + Flask 3.1. + +## Version 0.19.8 + +Released 2024-10-25 + +- Fix missing check that caused the previous fix to raise an error. + +## Version 0.19.7 + +Released 2024-10-25 + +- Fix how `max_form_memory_size` is applied when parsing large non-file fields. + + +## Version 0.19.6 + +Released 2024-05-19 + +- Use `ContentRange` in the right way. +- Hold a strong reference to background tasks. +- Avoid `ResourceWarning` in `DataBody.__aiter__`. + +## Version 0.19.5 + +Released 2024-04-01 + +- Address `DeprecationWarning` from `datetime.utcnow()`. +- Ensure request files are closed. +- Fix development server restarting when commands are passed. +- Restore `teardown_websocket` methods. +- Correct the `config_class` type. +- Allow `kwargs` to be passed to the test client (matches Flask API). + +## Version 0.19.4 + +Released 2023-11-19 + +- Fix program not closing on Ctrl+C in Windows. +- Fix the typing for `AfterWebsocket` functions. +- Improve the typing of the `ensure_async` method. +- Add a shutdown event to the app. + +## Version 0.19.3 + +Released 2023-10-04 + +- Update the default config to better match Flask. + +## Version 0.19.2 + +Released 2023-10-01 + +- Restore the app `after_`/`before_websocket` methods. +- Correctly set the `cli` group in Quart. + +## Version 0.19.1 + +Released 2023-09-30 + +- Remove `QUART_ENV` and env usage. + +## Version 0.19.0 + +Released 2023-09-30 + +- Remove Flask-Patch. It has been replaced with the Quart-Flask-Patch extension. +- Remove references to first request, as per Flask. +- Await the background tasks before calling the after serving functions. +- Don't copy the app context into the background task. +- Allow background tasks a grace period to complete during shutdown. +- Base Quart on Flask, utilising Flask code where possible. This introduces a + dependency on Flask. +- Fix trailing slash issue in URL concatenation for empty `path`. +- Use only CR in SSE documentation. +- Fix typing for websocket to accept auth data. +- Ensure subdomains apply to nested blueprints. +- Ensure `make_response` errors if the value is incorrect. +- Fix propagated exception handling. +- Ensure exceptions propagate before logging. +- Cope with `scope` extension value being `None`. +- Ensure the conditional 304 response is empty. +- Handle empty path in URL concatenation. +- Corrected typing hint for `abort` method. +- Fix `root_path` usage. +- Fix Werkzeug deprecation warnings. +- Add `.svg` to Jinja's autoescaping. +- Improve the `WebsocketResponse` error, by including the response. +- Add a file `mode` parameter to the `config.from_file` method. +- Show the subdomain or host in the `routes` command output. +- Upgrade to Blinker 1.6. +- Require Werkzeug 3.0.0 and Flask 3.0.0. +- Use `tomllib` rather than `toml`. + +## Version 0.18.4 + +Released 2023-04-09 + +- Restrict Blinker to < 1.6 for 0.18.x versions to ensure it works with Quart's + implementation. + +## Version 0.18.3 + +Released 2022-10-08 + +- Corrected `quart.json.loads` type annotation. +- Fix signal handling on Windows. +- Add missing globals to Flask-Patch. + +## Version 0.18.2 + +Released 2022-10-04 + +- Use `add_signal_handler` not `signal.signal`. + +## Version 0.18.1 + +Released 2022-10-03 + +- Fix static hosting with resource path escaping the root. +- Adopt the Werkzeug/Flask `make_conditional` API/functionality. +- Restore the reloader to Quart. +- Support subdomains when testing. +- Fix the signal handling to work on Windows. + +## Version 0.18.0 + +Released 2022-07-23 + +- Remove Quart's `safe_join`, use Werkzeug's version instead. +- Drop toml dependency, as it isn't required in Quart (use `config.from_file` as + desired). +- Change `websocket.send_json` to match `jsonify`'s options. +- Allow while serving decorators on blueprints. +- Support synchronous background tasks, they will be run on a thread. +- Follow Flask's API and allow empty argument `Response` construction. +- Add `get_root_path` to helpers to match Flask. +- Support `silent` argument in `config.from_envvar`. +- Adopt Flask's logging setup. +- Add `stream_template` and `stream_template_string` functions to stream a large + template in parts. +- Switch to Flask's top level name export style. +- Add `aborter` object to app to allow for `abort` customisation. +- Add `redirect` method to app to allow for `redirect` customisation. +- Remove usage of `LocalStacks`, using `ContextVars` more directly. This should + improve performance, but introduces backwards incompatibility. `_*_ctx_stack` + globals are removed, use `_context` instead. Extensions should store on + `g` as appropriate. Requires Werkzeug >= 2.2.0. +- Returned lists are now jsonified. +- Move `url_for` to the app to allow for `url_for` customisation. +- Remove `config.from_json`, use `from_file` instead. +- Match the Flask views classes and API. +- Adopt the Flask cli code adding `--app`, `--env`, and `-debug` options to the + CLI. +- Adopt the Flask JSON provider interface, use instead of JSON encoders and + decoders. +- Switch to being a Pallets project. +- Requires at least Click version 8. + +## Version 0.17.0 + +Released 2022-03-26 + +- Raise startup and shutdown errors. +- Allow loading of environment variables into the config. +- Switch to Werkzeug's `redirect`. +- Import `Markup` and `escape` from MarkupSafe. + +## Version 0.16.3 + +Released 2022-02-02 + +- Ensure auth is sent on test client requests. + +## Version 0.16.2 + +Released 2021-12-14 + +- Await background task shutdown after shutdown functions. +- Use the before websocket not request functions. + +## Version 0.16.1 + +Released 2021-11-17 + +- Add missing serving exception handling. + +## Version 0.16.0 + +Released 2021-11-09 + +- Support an `auth` argument in the test client. +- Support Python 3.10. +- Utilise `ensure_async` in the copy context functions. +- Add support for background tasks via `app.add_background_task`. +- Give a clearer error for invalid response types. +- Make `open_resource` and `open_instance_resource` async. +- Allow `save_session` to accept None as a response value. +- Rename errors to have an `Error` suffix. +- Fix typing of before (first) request callables. +- Support view sync handlers. +- Fix import of method `redirect` from Flask. +- Fix registering a blueprint twice with differing names. +- Support `ctx.pop()` without passing `exc` explicitly. +- A request timeout error should be raised on timeout. +- Remove Jinja warnings. +- Use the websocket context in the websocket method. +- Raise any lifespan startup failures when testing. +- Fix handler call order based on blueprint nesting. +- Allow for generators that yield strings to be used. +- Reorder acceptance to prevent race conditions. +- Prevent multiple task form body parsing via a lock. + +## Version 0.15.1 + +Released 2021-05-24 + +- Improve the `g` `AppGlobals` typing. +- Fix nested blueprint `url_prefixes`. +- Ensure the session is created before url matching. +- Fix Flask Patch sync wrapping async. +- Don't try to parse the form data multiple times. +- Fix blueprint naming allowing blueprints to be registered with a + different name. +- Fix teardown callable typing. + +## Version 0.15.0 + +Released 2021-05-11 + +- Add the `quart routes` to output the routes in the app. +- Add the ability to close websocket connections with a reason if supported by + the server. +- Revert `AppContext` lifespan interaction change in 0.14. It is not possible to + introduce this and match Flask's `g` usage. +- Add syntactic sugar for route registration allowing `app.get`, `app.post`, + etc. for app and blueprint instances. +- Support handlers returning a Werkzeug `Response`. +- Remove Quart's exceptions and use Werkzeug's. This may cause incompatibility + to fix import from `werkzeug.exceptions` instead of `quart.exceptions`. +- Switch to Werkzeug's locals and Sans-IO wrappers. +- Allow for files to be sent via test client, via a `files` argument. +- Make the `NoAppException` clearer. +- Support nested blueprints. +- Support `while_serving` functionality. +- Correct routing host case matching. +- Cache flashed message on `request.flashes`. +- Fix debug defaults and overrides using `run`. +- Adopt Werkzeug's timestamp parsing. +- Only show the traceback response if propagating exceptions. +- Fix unhandled exception handling. +- Support `url_for` in websocket contexts. +- Fix cookie jar handling in test client. +- Support `SERVER_NAME` configuration for the `run` method. +- Correctly support `root_paths`. +- Support str and bytes streamed responses. +- Match Flask and consume the raw data when form parsing. + +## Version 0.14.1 + +Released 2020-12-13 + +- Add missing receive to test request connection and docs. +- Add the `templates_auto_reload` API. +- Setting the `debug` property on the app now also sets the auto reloading for + the Jinja environment. + +## Version 0.14.0 + +Released 2020-12-05 + +- Add `user_agent` property to requests/websockets to easily extract the user + agent using Werkzeug's `UserAgent` class. +- Set the content length when using send file instead of using chunked + transfer encoding. +- Introduce a `test_app` method, this should be used to ensure that the startup + & shutdown functions are run during testing. +- Prevent local data sharing. +- Officially support Python 3.9. +- Add send and receive json to the test websocket client, allows a simpler way + for json to be sent and received using the app's encoder and decoder. +- Add signals for websocket message receipt and sending, specifically the + `websocket_received` and `websocket_sent` signals. +- Add `dump` and `load` functions to the json module, as matching Flask. +- Enhance the dev server output. +- Change `AppContext` lifespan interaction - this pushes the app context on + startup and pops on shutdown meaning `g` is available throughout without being + cleared. +- Major refactor of the testing system - this ensures that any middleware and + lifespans are correctly tested. It also introduces a`request` method on the + test client for a request connection (like the websocket connection) for + testing streaming. + +## Version 0.13.1 + +Released 2020-09-09 + +- Add the `data` property to the patched request attributes. +- Fix WebSocket ASGI rejection (for servers that don't support the ASGI + WebSocket response extension). +- Don't wrap commands in `with_appcontext` by default. +- Fix CSP parsing for the report-only header. +- Wait for tasks to complete when cancelled. +- Clean up the generator when the response exits. +- Fix request data handling with Flask-Patch. + +## Version 0.13.0 + +Released 2020-07-14 + +- Set cookies from the testing jar for websockets. +- Restore Flask-Patch sync handling to pre 0.11. This means that sync route + handlers, before request, and more, are **not** run in a thread if Flask-Patch + is used. This restores Flask-SQLAlchemy support (with Flask-Patch). +- Accept additional attributes to the delete cookie. + +## Version 0.12.0 + +Released 2020-05-21 + +- Add `certfile` and `keyfile` arguments to cli. +- `Request.host` value returns an empty string rather than `None` for HTTP/1.0 + requests without a `Host` header. +- Fix type of query string argument to Werkzeug `Map` fixing a `TypeError`. +- Add ASGI `scope` dictionary to `request`. +- Ensure that `FlaskGroup` exists when using `flask_patch` by patching the + `flask.cli` module from quart. +- Add `quart.cli.with_appcontext` matching the Flask API. +- Make `quart.Blueprint` registration compatible with `flask.Blueprint`. +- Make the `add_url_rule` API match the Flask API. +- Resolve error handlers by most specific first (matches Flask). +- Support test sessions and context preservation when testing. +- Add `lookup_app` and `lookup_request` to Flask patch globals. +- Make `quart.Blueprint` constructor fully compatible with `flask.Blueprint`. +- Ensure url defaults aren't copied between blueprint routes. + +## Version 0.11.5 + +Released 2020-03-31 + +- Ensure any exceptions are raised in the ASGI handling code. +- Support url defaults in the blueprint API. + +## Version 0.11.4 + +Released 2020-03-29 + +- Add a testing patch to ensure `FlaskClient` exists. +- Security fix for the `htmlsafe` function. +- Default to the map's strict slashes setting. +- Fix host normalisation for route matching. +- Add subdomain to the blueprint API. + +## Version 0.11.3 + +Released 2020-02-26 + +- Lowercase header names passed to cgi `FieldStorage`. + +## Version 0.11.2 + +Released 2020-02-10 + +- Fix debug traceback rendering. +- Fix `multipart/form-data` parsing. +- Uncomment cookie parameters. +- Add `await` to the `LocalProxy` mappings. + +## Version 0.11.1 + +Released 2020-02-09 + +- Fix cors header accessors and setters. +- Fix `iscoroutinefunction` with Python3.7. +- Fix `after_request`/`_websocket` function typing. + +## Version 0.11.0 + +Released 2020-02-08 + +*This contains all the bug fixes from the 0.6 branch.* + +- Allow relative `root_path` values. +- Add a `TooManyRequests`, 429, exception. +- Run synchronous code via a `ThreadPoolExecutor`. This means that sync route + handlers, before request, and more, are run in a thread. + **This is a major change.** +- Add an `asgi_app` method for middleware usage, for example + `quart_app.asgi_app = Middleware(quart_app.asgi_app)`. +- Add a `run_sync` function to run synchronous code in a thread pool with the + Quart contexts present. +- Set cookies on redirects when testing. +- Follow the Flask API for `dumps`/`loads`. +- Support loading configuration with a custom loader, `from_file` this allows + for toml format configurations (among others). +- Match the Werkzeug API in `redirect`. +- Respect `QUART_DEBUG` when using `quart run`. +- Follow the Flask exception propagation rules, ensuring exceptions + are propagated in testing. +- Support Python 3.8. +- Redirect with a 308 rather than 301 (following Flask/Werkzeug). +- Add a `_QUART_PATCHED` marker to all patched modules. +- Ensure multiple cookies are respected during testing. +- Switch to Werkzeug for datastructures and header parsing and dumping. + **This is a major change.** +- Make the lock class customisable by the app subclass, this allows Quart-Trio + to override the lock type. +- Add a `run_task` method to `Quart` (app) class. This is a task based on the + `run` method assumptions that can be awaited or run as desired. +- Switch JSON tag datetime format to allow reading of Flask encoded tags. +- Switch to Werkzeug's cookie code. **This is a major change.** +- Switch to Werkzeug's routing code. **This is a major change.** +- Add signal handling to `run` method, but not the `run_task` method. + +## Version 0.6.15 + +Released 2019-10-17 + +**This is the final 0.6 release and the final release to support Python3.6, +Python3.8 is now available.** + +- Handle `http.request` without a `body` key + +## Version 0.10.0 + +Released 2019-08-30 + +*This contains all the bug fixes from the 0.6 branch.* + +- Support aborting with a `Response` argument. +- Fix JSON type hints to match typeshed. +- Update to Hypercorn 0.7.0 as minimum version. +- Ensure the default response timeout is set. +- Allow returning dictionaries from view functions, this follows a new addition + to Flask. +- Ensure the response timeout has a default. +- Correct testing websocket typing. +- Accept `json`, `data`, or `form` arguments to `test_request_context`. +- Support `send_file` sending a `BytesIO` object. +- Add `samesite` cookie support (requires Python 3.8). +- Add a `ContentSecurityPolicy` datastructure, this follows a new addition to + Werkzeug. +- Unblock logging I/O by logging in separate threads. +- Support ASGI `root_path` as a prepended path to all routes. + +## Version 0.6.14 + +Released 2019-08-30 + +- Follow Werkzeug `LocalProxy` name API. +- Ensure multiple files are correctly loaded. +- Ensure `make_response` status code is an int. +- Be clear about header encoding. +- Ensure loading form/files data is timeout protected. +- Add missing `Unauthorized`, `Forbidden`, and `NotAcceptable` exception + classes. + +## Version 0.9.1 + +Released 2019-05-12 + +- Unquote the path in the test client, following the ASGI standard. +- Follow Werkzeug `LocalProxy` name API. +- Ensure multiple files are correctly loaded. + +## Version 0.9.0 + +Released 2019-04-22 + +*This contains all the bug fixes from the 0.6 and 0.8 branches.* + +- Highlight the traceback line of code when using the debug system. +- Ensure `debug` has an effect when passed to `app.run`. +- Change the `test_request_context` arguments to match the test client `open` + arguments. +- Fix form data loading limit type. +- Support async Session Interfaces (with continued support for sync interfaces). +- Added `before_app_websocket`, and `after_app_websocket` methods to + `Blueprint`. +- Support sending headers on WebSocket acceptance (this requires ASGI server + support, the default Hypercorn supports this). +- Support async teardown functions (with continued support for sync functions). +- Match the Flask API argument order for `send_file` adding a `mimetype` + argument and supporting attachment sending. +- Make the requested subprotocols available via the websocket class, + `websocket.requested_subprotocols`. +- Support session saving with WebSockets (errors for cookie sessions if the + WebSocket connection has been accepted). +- Switch to be an ASGI 3 framework (this requires ASGI server support, the + default Hypercorn supports this). +- Refactor push promise API, removes the `response.push_promises` attribute. +- Accept `Path` types throughout and switch to `Path` types internally. + +## Version 0.6.13 + +Released 2019-04-22 + +- Fix multipart parsing. +- Added `Map.iter_rules(endpoint)` method. +- Cope if there is no source code when using the debug system. + +## Version 0.8.1 + +Released 2019-02-09 + +- Make the `safe_join` function stricter. +- Parse `multipart/form-data` correctly. +- Add missing `await`. + +## Version 0.8.0 + +Released 2019-01-29 + +*This contains all the bug fixes from the 0.6 and 0.7 branches.* + +- Raise an error if the loaded app is not a Quart instance. +- Remove unused `AccessLogAtoms`. +- Change the `Quart.run` method interface, this reduces the available options + for simplicity. See hypercorn for an extended set of deployment configuration. +- Utilise the Hypercorn `serve` function, requires Hypercorn >= 0.5.0. +- Added `list_templates` method to `DispatchingJinjaLoader`. +- Add additional methods to the `Accept` datastructure, specifically keyed + accessors. +- Expand the `abort` functionality and signature, to allow for the `description` + and `name` to be optionally specified. +- Add a `make_push_promise` function, to allow for push promises to be sent at + any time during the request handling e.g. pre-emptive pushes. +- Rethink the Response Body structure to allow for more efficient handling of + file bodies and the ability to extend how files are managed (for Quart-Trio + and others). +- Add the ability to send conditional 206 responses. Optionally, a response can + be made conditional by awaiting the `make_conditional` method with an argument + of the request range. +- Recommend Mangum for serverless deployments. +- Added `instance_path` and `instance_relative_config` to allow for an instance + folder to be used. + +## Version 0.6.12 + +Released 2019-01-29 + +- Raise a `BadRequest` if the body encoding is wrong. +- Limit Hypercorn to versions < 0.6. +- Fix matching of `MIMEAccept` values. +- Handle the special routing case of `/`. +- Ensure sync functions work with async signals. +- Ensure redirect location headers are full URLs. +- Ensure open-ended `Range` header works. +- Ensure `RequestEntityTooLarge` errors are correctly raised. + +## Version 0.7.2 + +Released 2019-01-03 + +- Fix the url display bug. +- Avoid crash in `flask_patch` isinstance. +- Cope with absolute paths sent in the scope. + +## Version 0.7.1 + +Released 2018-12-18 + +- Fix Flask patching step definition. + +## Version 0.7.0 + +Released 2018-12-16 + +- Support only Python 3.7, see the 0.6.X releases for continued Python + 3.6 support. +- Introduce `ContextVar` for local storage. +- Change default redirect status code to 302. +- Support integer/float cookie expires. +- Specify cookie date format (differs to Flask). +- Remove the Gunicorn workers, please use a ASGI server instead. +- Remove Gunicorn compatibility. +- Introduce a `Headers` data structure. +- Implement `follow_redirects` in Quart test client. +- Adopt the ASGI lifespan protocol. + +## Version 0.6.11 + +Released 2018-12-09 + +- Support static files in blueprints. +- Ensure automatic options API matches Flask and works. +- Fix `app.run` SSL usage and Hypercorn compatibility. + +## Version 0.6.10 + +Released 2018-11-12 + +- Fix async body iteration cleanup. + +## Version 0.6.9 + +Released 2018-11-10 + +- Fix async body iteration deadlock. +- Fix ASGI handling to ensure completion. + +## Version 0.6.8 + +Released 2018-10-21 + +- Ensure an event loop is specified on `app.run`. +- Ensure handler responses are finalized. +- Ensure the ASGI callable returns on completion. + +## Version 0.6.7 + +Released 2018-09-23 + +- Fix ASGI conversion of websocket data (str or bytes). +- Ensure redirect url includes host when host matching. +- Ensure query strings are present in redirect urls. +- Ensure header values are string types. +- Fix incorrect endpoint override error for synchronous view functions. + +## Version 0.6.6 + +Released 2018-08-27 + +- Add type conversion to `getlist` (on multidicts) +- Correct ASGI client usage (allows for `None`) +- Ensure overlapping requests work without destroying the other contexts. +- Ensure only integer status codes are accepted. + +## Version 0.6.5 + +Released 2018-08-05 + +- Change default redirect status code to 302. +- Support query string parsing from test client paths. +- Support int/float cookie expires values. +- Correct the cookie date format to RFC 822. +- Copy `sys.modules` to prevent dictionary changed errors. +- Ensure request body iteration returns all data. +- Set `Host` header (if missing) for HTTP/1.0. +- Set the correct defaults for `_external` in `url_for`. + +## Version 0.6.4 + +Released 2018-07-15 + +- Correctly handle request query strings. +- Restore log output when running in development mode. +- Allow for multiple query string values when building urls, e.g. `a=1&a=2`. +- Ensure the Flask Patch system works with Python 3.7. + +## Version 0.6.3 + +Released 2018-07-05 + +- Ensure compatibility with Python 3.7 + +## Version 0.6.2 + +Released 2018-06-24 + +- Remove class member patching from flask-patch system, as it was unreliable. +- Ensure ASGI websocket handler closes on disconnect. +- Cope with optional client values in ASGI scope. + +## Version 0.6.1 + +Released 2018-06-18 + +- Accept `PathLike` objects to the `send_file` function. +- Fix mutable methods in blueprint routes or url rule addition. +- Don't lowercase header values. +- Support automatic options on `View` classes. + +## Version 0.6.0 + +Released 2018-06-11 + +- Quart is now an ASGI framework, and requires an ASGI server to serve requests. + [Hypercorn](https://github.com/pgjones/hypercorn) is used in development and + is recommended for production. Hypercorn is a continuation of the Quart + serving code. +- Add before and after serving functionality, this is provisional. +- Add caching, last modified and etag information to static files served via + `send_file`. +- Fix date formatting in response headers. +- `make_response` should error if response is `None`. +- Deprecate the Gunicorn workers, see ASGI servers (e.g. Uvicorn). +- Ensure shell context processors work. +- Change template context processors to be async, this is backwards + incompatible. +- Change websocket API to be async, this is backwards incompatible. +- Allow the websocket class to be configurable by users. +- Catch signals on Windows. +- Preserve context in Flask-Patch system. +- Add the websocket API to blueprints. +- Add host, subdomain, and default options to websocket routes. +- Support `defaults` on `route` or `add_url_rule` usage. +- Introduce a more useful `BuildError` +- Match Flask after request function execution order. +- Support `required_methods` on view functions. +- Added CORS, Access Control, datastructures to request and response objects. +- Allow type conversion in (CI)MultiDict get. + +## Version 0.5.0 + +Released 2018-04-13 + +- Further API compatibility with Flask, specifically submodules, wrappers, and + the app. +- Ensure error handlers work. +- Await `get_data` in Flask Patch system. +- Fix rule building, specifically additional arguments as query strings. +- Ability to add defaults to routes on definition. +- Allow set_cookie to accept bytes arguments. +- Ensure mimetype are returned. +- Add host matching, and subdomains for routes. +- Introduce implicit sequence conversion to response data. +- URL and host information on requests. +- Add a debug page, which shows tracebacks on errors. +- Fix accept header parsing. +- Cope with multi lists in forms. +- Add cache control, etag and range header structures. +- Add host, url, scheme and path correctly to path wrappers. +- Fix CLI module parsing. +- Add auto reloading on file changes. +- Ignore invalid upgrade headers. +- Fix h2c requests when there is a body (to not upgrade). +- Refactor of websocket API, matching the request API as an analogue. +- Refactor to mitigate DOS attacks, add documentation section. +- Allow event loop to be specified when running apps. +- Ensure automatic options work. +- Rename `TestClient` -> `QuartClient` to match Flask naming. + +## Version 0.4.1 + +Released 2018-01-27 + +- Fix HTTP/2 support and pass h2spec compliance testing. +- Fix Websocket support and pass autobahn fuzzy test compliance testing. +- Fix HEAD request support (don't try to send a body). +- Fix content-type (remove forced override). + +## Version 0.4.0 + +Released 2018-01-14 + +- Change to async signals and context management. This allows the signal + receivers to be async (which is much more useful) but requires changes to any + current usage (notably test contexts). +- Add initial support of websockets. +- Support HTTP/1.1 to HTTP/2 (h2c) upgrades, includes supporting HTTP/2 without + SSL (note browsers don't support this). +- Add timing to access logging. +- Add a new logo :) thanks to @koddr. +- Support streaming of the request body. +- Add initial CLI support, using click. +- Add context copying helper functions and clarify how to stream a response. +- Improved tutorials. +- Allow the request to be limited to prevent DOS attacks. + +## Version 0.3.1 + +Released 2017-10-25 + +- Fix incorrect error message for HTTP/1.1 requests. +- Fix HTTP/1.1 pipelining support and error handling. + +## Version 0.3.0 + +Released 2017-10-10 + +- Change `flask_ext` name to `flask_patch` to clarify that it is not the + pre-existing `flask_ext` system and that it patches Quart to provide + Flask imports. +- Added support for views. +- Match Werkzeug API for FileStorage. +- Support HTTP/2 pipelining. +- Add access logging. +- Add HTTP/2 Server push, see the `push_promises` set on a `Response` object. +- Add idle timeouts. + +## Version 0.2.0 + +Released 2017-07-22 + +*This is still an alpha version of Quart.* + +- Support for Flask extensions via the `flask_ext` module (if imported). +- Initial documentation setup and actual documentation including API docstrings. +- Closer match to the Flask API, most modules now match the Flask public API. + +## Version 0.1.0 + +Released 2017-05-21 + +- Released initial pre alpha version. diff --git a/CHANGES.rst b/CHANGES.rst deleted file mode 100644 index df5c5bf8..00000000 --- a/CHANGES.rst +++ /dev/null @@ -1,767 +0,0 @@ -0.19.9 2024-11-14 ------------------ - -* Fix missing ``PROVIDE_AUTOMATIC_OPTIONS`` config for compatibility - with Flask 3.1. - -0.19.8 2024-10-25 ------------------ - -* Bugfix Fix missing check that caused the previous fix to raise an error. #366 - -0.19.7 2024-10-25 ------------------ - -* Security Fix how ``max_form_memory_size`` is applied when parsing large - non-file fields. https://github.com/advisories/GHSA-q34m-jh98-gwm2 - -0.19.6 2024-05-19 ------------------ - -* Bugfix use ContentRange in the right way. See issue #331. -* Bugfix hold a strong reference to background tasks. -* Bugfix avoid ResourceWarning in DataBody.__aiter__. - -0.19.5 2024-04-01 ------------------ - -* Bugfix DeprecationWarning from datetime.utcnow(). -* Bugfix ensure request files are closed. -* Bugfix development restarting when commands are passed. -* Restore teardown_websocket methods. -* Correct the config_class type. -* Allow kwargs to be passed to the test client (matches Flask API). - -0.19.4 2023-11-19 ------------------ - -* Bugfix program not closing on Ctrl+C in Windows. -* Bugfix the typing for AfterWebsocket functions. -* Improve the typing of the ensure_async method. -* Add a shutdown event to the app. - -0.19.3 2023-10-04 ------------------ - -* Bugfix update the default config to better match Flask. - -0.19.2 2023-10-01 ------------------ - -* Bugfix restore the app {after, before}_websocket methods. -* Bugfix correctly set the cli Group in Quart. - -0.19.1 2023-09-30 ------------------ - -* Bugfix remove QUART_ENV and env usage. - -0.19.0 2023-09-30 ------------------ - -* Remove Flask-Patch. It has been replaced with the Quart-Flask-Patch - extension. -* Remove references to first request, as per Flask. -* Await the background tasks before calling the after serving funcs. -* Don't copy the app context into the background task. -* Allow background tasks a grace period to complete during shutdown. -* Base Quart on Flask, utilising Flask code where possible. This - introduces a dependency on Flask. -* Bugfix trailing slash issue in URL concatenation for empty 'path' -* Bugfix Issue #219. Use only CR in SSE documentation. -* Bugfix typing for websocket to accept auth data. -* Bugfix ensure subdomains apply to nested blueprints. -* Bugfix ensure make_response errors if the value is incorrect. -* Bugfix propagated exception handling. -* Bugfix ensure exceptions propagate before logging. -* Bugfix cope with scope extension value being None. -* Bugfix ensure the conditional 304 response is empty. -* Bugfix handle empty path in URL concatenation. -* Bugfix corrected typing hint for abort method at helpers.py. -* Bugfix root_path usage. -* Fix Werkzeug deprecation warnings. -* Add svg's to jinja's autoescaping. -* Improve the WebsocketResponse error, by including the response. -* Add a file mode parameter to the config.from_file method. -* Show the subdomain or host in the routes command output. -* Upgrade to blinker 1.6. -* Require Werkzeug 3.0.0 and Flask 3.0.0. -* Use tomllib rather than toml. - -0.18.4 2023-04-09 ------------------ - -* Restrict blinker to < 1.6 for 0.18.x versions to ensure it works - with Quart's implementation. - -0.18.3 2022-10-08 ------------------ - -* Fixed Issue #206. Corrected quart.json.loads type annotation. -* Bugfix signal handling on Windows. -* Bugfix add missing globals to Flask-Patch. - -0.18.2 2022-10-04 ------------------ - -* Bugfix use add_signal_handler not signal.signal. - -0.18.1 2022-10-03 ------------------ - -* Bugfix static hosting with resource path escaping the root. -* Bugfix adopt the Werkzeug/Flask make_conditional API/functionality. -* Bugfix restore the reloader to Quart. -* Bugfix support subdomains when testing. -* Bugfix the signal handling to work on Windows. - -0.18.0 2022-07-23 ------------------ - -* Remove Quart's safe_join, use Werkzeug's version instead. -* Drop toml dependency, as it isn't required in Quart (use - config.from_file as desired). -* Change websocket.send_json to match jsonify's options. -* Allow while serving decorators on blueprints. -* Support synchronous background tasks, they will be run on a thread. -* Follow Flask's API an allow empty argument Response construction. -* Add get_root_path to helpers to match Flask. -* Support silent argument in config.from_envvar. -* Adopt Flask's logging setup. -* Add stream_template and stream_template_string functions to stream a - large template in parts. -* Switch to Flask's top level name exportion style. -* Add aborter object to app to allow for abort customisation. -* Add redirect method to app to allow for redirect customisation. -* Remove usage of LocalStacks, using ContextVars more directly. This - should improve performance, but introduces backwards - incompatibility. _*_ctx_stack globals are removed, use *_context - instead. Extensions should store on ``g`` as appropriate. Requires - Werkzeug >= 2.2.0. -* Returned lists are now jsonified. -* Move url_for to the app to allow for url_for customisation. -* Remove config from_json use from_file instead. -* Match the Flask views classes and API. -* Adopt the Flask cli code adding ``--app``, ``--env``, and ``-debug`` - options to the CLI. -* Adopt the Flask JSON provider interface, use instead of JSON - encoders and decoders. -* Switch to being a Pallets project. -* Requires at least v8 of click. - - -0.17.0 2022-03-26 ------------------ - -* Raise startup and shutdown errors. -* Allow loading of environment variables into the config. -* Bugfix switch to Werkzeug's redirect. -* Bugfix import Markup and escape from MarkupSafe. - -0.16.3 2022-02-02 ------------------ - -* Bugfix ensure auth is sent on test client requests. - -0.16.2 2021-12-14 ------------------ - -* Bugfix await background task shutdown after shutdown funcs. -* Bugfix use the before websocket not request funcs. - -0.16.1 2021-11-17 ------------------ - -* Add missing serving exception handling. - -0.16.0 2021-11-09 ------------------ - -* Support an auth argument in the test client. -* Support Python 3.10. -* Utilise ensure_async in the copy context functions. -* Add support for background tasks via ``app.add_background_task``. -* Give a clearer error for invalid response types. -* Make open_resource and open_instance_resource async. -* Allow save_session to accept None as a response value. -* Rename errors to have an ``Error`` suffix. -* Bugfix typing of before (first) request callables. -* Bugfix support view sync handlers. -* Bugfix import of method "redirect" from flask. -* Bugfix registering a blueprint twice with differing names. -* Bugfix support ctx.pop() without passing exc explicitly. -* Bugfix a request timeout error should be raised on timeout. -* Bugfix remove jinja warnings. -* Bugfix use the websocket context in the websocket method. -* Bugfix raise any lifespan startup failures when testing. -* Bugfix handler call order based on blueprint nesting. -* Bugfix allow for generators that yield strings to be used. -* Bugfix reorder acceptance to prevent race conditions. -* Bugfix prevent multiple task form body parsing via a lock. - -0.15.1 2021-05-24 ------------------ - -* Improve the ``g`` AppGlobals typing. -* Bugfix nested blueprint url_prefixes. -* Bugfix ensure the session is created before url matching. -* Bugfix Flask Patch sync wrapping async. -* Bugfix don't try an parse the form data multiple times. -* Bugfix blueprint naming allowing blueprints to be registered with a - different name. -* Bugfix teardown callable typing. - -0.15.0 2021-05-11 ------------------ - -* Add the routes command, ``quart routes`` to output the routes in the - app. -* Add the ability to close websocket connections with a reason if - supported by the server. -* Revert AppContext lifespan interaction change in 0.14.0. It is not - possible to introduce this and match Flask's ``g`` usage. -* Add syntatic sugar for route registration allowing ``app.get``, - ``app.post``, etc... for app and blueprint instances. -* Support handlers returning a Werkzeug Response. -* Remove Quart's exceptions and use Werkzeug's. This may cause - incompatibility to fix import from ``werkzeug.exceptions`` instead - of ``quart.exceptions``. -* Switch to Werkzeug's locals and Sans-IO wrappers. -* Allow for files to be sent via test client, via a ``files`` - argument. -* Make the NoAppException clearer. -* Support nested blueprints. -* Support while_serving functionality. -* Bugfix Correct routing host case matching. -* Bugfix cache flashed msg on request.flashes. -* Bugfix debug defaults and overrides using run. -* Bugfix adopt Werkzeug's timestamp parsing. -* Bugfix only show the traceback response if propagating exceptions. -* Bugfix unhandled exception handling. -* Bugfix support url_for in websocket contexts. -* Bugfix cookie jar handling in test client. -* Bugfix support SERVER_NAME configuration for the run method. -* Bugfix correctly support root_paths. -* Bugfix support str and byte streamed responses. -* Bugfix match Flask and consume the raw data when form parsing. - -0.14.1 2020-12-13 ------------------ - -* Bugfix add missing receive to test request connection and docs. -* Bugfix Add the templates_auto_reload API. -* Bugfix setting the debug property on the app now also sets the auto - reloading for the jinja environment. - -0.14.0 2020-12-05 ------------------ - -* Add user_agent property to requests/websockets - to easily extract - the user agent using Werkzeug's UserAgent class. -* Bugfix set the content length when using send file - instead of - using chunked transfer encoding. -* Introduce a test_app method - this should be used to ensure that - the startup & shutdown functions are run during testing. -* Bugfix prevent local data sharing. -* Officially support Python 3.9. -* Add send and receive json to the test websocket client - allows a - simpler way for json to be sent and received using the app's encoder - and decoder. -* Add signals for websocket message receipt and sending - specifically - the ``websocket_received`` and ``websocket_sent`` signals. -* Add dump and load functions to the json module - as matching Flask. -* Enhance the dev server output. -* Change AppContext lifespan interaction - this pushes the app context - on startup and pops on shutdown meaning ``g`` is available - throughout without being cleared. -* Major refactor of the testing system - this ensures that any - middleware and lifespans are correctly tested. It also introduces a - ``request`` method on the test client for a request connection (like - the websocket connection) for testing streaming. - -0.13.1 2020-09-09 ------------------ - -* Bugfix add the data property to the patched request attributes. -* Bugfix WebSocket ASGI rejection (for servers that don't support the - ASGI WebSocket response extension). -* Bugfix don't wrap commands in with_appcontext by default. -* Bugfix CSP parsing for the report-only header. -* Bugfix wait for tasks to complete when cancelled. -* Bugfix clean up the generator when the response exits. -* Bugfix request data handling with Flask-Patch. - -0.13.0 2020-07-14 ------------------ - -* Bugfix set cookies from the testing jar for websockets. -* Restore Flask-Patch sync handling to pre 0.11. This means that sync - route handlers, before request, and more, are **not** run in a - thread if Flask-Patch is used. This restores Flask-SQLAlchemy - support (with Flask-Patch). -* Bugfix accept additional attributes to the delete cookie. - -0.12.0 2020-05-21 ------------------ - -* Add certfile and keyfile arguments to cli. -* Bugfix request host value returns an empty string rather than None - for HTTP/1.0 requests without a host header. -* Bugfix type of query string argument to Werkzeug Map fixing a - TypeError. -* Add ASGI scope dictionary to request. -* Ensure that FlaskGroup exists when using flask_patch by patchin the - flask.cli module from quart. -* Add quart.cli.with_appcontext matching the Flask API. -* Make the quart.Blueprint registration api compatible with - flask.Blueprint. -* Make the add_url_rule api match the flask API. -* Resolve error handlers by most specific first (matches Flask). -* Support test sessions and context preservation when testing. -* Add lookup_app and lookup_request to flask patch globals. -* Make quart.Blueprint API constructor fully compatible with - flask.Blueprint -* Bugfix ensure (url) defaults aren't copied between blueprint routes. - -0.11.5 2020-03-31 ------------------ - -* Bugfix ensure any exceptions are raised in the ASGI handling code. -* Bugfix support url defaults in the blueprint API. - -0.11.4 2020-03-29 ------------------ - -* Bugfix add a testing patch to ensure FlaskClient exists. -* Security/Bugfix htmlsafe function. -* Bugfix default to the map's strict slashes setting. -* Bugfix host normalisation for route matching. -* Bugfix add subdomain to the blueprint API. - -0.11.3 2020-02-26 ------------------ - -* Bugfix lowercase header names passed to cgi FieldStorage. - -0.11.2 2020-02-10 ------------------ - -* Bugfix debug traceback rendering. -* Bugfix multipart/form-data parsing. -* Bugfix uncomment cookie parameters. -* Bugfix add await to the LocalProxy mappings. - -0.11.1 2020-02-09 ------------------ - -* Bugfix cors header accessors and setters. -* Bugfix iscoroutinefunction with Python3.7. -* Bugfix after request/websocket function typing. - -0.11.0 2020-02-08 ------------------ - -*This contains all the Bugfixes in the 0.6 branch.* - -* Allow relative root_path values. -* Add a TooManyRequests, 429, exception. -* Run synchronous code via a Thread Pool Executor. This means that - sync route handlers, before request, and more, are run in a - thread. **This is a major change.** -* Add an asgi_app method for middleware usage, for example - ``quart_app.asgi_app = Middleware(quart_app.asgi_app)``. -* Add a ``run_sync`` function to run synchronous code in a thread - pool with the Quart contexts present. -* Bugfix set cookies on redirects when testing. -* Bugfix follow the Flask API for dumps/loads. -* Support loading configuration with a custom loader, ``from_file`` - this allows for toml format configurations (among others). -* Bugfix match the Werkzeug API in redirect. -* Bugfix Respect QUART_DEBUG when using ``quart run``. -* Follow the Flask exception propagation rules, ensuring exceptions - are propogated in testing. -* Support Python 3.8. -* Redirect with a 308 rather than 301 (following Flask/Werkzeug). -* Add a _QUART_PATCHED marker to all patched modules. -* Bugfix ensure multiple cookies are respected during testing. -* Switch to Werkzeug for datastructures and header parsing and - dumping. **This is a major change.** -* Make the lock class customisable by the app subclass, this allows - Quart-Trio to override the lock type. -* Add a run_task method to Quart (app) class. This is a task based on - the run method assumptions that can be awaited or run as desired. -* Switch JSON tag datetime format to allow reading of Flask encoded - tags. -* Switch to Werkzeug's cookie code. **This is a major change.** -* Switch to Werkzeug's routing code. **This is a major change.** -* Add signal handling to run method, but not the run_task method. - -0.6.15 2019-10-17 ------------------ - -**This is the final 0.6 release and the final release to support Python3.6, Python3.8 is now available.** - -* Bugfix handle 'http.request' without a 'body' key - -0.10.0 2019-08-30 ------------------ - -*This contains all the Bugfixes in the 0.6 branch.* - -* Support aborting with a Response argument. -* Fix JSON type hints to match typeshed. -* Update to Hypercorn 0.7.0 as minimum version. -* Bugfix ensure the default response timeout is set. -* Allow returning dictionaries from view functions, this follows a new - addition to Flask. -* Bugfix ensure the response timeout has a default. -* Bugfix correct testing-websocket typing. -* Accept json, data, or form arguments to test_request_context. -* Support send_file sending a BytesIO object. -* Add samesite cookie support (requires Python3.8). -* Add a ContentSecurityPolicy datastructure, this follows a new - addition to Werkzeug. -* Unblock logging I/O by logging in separate threads. -* Support ASGI root_path as a prepended path to all routes. - -0.6.14 2019-08-30 ------------------ - -* Bugfix follow Werkzeug LocalProxy name API. -* Bugfix ensure multiple files are correctly loaded. -* Bugfix ensure make_response status code is an int. -* Bugfix be clear about header encoding. -* Bugfix ensure loading form/files data is timeout protected. -* Bugfix add missing Unauthorized, Forbidden, and NotAcceptable - exception classes. - -0.9.1 2019-05-12 ----------------- - -* Bugfix unquote the path in the test client, following the ASGI - standard. -* Bugfix follow Werkzeug LocalProxy name API. -* Bugfix ensure multiple files are correctly loaded. - -0.9.0 2019-04-22 ----------------- - -*This contains all the Bugfixes in the 0.6 and 0.8 branches.* - -* Highlight the traceback line of code when using the debug system. -* Bugfix ensure debug has an affect when passed to app run. -* Change the test_request_context arguments to match the test client - open arguments. -* Bugfix form data loading limit type. -* Support async Session Interfaces (with continued support for sync - interfaces). -* Added before_app_websocket, and after_app_websocket methods to the - Blueprint. -* Support sending headers on WebSocket acceptance (this requires ASGI - server support, the default Hypercorn supports this). -* Support async teardown functions (with continued support for sync - functions). -* Match the Flask API argument order for send_file adding a mimetype - argument and supporting attachment sending. -* Make the requested subprotocols available via the websocket class, - ``websocket.requested_subprotocols``. -* Support session saving with WebSockets (errors for cookie sessions - if the WebSocket connection has been accepted). -* Switch to be an ASGI 3 framework (this requires ASGI server support, - the default Hypercorn supports this). -* Refactor push promise API, the removes the - ``response.push_promises`` attribute. -* Aceept Path (types) throughout and switch to Path (types) - internally. - -0.6.13 2019-04-22 ------------------ - -* Bugfix multipart parsing. -* Added Map.iter_rules(endpoint) Method. -* Bugfix cope if there is no source code (when using the debug - system). - -0.8.1 2019-02-09 ----------------- - -* Bugfix make the safe_join function stricter. -* Bugfix parse multipart form data correctly. -* Bugfix add missing await. - -0.8.0 2019-01-29 ----------------- - -*This contains all the Bugfixes in the 0.6 and 0.7 branches.* - -* Bugfix raise an error if the loaded app is not a Quart instance. -* Remove unused AccessLogAtoms -* Change the Quart::run method interface, this reduces the available - options for simplicity. See hypercorn for an extended set of - deployment configuration. -* Utilise the Hypercorn serve function, requires Hypercorn >= 0.5.0. -* Added list_templates method to DispatchingJinjaLoader. -* Add additional methods to the Accept datastructure, specifically - keyed accessors. -* Expand the abort functionality and signature, to allow for the - description and name to be optionally specified. -* Add a make_push_promise function, to allow for push promises to be - sent at any time during the request handling e.g. pre-emptive - pushes. -* Rethink the Response Body structure to allow for more efficient - handling of file bodies and the ability to extend how files are - managed (for Quart-Trio and others). -* Add the ability to send conditional 206 responses. Optionally a - response can be made conditional by awaiting the make_conditional - method with an argument of the request range. -* Recommend Mangum for serverless deployments. -* Added instance_path and instance_relative_config to allow for an - instance folder to be used. - -0.6.12 2019-01-29 ------------------ - -* Bugfix raise a BadRequest if the body encoding is wrong. -* Limit Hypercorn to versions < 0.6. -* Bugfix matching of MIMEAccept values. -* Bugfix handle the special routing case of /. -* Bugfix ensure sync functions work with async signals. -* Bugfix ensure redirect location headers are full URLs. -* Bugfix ensure open ended Range header works. -* Bugfix ensure RequestEntityTooLarge errors are correctly raised. - -0.7.2 2019-01-03 ----------------- - -* Fix the url display bug. -* Avoid crash in flask_patch isinstance. -* Cope with absolute paths sent in the scope. - -0.7.1 2018-12-18 ----------------- - -* Bugfix Flask patching step definition. - -0.7.0 2018-12-16 ----------------- - -* Support only Python 3.7, see the 0.6.X releases for continued Python - 3.6 support. -* Introduce ContextVars for local storage. -* Change default redirect status code to 302. -* Support integer/float cookie expires. -* Specify cookie date format (differs to Flask). -* Remove the Gunicorn workers, please use a ASGI server instead. -* Remove Gunicorn compatibility. -* Introduce a Headers data structure. -* Implement follow_redirects in Quart test client. -* Adopt the ASGI lifespan protocol. - -0.6.11 2018-12-09 ------------------ - -* Bugfix support static files in blueprints. -* Bugfix ensure automatic options API matches Flask and works. -* Bugfix app.run SSL usage and Hypercorn compatibility. - -0.6.10 2018-11-12 ------------------ - -* Bugfix async body iteration cleanup. - -0.6.9 2018-11-10 ----------------- - -* Bugfix async body iteration deadlock. -* Bufgix ASGI handling to ensure completion. - -0.6.8 2018-10-21 ----------------- - -* Ensure an event loop is specified on app.run. -* Bugfix ensure handler responses are finalized. -* Bugfix ensure the ASGI callable returns on completion. - -0.6.7 2018-09-23 ----------------- - -* Bugfix ASGI conversion of websocket data (str or bytes). -* Bugfix ensure redirect url includes host when host matching. -* Bugfix ensure query strings are present in redirect urls. -* Bugfix ensure header values are string types. -* Bugfix incorrect endpoint override error for synchronous view - functions. - -0.6.6 2018-08-27 ----------------- - -* Bugfix add type conversion to getlist (on multidicts) -* Bugfix correct ASGI client usage (allows for None) -* Bugfix ensure overlapping requests work without destroying the - others context. -* Bugfix ensure only integer status codes are accepted. - -0.6.5 2018-08-05 ----------------- - -* Bugfix change default redirect status code to 302. -* Bugfix support query string parsing from test client paths. -* Bugfix support int/float cookie expires values. -* Bugfix correct the cookie date format to RFC 822. -* Bugfix copy sys.modules to prevent dictionary changed errors. -* Bugfix ensure request body iteration returns all data. -* Bugfix correct set host header (if missing) for HTTP/1.0. -* Bugfix set the correct defaults for _external in url_for. - -0.6.4 2018-07-15 ----------------- - -* Bugfix correctly handle request query strings. -* Restore log output when running in development mode. -* Bugfix allow for multiple query string values when building urls, - e.g. ``a=1&a=2``. -* Bugfix ensure the Flask Patch system works with Python 3.7. - -0.6.3 2018-07-05 ----------------- - -* Bugfix ensure compatibility with Python 3.7 - -0.6.2 2018-06-24 ----------------- - -* Bugfix remove class member patching from flask-patch system, as was - unreliable. -* Bugfix ensure ASGI websocket handler closes on disconnect. -* Bugfix cope with optional client values in ASGI scope. - -0.6.1 2018-06-18 ----------------- - -* Bugfix accept PathLike objects to the ``send_file`` function. -* Bugfix mutable methods in blueprint routes or url rule addition. -* Bugfix don't lowercase header values. -* Bugfix support automatic options on View classes. - -0.6.0 2018-06-11 ----------------- - -* Quart is now an ASGI framework, and requires an ASGI server to serve - requests. `Hypercorn `_ is - used in development and is recommended for production. Hypercorn - is a continuation of the Quart serving code. -* Add before and after serving functionality, this is provisional. -* Add caching, last modified and etag information to static files - served via send_file. -* Bugfix date formatting in response headers. -* Bugfix make_response should error if response is None. -* Deprecate the Gunicorn workers, see ASGI servers (e.g. Uvicorn). -* Bugfix ensure shell context processors work. -* Change template context processors to be async, this is backwards - incompatible. -* Change websocket API to be async, this is backwards incompatible. -* Allow the websocket class to be configurable by users. -* Bugfix catch signals on Windows. -* Perserve context in Flask-Patch system. -* Add the websocket API to blueprints. -* Add host, subdomain, and default options to websocket routes. -* Bugfix support defaults on route or add_url_rule usage. -* Introduce a more useful BuildError -* Bugfix match Flask after request function execution order. -* Support ``required_methods`` on view functions. -* Added CORS, Access Control, datastructures to request and response - objects. -* Allow type conversion in (CI)MultiDict get. - -0.5.0 2018-04-13 ----------------- - -* Further API compatibility with Flask, specifically submodules, - wrappers, and the app. -* Bugfix ensure error handlers work. -* Bugfix await get_data in Flask Patch system. -* Bugfix rule building, specifically additional arguments as query - strings. -* Ability to add defaults to routes on definition. -* Bugfix allow set_cookie to accept bytes arguments. -* Bugfix ensure mimetype are returned. -* Add host matching, and subdomains for routes. -* Introduce implicit sequence conversion to response data. -* URL and host information on requests. -* Add a debug page, which shows tracebacks on errors. -* Bugfix accept header parsing. -* Bugfix cope with multi lists in forms. -* Add cache control, etag and range header structures. -* Add host, url, scheme and path correctly to path wrappers. -* Bugfix CLI module parsing. -* Add auto reloading on file changes. -* Bugfix ignore invalid upgrade headers. -* Bugfix h2c requests when there is a body (to not upgrade). -* Refactor of websocket API, matching the request API as an analogue. -* Refactor to mitigate DOS attacks, add documentation section. -* Allow event loop to be specified when running apps. -* Bugfix ensure automatic options work. -* Rename TestClient -> QuartClient to match Flask naming. - -0.4.1 2018-01-27 ----------------- - -* Bugfix HTTP/2 support and pass h2spec compliance testing. -* Bugifx Websocket support and pass autobahn fuzzy test compliance - testing. -* Bugfix HEAD request support (don't try to send a body). -* Bugfix content-type (remove forced override). - -0.4.0 2018-01-14 ----------------- - -* Change to async signals and context management. This allows the - signal receivers to be async (which is much more useful) but - requires changes to any current usage (notably test contexts). -* Add initial support of websockets. -* Support HTTP/1.1 to HTTP/2 (h2c) upgrades, includes supporting - HTTP/2 without SSL (note browsers don't support this). -* Add timing to access logging. -* Add a new Logo :). Thanks to @koddr. -* Support streaming of the request body. -* Add initial CLI support, using click. -* Add context copying helper functions and clarify how to stream a - response. -* Improved tutorials. -* Allow the request to be limited to prevent DOS attacks. - -0.3.1 2017-10-25 ----------------- - -* Fix incorrect error message for HTTP/1.1 requests. -* Fix HTTP/1.1 pipelining support and error handling. - -0.3.0 2017-10-10 ----------------- - -* Change flask_ext name to flask_patch to clarify that it is not the - pre-existing flask_ext system and that it patches Quart to provide - Flask imports. -* Added support for views. -* Match Werkzeug API for FileStorage. -* Support HTTP/2 pipelining. -* Add access logging. -* Add HTTP/2 Server push, see the ``push_promises`` Set on a Response - object. -* Add idle timeouts. - -0.2.0 2017-07-22 ----------------- - -This is still an alpha version of Quart, some notable changes are, - -* Support for Flask extensions via the flask_ext module (if imported). -* Initial documentation setup and actual documentation including API - docstrings. -* Closer match to the Flask API, most modules now match the Flask - public API. - -0.1.0 2017-05-21 ----------------- - -* Released initial pre alpha version. 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.md b/docs/changes.md new file mode 100644 index 00000000..5f522c22 --- /dev/null +++ b/docs/changes.md @@ -0,0 +1,4 @@ +# Changes + +```{include} ../CHANGES.md +``` 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.md b/docs/license.md new file mode 100644 index 00000000..01aee1b5 --- /dev/null +++ b/docs/license.md @@ -0,0 +1,6 @@ +MIT License +=========== + +```{literalinclude} ../LICENSE.txt +:language: text +``` 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/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/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..03813e6c 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,98 +30,104 @@ 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 - -try: +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 + +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 @@ -150,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") @@ -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] @@ -454,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) @@ -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) @@ -1010,9 +1027,11 @@ 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) -> 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 @@ -1036,7 +1055,7 @@ async def handle_user_exception(self, error: Exception) -> HTTPException | Respo 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. @@ -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, # type: ignore[arg-type] + exception=error, ) propagate = self.config["PROPAGATE_EXCEPTIONS"] @@ -1067,18 +1088,22 @@ 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) - 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, # type: ignore[arg-type] + exception=error, ) propagate = self.config["PROPAGATE_EXCEPTIONS"] @@ -1099,13 +1124,14 @@ async def handle_websocket_exception(self, error: Exception) -> ResponseTypes | 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) 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, # type: ignore[arg-type] + exc=exc, ) 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, # type: ignore[arg-type] + exc=exc, ) 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, # type: ignore[arg-type] + exc=exc, ) 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, # type: ignore[arg-type] + exception=error, ) 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 @@ -1491,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 @@ -1517,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 @@ -1541,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 @@ -1557,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, @@ -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, # type: ignore[arg-type] + response=response, ) 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, # type: ignore[arg-type] + response=response, ) except Exception: if not from_error_handler: @@ -1627,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]): @@ -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( @@ -1653,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_): @@ -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, # type: ignore[arg-type] + exception=error, ) 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, # type: ignore[arg-type] + exception=error, ) 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..cbf05b2d 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[arg-type] ) 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[arg-type] ) 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..b9644551 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, # type: ignore[arg-type] + template=template, + context=context, ) 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, # type: ignore[arg-type] + template=template, + context=context, ) 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, # type: ignore[arg-type] + template=template, + context=context, ) 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, # type: ignore[arg-type] + template=template, + context=context, ) # 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..a39e3efb 100644 --- a/src/quart/testing/utils.py +++ b/src/quart/testing/utils.py @@ -1,11 +1,25 @@ 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 Literal +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 @@ -15,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() @@ -89,9 +98,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 +125,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..f96bb122 100644 --- a/src/quart/typing.py +++ b/src/quart/typing.py @@ -1,44 +1,40 @@ from __future__ import annotations import os -from datetime import datetime, timedelta +import sys +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 -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, 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 +50,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 +58,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 +100,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 +128,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 +302,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..cf50b6f5 100644 --- a/src/quart/wrappers/request.py +++ b/src/quart/wrappers/request.py @@ -1,20 +1,26 @@ 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 Literal +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 - -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore +from .base import BaseRequestWebsocket SERVER_PUSH_HEADERS_TO_COPY = { "accept", @@ -42,7 +48,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 +105,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 +203,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 +231,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 +301,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..df13ab09 100644 --- a/src/quart/wrappers/response.py +++ b/src/quart/wrappers/response.py @@ -1,24 +1,25 @@ 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 Literal +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,16 +27,12 @@ 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 -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore - class ResponseBody(ABC): """Base class wrapper for response body data. @@ -54,7 +51,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 +71,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 +111,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 +135,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 +153,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 +207,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 +440,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) 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}