diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 45198266..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "pallets/flask", - "image": "mcr.microsoft.com/devcontainers/python:3", - "customizations": { - "vscode": { - "settings": { - "python.defaultInterpreterPath": "${workspaceFolder}/.venv", - "python.terminal.activateEnvInCurrentTerminal": true, - "python.terminal.launchArgs": [ - "-X", - "dev" - ] - } - } - }, - "onCreateCommand": ".devcontainer/on-create-command.sh" -} diff --git a/.devcontainer/on-create-command.sh b/.devcontainer/on-create-command.sh deleted file mode 100755 index eaebea61..00000000 --- a/.devcontainer/on-create-command.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -e -python3 -m venv --upgrade-deps .venv -. .venv/bin/activate -pip install -r requirements/dev.txt -pip install -e . -pre-commit install --install-hooks diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 2ff985a6..00000000 --- a/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 0917c797..00000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Bug report -about: Report a bug in Flask (not other projects which depend on Flask) ---- - - - - - - - -Environment: - -- Python version: -- Flask version: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index a8f9f0b7..00000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,11 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Security issue - url: https://github.com/pallets/flask/security/advisories/new - about: Do not report security issues publicly. Create a private advisory. - - name: Questions on GitHub Discussions - url: https://github.com/pallets/flask/discussions/ - about: Ask questions about your own code on the Discussions tab. - - name: Questions on Discord - url: https://discord.gg/pallets - 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 deleted file mode 100644 index 52c2aed4..00000000 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: Feature request -about: Suggest a new feature for Flask ---- - - - - diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index eb124d25..00000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml deleted file mode 100644 index 533151a8..00000000 --- a/.github/workflows/lock.yaml +++ /dev/null @@ -1,26 +0,0 @@ -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: {} -concurrency: - group: lock - cancel-in-progress: true -jobs: - lock: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - discussions: write - steps: - - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0 - with: - issue-inactive-days: 14 - pr-inactive-days: 14 - discussion-inactive-days: 14 diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml deleted file mode 100644 index eff10995..00000000 --- a/.github/workflows/pre-commit.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: pre-commit -on: - pull_request: - push: - branches: [main, stable] -permissions: {} -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true -jobs: - main: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - with: - enable-cache: true - prune-cache: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - id: setup-python - with: - python-version-file: pyproject.toml - - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - with: - path: ~/.cache/pre-commit - key: pre-commit|${{ hashFiles('pyproject.toml', '.pre-commit-config.yaml') }} - - run: uv run --locked --no-default-groups --group pre-commit pre-commit run --show-diff-on-failure --color=always --all-files diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml deleted file mode 100644 index 0c4f301a..00000000 --- a/.github/workflows/publish.yaml +++ /dev/null @@ -1,62 +0,0 @@ -name: Publish -on: - push: - tags: ['*'] -permissions: {} -concurrency: - group: publish-${{ github.event.push.ref }} - cancel-in-progress: true -jobs: - build: - runs-on: ubuntu-latest - outputs: - artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - with: - enable-cache: false - prune-cache: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version-file: pyproject.toml - - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV - - run: uv build - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - id: upload-artifact - with: - name: dist - path: dist/ - if-no-files-found: error - create-release: - needs: [build] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - artifact-ids: ${{ needs.build.outputs.artifact-id }} - path: dist/ - - name: create release - run: gh release create --draft --repo ${GITHUB_REPOSITORY} ${GITHUB_REF_NAME} dist/* - env: - GH_TOKEN: ${{ github.token }} - publish-pypi: - needs: [build] - environment: - name: publish - url: https://pypi.org/project/Flask/${{ github.ref_name }} - runs-on: ubuntu-latest - permissions: - id-token: write - steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - artifact-ids: ${{ needs.build.outputs.artifact-id }} - path: dist/ - - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - packages-dir: "dist/" diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml deleted file mode 100644 index 97064c8c..00000000 --- a/.github/workflows/tests.yaml +++ /dev/null @@ -1,63 +0,0 @@ -name: Tests -on: - pull_request: - paths-ignore: ['docs/**', 'README.md'] - push: - branches: [main, stable] - paths-ignore: ['docs/**', 'README.md'] -permissions: {} -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true -jobs: - tests: - name: ${{ matrix.name || matrix.python }} - runs-on: ${{ matrix.os || 'ubuntu-latest' }} - strategy: - fail-fast: false - matrix: - include: - - {python: '3.14'} - - {python: '3.14t'} - - {name: Windows, python: '3.14', os: windows-latest} - - {name: Mac, python: '3.14', os: macos-latest} - - {python: '3.13'} - - {python: '3.12'} - - {python: '3.11'} - - {python: '3.10'} - - {name: PyPy, python: 'pypy-3.11', tox: pypy3.11} - - {name: Minimum Versions, python: '3.14', tox: tests-min} - - {name: Development Versions, python: '3.10', tox: tests-dev} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - with: - enable-cache: true - prune-cache: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ matrix.python }} - - run: uv run --locked --no-default-groups --group dev tox run - env: - TOX_ENV: ${{ matrix.tox || format('py{0}', matrix.python) }} - typing: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - with: - enable-cache: true - prune-cache: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version-file: pyproject.toml - - name: cache mypy - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - with: - path: ./.mypy_cache - key: mypy|${{ hashFiles('pyproject.toml') }} - - run: uv run --locked --no-default-groups --group dev tox run -e typing diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml deleted file mode 100644 index 04082427..00000000 --- a/.github/workflows/zizmor.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: GitHub Actions security analysis with zizmor -on: - pull_request: - paths: ["**/*.yaml?"] - push: - branches: [main, stable] - paths: ["**/*.yaml?"] -permissions: {} -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true -jobs: - zizmor: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 - with: - advanced-security: false - annotations: true diff --git a/.gitignore b/.gitignore index 8441e5a6..5c012aaa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ -.idea/ -.vscode/ -__pycache__/ -dist/ -.coverage* -htmlcov/ -.tox/ -docs/_build/ +.DS_Store +*.pyc +*.pyo +env +env* +dist +*.egg +*.egg-info +_mailinglist +.tox diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..bf7b494f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "docs/_themes"] + path = docs/_themes + url = git://github.com/mitsuhiko/flask-sphinx-themes.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 5d1c89cb..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,23 +0,0 @@ -repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 5e2fb545eba1ea9dc051f6f962d52fe8f76a9794 # frozen: v0.15.13 - hooks: - - id: ruff-check - - id: ruff-format - - repo: https://github.com/astral-sh/uv-pre-commit - rev: fa60a193803535a9e2accdb3ca4b1b584b1150cb # frozen: 0.11.15 - hooks: - - id: uv-lock - - repo: https://github.com/codespell-project/codespell - rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2 - hooks: - - id: codespell - args: ['--write-changes'] - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 - hooks: - - id: check-merge-conflict - - id: debug-statements - - id: fix-byte-order-marker - - id: trailing-whitespace - - id: end-of-file-fixer diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index acbd83f9..00000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,10 +0,0 @@ -version: 2 -build: - os: ubuntu-24.04 - tools: - python: '3.13' - commands: - - asdf plugin add uv - - asdf install uv latest - - asdf global uv latest - - uv run --group docs sphinx-build -W -b dirhtml docs $READTHEDOCS_OUTPUT/html diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..0f2a9827 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,30 @@ +Flask is written and maintained by Armin Ronacher and +various contributors: + +Development Lead +```````````````` + +- Armin Ronacher + +Patches and Suggestions +``````````````````````` + +- Adam Zapletal +- Ali Afshar +- Chris Edgemon +- Chris Grindstaff +- Christopher Grebs +- Florent Xicluna +- Georg Brandl +- Justin Quick +- Kenneth Reitz +- Marian Sigler +- Matt Campell +- Matthew Frazier +- Michael van Tellingen +- Ron DuPlain +- Sebastien Estienne +- Simon Sapin +- Stephane Wirtel +- Thomas Schranz +- Zhao Xiaohong diff --git a/CHANGES b/CHANGES new file mode 100644 index 00000000..014d579f --- /dev/null +++ b/CHANGES @@ -0,0 +1,302 @@ +Flask Changelog +=============== + +Here you can see the full list of changes between each Flask release. + +Version 0.8.1 +------------- + +Bugfix release, released on July 1th 2012 + +- Fixed an issue with the undocumented `flask.session` module to not + work properly on Python 2.5. It should not be used but did cause + some problems for package managers. + +Version 0.8 +----------- + +Released on September 29th 2011, codename Rakija + +- Refactored session support into a session interface so that + the implementation of the sessions can be changed without + having to override the Flask class. +- Empty session cookies are now deleted properly automatically. +- View functions can now opt out of getting the automatic + OPTIONS implementation. +- HTTP exceptions and Bad Request errors can now be trapped so that they + show up normally in the traceback. +- Flask in debug mode is now detecting some common problems and tries to + warn you about them. +- Flask in debug mode will now complain with an assertion error if a view + was attached after the first request was handled. This gives earlier + feedback when users forget to import view code ahead of time. +- Added the ability to register callbacks that are only triggered once at + the beginning of the first request. (:meth:`Flask.before_first_request`) +- Malformed JSON data will now trigger a bad request HTTP exception instead + of a value error which usually would result in a 500 internal server + error if not handled. This is a backwards incompatible change. +- Applications now not only have a root path where the resources and modules + are located but also an instane path which is the designated place to + drop files that are modified at runtime (uploads etc.). Also this is + conceptionally only instance depending and outside version control so it's + the perfect place to put configuration files etc. For more information + see :ref:`instance-folders`. +- Added the ``APPLICATION_ROOT`` configuration variable. +- Implemented :meth:`~flask.testing.TestClient.session_transaction` to + easily modify sessions from the test environment. +- Refactored test client internally. The ``APPLICATION_ROOT`` configuration + variable as well as ``SERVER_NAME`` are now properly used by the test client + as defaults. +- Added :attr:`flask.views.View.decorators` to support simpler decorating of + pluggable (class based) views. +- Fixed an issue where the test client if used with the with statement did not + trigger the execution of the teardown handlers. +- Added finer control over the session cookie parameters. +- HEAD requests to a method view now automatically dispatch to the `get` + method if no handler was implemented. +- Implemented the virtual :mod:`flask.ext` package to import extensions from. +- The context preservation on exceptions is now an integral component of + Flask itself and no longer of the test client. This cleaned up some + internal logic and lowers the odds of runaway request contexts in unittests. + +Version 0.7.3 +------------- + +Bugfix release, release date to be decided + +- Fixed the Jinja2 environment's list_templates method not returning the + correct names when blueprints or modules were involved. + +Version 0.7.2 +------------- + +Bugfix release, released on July 6th 2011 + +- Fixed an issue with URL processors not properly working on + blueprints. + +Version 0.7.1 +------------- + +Bugfix release, released on June 29th 2011 + +- Added missing future import that broke 2.5 compatibility. +- Fixed an infinite redirect issue with blueprints. + +Version 0.7 +----------- + +Released on June 28th 2011, codename Grappa + +- Added :meth:`~flask.Flask.make_default_options_response` + which can be used by subclasses to alter the default + behaviour for `OPTIONS` responses. +- Unbound locals now raise a proper :exc:`RuntimeError` instead + of an :exc:`AttributeError`. +- Mimetype guessing and etag support based on file objects is now + deprecated for :func:`flask.send_file` because it was unreliable. + Pass filenames instead or attach your own etags and provide a + proper mimetype by hand. +- Static file handling for modules now requires the name of the + static folder to be supplied explicitly. The previous autodetection + was not reliable and caused issues on Google's App Engine. Until + 1.0 the old behaviour will continue to work but issue dependency + warnings. +- fixed a problem for Flask to run on jython. +- added a `PROPAGATE_EXCEPTIONS` configuration variable that can be + used to flip the setting of exception propagation which previously + was linked to `DEBUG` alone and is now linked to either `DEBUG` or + `TESTING`. +- Flask no longer internally depends on rules being added through the + `add_url_rule` function and can now also accept regular werkzeug + rules added to the url map. +- Added an `endpoint` method to the flask application object which + allows one to register a callback to an arbitrary endpoint with + a decorator. +- Use Last-Modified for static file sending instead of Date which + was incorrectly introduced in 0.6. +- Added `create_jinja_loader` to override the loader creation process. +- Implemented a silent flag for `config.from_pyfile`. +- Added `teardown_request` decorator, for functions that should run at the end + of a request regardless of whether an exception occurred. Also the behavior + for `after_request` was changed. It's now no longer executed when an exception + is raised. See :ref:`upgrading-to-new-teardown-handling` +- Implemented :func:`flask.has_request_context` +- Deprecated `init_jinja_globals`. Override the + :meth:`~flask.Flask.create_jinja_environment` method instead to + achieve the same functionality. +- Added :func:`flask.safe_join` +- The automatic JSON request data unpacking now looks at the charset + mimetype parameter. +- Don't modify the session on :func:`flask.get_flashed_messages` if there + are no messages in the session. +- `before_request` handlers are now able to abort requests with errors. +- it is not possible to define user exception handlers. That way you can + provide custom error messages from a central hub for certain errors that + might occur during request processing (for instance database connection + errors, timeouts from remote resources etc.). +- Blueprints can provide blueprint specific error handlers. +- Implemented generic :ref:`views` (class based views). + +Version 0.6.1 +------------- + +Bugfix release, released on December 31st 2010 + +- Fixed an issue where the default `OPTIONS` response was + not exposing all valid methods in the `Allow` header. +- Jinja2 template loading syntax now allows "./" in front of + a template load path. Previously this caused issues with + module setups. +- Fixed an issue where the subdomain setting for modules was + ignored for the static folder. +- Fixed a security problem that allowed clients to download arbitrary files + if the host server was a windows based operating system and the client + uses backslashes to escape the directory the files where exposed from. + +Version 0.6 +----------- + +Released on July 27th 2010, codename Whisky + +- after request functions are now called in reverse order of + registration. +- OPTIONS is now automatically implemented by Flask unless the + application explicitly adds 'OPTIONS' as method to the URL rule. + In this case no automatic OPTIONS handling kicks in. +- static rules are now even in place if there is no static folder + for the module. This was implemented to aid GAE which will + remove the static folder if it's part of a mapping in the .yml + file. +- the :attr:`~flask.Flask.config` is now available in the templates + as `config`. +- context processors will no longer override values passed directly + to the render function. +- added the ability to limit the incoming request data with the + new ``MAX_CONTENT_LENGTH`` configuration value. +- the endpoint for the :meth:`flask.Module.add_url_rule` method + is now optional to be consistent with the function of the + same name on the application object. +- added a :func:`flask.make_response` function that simplifies + creating response object instances in views. +- added signalling support based on blinker. This feature is currently + optional and supposed to be used by extensions and applications. If + you want to use it, make sure to have `blinker`_ installed. +- refactored the way URL adapters are created. This process is now + fully customizable with the :meth:`~flask.Flask.create_url_adapter` + method. +- modules can now register for a subdomain instead of just an URL + prefix. This makes it possible to bind a whole module to a + configurable subdomain. + +.. _blinker: http://pypi.python.org/pypi/blinker + +Version 0.5.2 +------------- + +Bugfix Release, released on July 15th 2010 + +- fixed another issue with loading templates from directories when + modules were used. + +Version 0.5.1 +------------- + +Bugfix Release, released on July 6th 2010 + +- fixes an issue with template loading from directories when modules + where used. + +Version 0.5 +----------- + +Released on July 6th 2010, codename Calvados + +- fixed a bug with subdomains that was caused by the inability to + specify the server name. The server name can now be set with + the `SERVER_NAME` config key. This key is now also used to set + the session cookie cross-subdomain wide. +- autoescaping is no longer active for all templates. Instead it + is only active for ``.html``, ``.htm``, ``.xml`` and ``.xhtml``. + Inside templates this behaviour can be changed with the + ``autoescape`` tag. +- refactored Flask internally. It now consists of more than a + single file. +- :func:`flask.send_file` now emits etags and has the ability to + do conditional responses builtin. +- (temporarily) dropped support for zipped applications. This was a + rarely used feature and led to some confusing behaviour. +- added support for per-package template and static-file directories. +- removed support for `create_jinja_loader` which is no longer used + in 0.5 due to the improved module support. +- added a helper function to expose files from any directory. + +Version 0.4 +----------- + +Released on June 18th 2010, codename Rakia + +- added the ability to register application wide error handlers + from modules. +- :meth:`~flask.Flask.after_request` handlers are now also invoked + if the request dies with an exception and an error handling page + kicks in. +- test client has not the ability to preserve the request context + for a little longer. This can also be used to trigger custom + requests that do not pop the request stack for testing. +- because the Python standard library caches loggers, the name of + the logger is configurable now to better support unittests. +- added `TESTING` switch that can activate unittesting helpers. +- the logger switches to `DEBUG` mode now if debug is enabled. + +Version 0.3.1 +------------- + +Bugfix release, released on May 28th 2010 + +- fixed a error reporting bug with :meth:`flask.Config.from_envvar` +- removed some unused code from flask +- release does no longer include development leftover files (.git + folder for themes, built documentation in zip and pdf file and + some .pyc files) + +Version 0.3 +----------- + +Released on May 28th 2010, codename Schnaps + +- added support for categories for flashed messages. +- the application now configures a :class:`logging.Handler` and will + log request handling exceptions to that logger when not in debug + mode. This makes it possible to receive mails on server errors + for example. +- added support for context binding that does not require the use of + the with statement for playing in the console. +- the request context is now available within the with statement making + it possible to further push the request context or pop it. +- added support for configurations. + +Version 0.2 +----------- + +Released on May 12th 2010, codename Jägermeister + +- various bugfixes +- integrated JSON support +- added :func:`~flask.get_template_attribute` helper function. +- :meth:`~flask.Flask.add_url_rule` can now also register a + view function. +- refactored internal request dispatching. +- server listens on 127.0.0.1 by default now to fix issues with chrome. +- added external URL support. +- added support for :func:`~flask.send_file` +- module support and internal request handling refactoring + to better support pluggable applications. +- sessions can be set to be permanent now on a per-session basis. +- better error reporting on missing secret keys. +- added support for Google Appengine. + +Version 0.1 +----------- + +First public preview release. diff --git a/CHANGES.rst b/CHANGES.rst deleted file mode 100644 index 232b144a..00000000 --- a/CHANGES.rst +++ /dev/null @@ -1,1665 +0,0 @@ -Version 3.2.0 -------------- - -Unreleased - -- Drop support for Python 3.9. :pr:`5730` -- Remove previously deprecated code: ``__version__``. :pr:`5648` -- ``RequestContext`` has merged with ``AppContext``. ``RequestContext`` is now - a deprecated alias. If an app context is already pushed, it is not reused - when dispatching a request. This greatly simplifies the internal code for tracking - the active context. :issue:`5639` -- Many ``Flask`` methods involved in request dispatch now take the current - ``AppContext`` as the first parameter, instead of using the proxy objects. - If subclasses were overriding these methods, the old signature is detected, - shows a deprecation warning, and will continue to work during the - deprecation period. :issue:`5815` -- All teardown callbacks are called, even if any raise an error. :pr:`5928` -- The ``should_ignore_error`` is deprecated. Handle errors as needed in - teardown handlers instead. :issue:`5816` -- ``template_filter``, ``template_test``, and ``template_global`` decorators - can be used without parentheses. :issue:`5729` -- ``redirect`` returns a ``303`` status code by default instead of ``302``. - This tells the client to always switch to ``GET``, rather than only - switching ``POST`` to ``GET``. This preserves the current behavior of - ``GET`` and ``POST`` redirects, and is also correct for frontend libraries - such as HTMX. :issue:`5895` -- ``provide_automatic_options=True`` can be used to enable it for a view when - it's disabled in config. Previously, only disabling worked. :issue:`5916` -- ``Flask.select_jinja_autoescape`` uses case-insensitive comparison instead - of only lower case file extensions. :pr:`6012` - - -Version 3.1.3 -------------- - -Released 2026-02-18 - -- The session is marked as accessed for operations that only access the keys - but not the values, such as ``in`` and ``len``. :ghsa:`68rp-wp8r-4726` - - -Version 3.1.2 -------------- - -Released 2025-08-19 - -- ``stream_with_context`` does not fail inside async views. :issue:`5774` -- When using ``follow_redirects`` in the test client, the final state - of ``session`` is correct. :issue:`5786` -- Relax type hint for passing bytes IO to ``send_file``. :issue:`5776` - - -Version 3.1.1 -------------- - -Released 2025-05-13 - -- Fix signing key selection order when key rotation is enabled via - ``SECRET_KEY_FALLBACKS``. :ghsa:`4grg-w6v8-c28g` -- Fix type hint for ``cli_runner.invoke``. :issue:`5645` -- ``flask --help`` loads the app and plugins first to make sure all commands - are shown. :issue:`5673` -- Mark sans-io base class as being able to handle views that return - ``AsyncIterable``. This is not accurate for Flask, but makes typing easier - for Quart. :pr:`5659` - - -Version 3.1.0 -------------- - -Released 2024-11-13 - -- Drop support for Python 3.8. :pr:`5623` -- Update minimum dependency versions to latest feature releases. - Werkzeug >= 3.1, ItsDangerous >= 2.2, Blinker >= 1.9. :pr:`5624,5633` -- Provide a configuration option to control automatic option - responses. :pr:`5496` -- ``Flask.open_resource``/``open_instance_resource`` and - ``Blueprint.open_resource`` take an ``encoding`` parameter to use when - opening in text mode. It defaults to ``utf-8``. :issue:`5504` -- ``Request.max_content_length`` can be customized per-request instead of only - through the ``MAX_CONTENT_LENGTH`` config. Added - ``MAX_FORM_MEMORY_SIZE`` and ``MAX_FORM_PARTS`` config. Added documentation - about resource limits to the security page. :issue:`5625` -- Add support for the ``Partitioned`` cookie attribute (CHIPS), with the - ``SESSION_COOKIE_PARTITIONED`` config. :issue:`5472` -- ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files. - ``load_dotenv`` loads default files in addition to a path unless - ``load_defaults=False`` is passed. :issue:`5628` -- Support key rotation with the ``SECRET_KEY_FALLBACKS`` config, a list of old - secret keys that can still be used for unsigning. Extensions will need to - add support. :issue:`5621` -- Fix how setting ``host_matching=True`` or ``subdomain_matching=False`` - interacts with ``SERVER_NAME``. Setting ``SERVER_NAME`` no longer restricts - requests to only that domain. :issue:`5553` -- ``Request.trusted_hosts`` is checked during routing, and can be set through - the ``TRUSTED_HOSTS`` config. :issue:`5636` - - -Version 3.0.3 -------------- - -Released 2024-04-07 - -- The default ``hashlib.sha1`` may not be available in FIPS builds. Don't - access it at import time so the developer has time to change the default. - :issue:`5448` -- Don't initialize the ``cli`` attribute in the sansio scaffold, but rather in - the ``Flask`` concrete class. :pr:`5270` - - -Version 3.0.2 -------------- - -Released 2024-02-03 - -- Correct type for ``jinja_loader`` property. :issue:`5388` -- Fix error with ``--extra-files`` and ``--exclude-patterns`` CLI options. - :issue:`5391` - - -Version 3.0.1 -------------- - -Released 2024-01-18 - -- Correct type for ``path`` argument to ``send_file``. :issue:`5336` -- Fix a typo in an error message for the ``flask run --key`` option. :pr:`5344` -- Session data is untagged without relying on the built-in ``json.loads`` - ``object_hook``. This allows other JSON providers that don't implement that. - :issue:`5381` -- Address more type findings when using mypy strict mode. :pr:`5383` - - -Version 3.0.0 -------------- - -Released 2023-09-30 - -- Remove previously deprecated code. :pr:`5223` -- Deprecate the ``__version__`` attribute. Use feature detection, or - ``importlib.metadata.version("flask")``, instead. :issue:`5230` -- Restructure the code such that the Flask (app) and Blueprint - classes have Sans-IO bases. :pr:`5127` -- Allow self as an argument to url_for. :pr:`5264` -- Require Werkzeug >= 3.0.0. - - -Version 2.3.3 -------------- - -Released 2023-08-21 - -- Python 3.12 compatibility. -- Require Werkzeug >= 2.3.7. -- Use ``flit_core`` instead of ``setuptools`` as build backend. -- Refactor how an app's root and instance paths are determined. :issue:`5160` - - -Version 2.3.2 -------------- - -Released 2023-05-01 - -- Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed. -- Update Werkzeug requirement to >=2.3.3 to apply recent bug fixes. - :ghsa:`m2qf-hxjv-5gpq` - - -Version 2.3.1 -------------- - -Released 2023-04-25 - -- Restore deprecated ``from flask import Markup``. :issue:`5084` - - -Version 2.3.0 -------------- - -Released 2023-04-25 - -- Drop support for Python 3.7. :pr:`5072` -- Update minimum requirements to the latest versions: Werkzeug>=2.3.0, Jinja2>3.1.2, - itsdangerous>=2.1.2, click>=8.1.3. -- Remove previously deprecated code. :pr:`4995` - - - The ``push`` and ``pop`` methods of the deprecated ``_app_ctx_stack`` and - ``_request_ctx_stack`` objects are removed. ``top`` still exists to give - extensions more time to update, but it will be removed. - - The ``FLASK_ENV`` environment variable, ``ENV`` config key, and ``app.env`` - property are removed. - - The ``session_cookie_name``, ``send_file_max_age_default``, ``use_x_sendfile``, - ``propagate_exceptions``, and ``templates_auto_reload`` properties on ``app`` - are removed. - - The ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_MIMETYPE``, and - ``JSONIFY_PRETTYPRINT_REGULAR`` config keys are removed. - - The ``app.before_first_request`` and ``bp.before_app_first_request`` decorators - are removed. - - ``json_encoder`` and ``json_decoder`` attributes on app and blueprint, and the - corresponding ``json.JSONEncoder`` and ``JSONDecoder`` classes, are removed. - - The ``json.htmlsafe_dumps`` and ``htmlsafe_dump`` functions are removed. - - Calling setup methods on blueprints after registration is an error instead of a - warning. :pr:`4997` - -- Importing ``escape`` and ``Markup`` from ``flask`` is deprecated. Import them - directly from ``markupsafe`` instead. :pr:`4996` -- The ``app.got_first_request`` property is deprecated. :pr:`4997` -- The ``locked_cached_property`` decorator is deprecated. Use a lock inside the - decorated function if locking is needed. :issue:`4993` -- Signals are always available. ``blinker>=1.6.2`` is a required dependency. The - ``signals_available`` attribute is deprecated. :issue:`5056` -- Signals support ``async`` subscriber functions. :pr:`5049` -- Remove uses of locks that could cause requests to block each other very briefly. - :issue:`4993` -- Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``. - :pr:`4947` -- Ensure subdomains are applied with nested blueprints. :issue:`4834` -- ``config.from_file`` can use ``text=False`` to indicate that the parser wants a - binary file instead. :issue:`4989` -- If a blueprint is created with an empty name it raises a ``ValueError``. - :issue:`5010` -- ``SESSION_COOKIE_DOMAIN`` does not fall back to ``SERVER_NAME``. The default is not - to set the domain, which modern browsers interpret as an exact match rather than - a subdomain match. Warnings about ``localhost`` and IP addresses are also removed. - :issue:`5051` -- The ``routes`` command shows each rule's ``subdomain`` or ``host`` when domain - matching is in use. :issue:`5004` -- Use postponed evaluation of annotations. :pr:`5071` - - -Version 2.2.5 -------------- - -Released 2023-05-02 - -- Update for compatibility with Werkzeug 2.3.3. -- Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed. - - -Version 2.2.4 -------------- - -Released 2023-04-25 - -- Update for compatibility with Werkzeug 2.3. - - -Version 2.2.3 -------------- - -Released 2023-02-15 - -- Autoescape is enabled by default for ``.svg`` template files. :issue:`4831` -- Fix the type of ``template_folder`` to accept ``pathlib.Path``. :issue:`4892` -- Add ``--debug`` option to the ``flask run`` command. :issue:`4777` - - -Version 2.2.2 -------------- - -Released 2022-08-08 - -- Update Werkzeug dependency to >= 2.2.2. This includes fixes related - to the new faster router, header parsing, and the development - server. :pr:`4754` -- Fix the default value for ``app.env`` to be ``"production"``. This - attribute remains deprecated. :issue:`4740` - - -Version 2.2.1 -------------- - -Released 2022-08-03 - -- Setting or accessing ``json_encoder`` or ``json_decoder`` raises a - deprecation warning. :issue:`4732` - - -Version 2.2.0 -------------- - -Released 2022-08-01 - -- Remove previously deprecated code. :pr:`4667` - - - Old names for some ``send_file`` parameters have been removed. - ``download_name`` replaces ``attachment_filename``, ``max_age`` - replaces ``cache_timeout``, and ``etag`` replaces ``add_etags``. - Additionally, ``path`` replaces ``filename`` in - ``send_from_directory``. - - The ``RequestContext.g`` property returning ``AppContext.g`` is - removed. - -- Update Werkzeug dependency to >= 2.2. -- The app and request contexts are managed using Python context vars - directly rather than Werkzeug's ``LocalStack``. This should result - in better performance and memory use. :pr:`4682` - - - Extension maintainers, be aware that ``_app_ctx_stack.top`` - and ``_request_ctx_stack.top`` are deprecated. Store data on - ``g`` instead using a unique prefix, like - ``g._extension_name_attr``. - -- The ``FLASK_ENV`` environment variable and ``app.env`` attribute are - deprecated, removing the distinction between development and debug - mode. Debug mode should be controlled directly using the ``--debug`` - option or ``app.run(debug=True)``. :issue:`4714` -- Some attributes that proxied config keys on ``app`` are deprecated: - ``session_cookie_name``, ``send_file_max_age_default``, - ``use_x_sendfile``, ``propagate_exceptions``, and - ``templates_auto_reload``. Use the relevant config keys instead. - :issue:`4716` -- Add new customization points to the ``Flask`` app object for many - previously global behaviors. - - - ``flask.url_for`` will call ``app.url_for``. :issue:`4568` - - ``flask.abort`` will call ``app.aborter``. - ``Flask.aborter_class`` and ``Flask.make_aborter`` can be used - to customize this aborter. :issue:`4567` - - ``flask.redirect`` will call ``app.redirect``. :issue:`4569` - - ``flask.json`` is an instance of ``JSONProvider``. A different - provider can be set to use a different JSON library. - ``flask.jsonify`` will call ``app.json.response``, other - functions in ``flask.json`` will call corresponding functions in - ``app.json``. :pr:`4692` - -- JSON configuration is moved to attributes on the default - ``app.json`` provider. ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, - ``JSONIFY_MIMETYPE``, and ``JSONIFY_PRETTYPRINT_REGULAR`` are - deprecated. :pr:`4692` -- Setting custom ``json_encoder`` and ``json_decoder`` classes on the - app or a blueprint, and the corresponding ``json.JSONEncoder`` and - ``JSONDecoder`` classes, are deprecated. JSON behavior can now be - overridden using the ``app.json`` provider interface. :pr:`4692` -- ``json.htmlsafe_dumps`` and ``json.htmlsafe_dump`` are deprecated, - the function is built-in to Jinja now. :pr:`4692` -- Refactor ``register_error_handler`` to consolidate error checking. - Rewrite some error messages to be more consistent. :issue:`4559` -- Use Blueprint decorators and functions intended for setup after - registering the blueprint will show a warning. In the next version, - this will become an error just like the application setup methods. - :issue:`4571` -- ``before_first_request`` is deprecated. Run setup code when creating - the application instead. :issue:`4605` -- Added the ``View.init_every_request`` class attribute. If a view - subclass sets this to ``False``, the view will not create a new - instance on every request. :issue:`2520`. -- A ``flask.cli.FlaskGroup`` Click group can be nested as a - sub-command in a custom CLI. :issue:`3263` -- Add ``--app`` and ``--debug`` options to the ``flask`` CLI, instead - of requiring that they are set through environment variables. - :issue:`2836` -- Add ``--env-file`` option to the ``flask`` CLI. This allows - specifying a dotenv file to load in addition to ``.env`` and - ``.flaskenv``. :issue:`3108` -- It is no longer required to decorate custom CLI commands on - ``app.cli`` or ``blueprint.cli`` with ``@with_appcontext``, an app - context will already be active at that point. :issue:`2410` -- ``SessionInterface.get_expiration_time`` uses a timezone-aware - value. :pr:`4645` -- View functions can return generators directly instead of wrapping - them in a ``Response``. :pr:`4629` -- Add ``stream_template`` and ``stream_template_string`` functions to - render a template as a stream of pieces. :pr:`4629` -- A new implementation of context preservation during debugging and - testing. :pr:`4666` - - - ``request``, ``g``, and other context-locals point to the - correct data when running code in the interactive debugger - console. :issue:`2836` - - Teardown functions are always run at the end of the request, - even if the context is preserved. They are also run after the - preserved context is popped. - - ``stream_with_context`` preserves context separately from a - ``with client`` block. It will be cleaned up when - ``response.get_data()`` or ``response.close()`` is called. - -- Allow returning a list from a view function, to convert it to a - JSON response like a dict is. :issue:`4672` -- When type checking, allow ``TypedDict`` to be returned from view - functions. :pr:`4695` -- Remove the ``--eager-loading/--lazy-loading`` options from the - ``flask run`` command. The app is always eager loaded the first - time, then lazily loaded in the reloader. The reloader always prints - errors immediately but continues serving. Remove the internal - ``DispatchingApp`` middleware used by the previous implementation. - :issue:`4715` - - -Version 2.1.3 -------------- - -Released 2022-07-13 - -- Inline some optional imports that are only used for certain CLI - commands. :pr:`4606` -- Relax type annotation for ``after_request`` functions. :issue:`4600` -- ``instance_path`` for namespace packages uses the path closest to - the imported submodule. :issue:`4610` -- Clearer error message when ``render_template`` and - ``render_template_string`` are used outside an application context. - :pr:`4693` - - -Version 2.1.2 -------------- - -Released 2022-04-28 - -- Fix type annotation for ``json.loads``, it accepts str or bytes. - :issue:`4519` -- The ``--cert`` and ``--key`` options on ``flask run`` can be given - in either order. :issue:`4459` - - -Version 2.1.1 -------------- - -Released on 2022-03-30 - -- Set the minimum required version of importlib_metadata to 3.6.0, - which is required on Python < 3.10. :issue:`4502` - - -Version 2.1.0 -------------- - -Released 2022-03-28 - -- Drop support for Python 3.6. :pr:`4335` -- Update Click dependency to >= 8.0. :pr:`4008` -- Remove previously deprecated code. :pr:`4337` - - - The CLI does not pass ``script_info`` to app factory functions. - - ``config.from_json`` is replaced by - ``config.from_file(name, load=json.load)``. - - ``json`` functions no longer take an ``encoding`` parameter. - - ``safe_join`` is removed, use ``werkzeug.utils.safe_join`` - instead. - - ``total_seconds`` is removed, use ``timedelta.total_seconds`` - instead. - - The same blueprint cannot be registered with the same name. Use - ``name=`` when registering to specify a unique name. - - The test client's ``as_tuple`` parameter is removed. Use - ``response.request.environ`` instead. :pr:`4417` - -- Some parameters in ``send_file`` and ``send_from_directory`` were - renamed in 2.0. The deprecation period for the old names is extended - to 2.2. Be sure to test with deprecation warnings visible. - - - ``attachment_filename`` is renamed to ``download_name``. - - ``cache_timeout`` is renamed to ``max_age``. - - ``add_etags`` is renamed to ``etag``. - - ``filename`` is renamed to ``path``. - -- The ``RequestContext.g`` property is deprecated. Use ``g`` directly - or ``AppContext.g`` instead. :issue:`3898` -- ``copy_current_request_context`` can decorate async functions. - :pr:`4303` -- The CLI uses ``importlib.metadata`` instead of ``pkg_resources`` to - load command entry points. :issue:`4419` -- Overriding ``FlaskClient.open`` will not cause an error on redirect. - :issue:`3396` -- Add an ``--exclude-patterns`` option to the ``flask run`` CLI - command to specify patterns that will be ignored by the reloader. - :issue:`4188` -- When using lazy loading (the default with the debugger), the Click - context from the ``flask run`` command remains available in the - loader thread. :issue:`4460` -- Deleting the session cookie uses the ``httponly`` flag. - :issue:`4485` -- Relax typing for ``errorhandler`` to allow the user to use more - precise types and decorate the same function multiple times. - :issue:`4095, 4295, 4297` -- Fix typing for ``__exit__`` methods for better compatibility with - ``ExitStack``. :issue:`4474` -- From Werkzeug, for redirect responses the ``Location`` header URL - will remain relative, and exclude the scheme and domain, by default. - :pr:`4496` -- Add ``Config.from_prefixed_env()`` to load config values from - environment variables that start with ``FLASK_`` or another prefix. - This parses values as JSON by default, and allows setting keys in - nested dicts. :pr:`4479` - - -Version 2.0.3 -------------- - -Released 2022-02-14 - -- The test client's ``as_tuple`` parameter is deprecated and will be - removed in Werkzeug 2.1. It is now also deprecated in Flask, to be - removed in Flask 2.1, while remaining compatible with both in - 2.0.x. Use ``response.request.environ`` instead. :pr:`4341` -- Fix type annotation for ``errorhandler`` decorator. :issue:`4295` -- Revert a change to the CLI that caused it to hide ``ImportError`` - tracebacks when importing the application. :issue:`4307` -- ``app.json_encoder`` and ``json_decoder`` are only passed to - ``dumps`` and ``loads`` if they have custom behavior. This improves - performance, mainly on PyPy. :issue:`4349` -- Clearer error message when ``after_this_request`` is used outside a - request context. :issue:`4333` - - -Version 2.0.2 -------------- - -Released 2021-10-04 - -- Fix type annotation for ``teardown_*`` methods. :issue:`4093` -- Fix type annotation for ``before_request`` and ``before_app_request`` - decorators. :issue:`4104` -- Fixed the issue where typing requires template global - decorators to accept functions with no arguments. :issue:`4098` -- Support View and MethodView instances with async handlers. :issue:`4112` -- Enhance typing of ``app.errorhandler`` decorator. :issue:`4095` -- Fix registering a blueprint twice with differing names. :issue:`4124` -- Fix the type of ``static_folder`` to accept ``pathlib.Path``. - :issue:`4150` -- ``jsonify`` handles ``decimal.Decimal`` by encoding to ``str``. - :issue:`4157` -- Correctly handle raising deferred errors in CLI lazy loading. - :issue:`4096` -- The CLI loader handles ``**kwargs`` in a ``create_app`` function. - :issue:`4170` -- Fix the order of ``before_request`` and other callbacks that trigger - before the view returns. They are called from the app down to the - closest nested blueprint. :issue:`4229` - - -Version 2.0.1 -------------- - -Released 2021-05-21 - -- Re-add the ``filename`` parameter in ``send_from_directory``. The - ``filename`` parameter has been renamed to ``path``, the old name - is deprecated. :pr:`4019` -- Mark top-level names as exported so type checking understands - imports in user projects. :issue:`4024` -- Fix type annotation for ``g`` and inform mypy that it is a namespace - object that has arbitrary attributes. :issue:`4020` -- Fix some types that weren't available in Python 3.6.0. :issue:`4040` -- Improve typing for ``send_file``, ``send_from_directory``, and - ``get_send_file_max_age``. :issue:`4044`, :pr:`4026` -- Show an error when a blueprint name contains a dot. The ``.`` has - special meaning, it is used to separate (nested) blueprint names and - the endpoint name. :issue:`4041` -- Combine URL prefixes when nesting blueprints that were created with - a ``url_prefix`` value. :issue:`4037` -- Revert a change to the order that URL matching was done. The - URL is again matched after the session is loaded, so the session is - available in custom URL converters. :issue:`4053` -- Re-add deprecated ``Config.from_json``, which was accidentally - removed early. :issue:`4078` -- Improve typing for some functions using ``Callable`` in their type - signatures, focusing on decorator factories. :issue:`4060` -- Nested blueprints are registered with their dotted name. This allows - different blueprints with the same name to be nested at different - locations. :issue:`4069` -- ``register_blueprint`` takes a ``name`` option to change the - (pre-dotted) name the blueprint is registered with. This allows the - same blueprint to be registered multiple times with unique names for - ``url_for``. Registering the same blueprint with the same name - multiple times is deprecated. :issue:`1091` -- Improve typing for ``stream_with_context``. :issue:`4052` - - -Version 2.0.0 -------------- - -Released 2021-05-11 - -- Drop support for Python 2 and 3.5. -- Bump minimum versions of other Pallets projects: Werkzeug >= 2, - Jinja2 >= 3, MarkupSafe >= 2, ItsDangerous >= 2, Click >= 8. Be sure - to check the change logs for each project. For better compatibility - with other applications (e.g. Celery) that still require Click 7, - there is no hard dependency on Click 8 yet, but using Click 7 will - trigger a DeprecationWarning and Flask 2.1 will depend on Click 8. -- JSON support no longer uses simplejson. To use another JSON module, - override ``app.json_encoder`` and ``json_decoder``. :issue:`3555` -- The ``encoding`` option to JSON functions is deprecated. :pr:`3562` -- Passing ``script_info`` to app factory functions is deprecated. This - was not portable outside the ``flask`` command. Use - ``click.get_current_context().obj`` if it's needed. :issue:`3552` -- The CLI shows better error messages when the app failed to load - when looking up commands. :issue:`2741` -- Add ``SessionInterface.get_cookie_name`` to allow setting the - session cookie name dynamically. :pr:`3369` -- Add ``Config.from_file`` to load config using arbitrary file - loaders, such as ``toml.load`` or ``json.load``. - ``Config.from_json`` is deprecated in favor of this. :pr:`3398` -- The ``flask run`` command will only defer errors on reload. Errors - present during the initial call will cause the server to exit with - the traceback immediately. :issue:`3431` -- ``send_file`` raises a ``ValueError`` when passed an ``io`` object - in text mode. Previously, it would respond with 200 OK and an empty - file. :issue:`3358` -- When using ad-hoc certificates, check for the cryptography library - instead of PyOpenSSL. :pr:`3492` -- When specifying a factory function with ``FLASK_APP``, keyword - argument can be passed. :issue:`3553` -- When loading a ``.env`` or ``.flaskenv`` file, the current working - directory is no longer changed to the location of the file. - :pr:`3560` -- When returning a ``(response, headers)`` tuple from a view, the - headers replace rather than extend existing headers on the response. - For example, this allows setting the ``Content-Type`` for - ``jsonify()``. Use ``response.headers.extend()`` if extending is - desired. :issue:`3628` -- The ``Scaffold`` class provides a common API for the ``Flask`` and - ``Blueprint`` classes. ``Blueprint`` information is stored in - attributes just like ``Flask``, rather than opaque lambda functions. - This is intended to improve consistency and maintainability. - :issue:`3215` -- Include ``samesite`` and ``secure`` options when removing the - session cookie. :pr:`3726` -- Support passing a ``pathlib.Path`` to ``static_folder``. :pr:`3579` -- ``send_file`` and ``send_from_directory`` are wrappers around the - implementations in ``werkzeug.utils``. :pr:`3828` -- Some ``send_file`` parameters have been renamed, the old names are - deprecated. ``attachment_filename`` is renamed to ``download_name``. - ``cache_timeout`` is renamed to ``max_age``. ``add_etags`` is - renamed to ``etag``. :pr:`3828, 3883` -- ``send_file`` passes ``download_name`` even if - ``as_attachment=False`` by using ``Content-Disposition: inline``. - :pr:`3828` -- ``send_file`` sets ``conditional=True`` and ``max_age=None`` by - default. ``Cache-Control`` is set to ``no-cache`` if ``max_age`` is - not set, otherwise ``public``. This tells browsers to validate - conditional requests instead of using a timed cache. :pr:`3828` -- ``helpers.safe_join`` is deprecated. Use - ``werkzeug.utils.safe_join`` instead. :pr:`3828` -- The request context does route matching before opening the session. - This could allow a session interface to change behavior based on - ``request.endpoint``. :issue:`3776` -- Use Jinja's implementation of the ``|tojson`` filter. :issue:`3881` -- Add route decorators for common HTTP methods. For example, - ``@app.post("/login")`` is a shortcut for - ``@app.route("/login", methods=["POST"])``. :pr:`3907` -- Support async views, error handlers, before and after request, and - teardown functions. :pr:`3412` -- Support nesting blueprints. :issue:`593, 1548`, :pr:`3923` -- Set the default encoding to "UTF-8" when loading ``.env`` and - ``.flaskenv`` files to allow to use non-ASCII characters. :issue:`3931` -- ``flask shell`` sets up tab and history completion like the default - ``python`` shell if ``readline`` is installed. :issue:`3941` -- ``helpers.total_seconds()`` is deprecated. Use - ``timedelta.total_seconds()`` instead. :pr:`3962` -- Add type hinting. :pr:`3973`. - - -Version 1.1.4 -------------- - -Released 2021-05-13 - -- Update ``static_folder`` to use ``_compat.fspath`` instead of - ``os.fspath`` to continue supporting Python < 3.6 :issue:`4050` - - -Version 1.1.3 -------------- - -Released 2021-05-13 - -- Set maximum versions of Werkzeug, Jinja, Click, and ItsDangerous. - :issue:`4043` -- Re-add support for passing a ``pathlib.Path`` for ``static_folder``. - :pr:`3579` - - -Version 1.1.2 -------------- - -Released 2020-04-03 - -- Work around an issue when running the ``flask`` command with an - external debugger on Windows. :issue:`3297` -- The static route will not catch all URLs if the ``Flask`` - ``static_folder`` argument ends with a slash. :issue:`3452` - - -Version 1.1.1 -------------- - -Released 2019-07-08 - -- The ``flask.json_available`` flag was added back for compatibility - with some extensions. It will raise a deprecation warning when used, - and will be removed in version 2.0.0. :issue:`3288` - - -Version 1.1.0 -------------- - -Released 2019-07-04 - -- Bump minimum Werkzeug version to >= 0.15. -- Drop support for Python 3.4. -- Error handlers for ``InternalServerError`` or ``500`` will always be - passed an instance of ``InternalServerError``. If they are invoked - due to an unhandled exception, that original exception is now - available as ``e.original_exception`` rather than being passed - directly to the handler. The same is true if the handler is for the - base ``HTTPException``. This makes error handler behavior more - consistent. :pr:`3266` - - - ``Flask.finalize_request`` is called for all unhandled - exceptions even if there is no ``500`` error handler. - -- ``Flask.logger`` takes the same name as ``Flask.name`` (the value - passed as ``Flask(import_name)``. This reverts 1.0's behavior of - always logging to ``"flask.app"``, in order to support multiple apps - in the same process. A warning will be shown if old configuration is - detected that needs to be moved. :issue:`2866` -- ``RequestContext.copy`` includes the current session object in the - request context copy. This prevents ``session`` pointing to an - out-of-date object. :issue:`2935` -- Using built-in RequestContext, unprintable Unicode characters in - Host header will result in a HTTP 400 response and not HTTP 500 as - previously. :pr:`2994` -- ``send_file`` supports ``PathLike`` objects as described in - :pep:`519`, to support ``pathlib`` in Python 3. :pr:`3059` -- ``send_file`` supports ``BytesIO`` partial content. - :issue:`2957` -- ``open_resource`` accepts the "rt" file mode. This still does the - same thing as "r". :issue:`3163` -- The ``MethodView.methods`` attribute set in a base class is used by - subclasses. :issue:`3138` -- ``Flask.jinja_options`` is a ``dict`` instead of an - ``ImmutableDict`` to allow easier configuration. Changes must still - be made before creating the environment. :pr:`3190` -- Flask's ``JSONMixin`` for the request and response wrappers was - moved into Werkzeug. Use Werkzeug's version with Flask-specific - support. This bumps the Werkzeug dependency to >= 0.15. - :issue:`3125` -- The ``flask`` command entry point is simplified to take advantage - of Werkzeug 0.15's better reloader support. This bumps the Werkzeug - dependency to >= 0.15. :issue:`3022` -- Support ``static_url_path`` that ends with a forward slash. - :issue:`3134` -- Support empty ``static_folder`` without requiring setting an empty - ``static_url_path`` as well. :pr:`3124` -- ``jsonify`` supports ``dataclass`` objects. :pr:`3195` -- Allow customizing the ``Flask.url_map_class`` used for routing. - :pr:`3069` -- The development server port can be set to 0, which tells the OS to - pick an available port. :issue:`2926` -- The return value from ``cli.load_dotenv`` is more consistent with - the documentation. It will return ``False`` if python-dotenv is not - installed, or if the given path isn't a file. :issue:`2937` -- Signaling support has a stub for the ``connect_via`` method when - the Blinker library is not installed. :pr:`3208` -- Add an ``--extra-files`` option to the ``flask run`` CLI command to - specify extra files that will trigger the reloader on change. - :issue:`2897` -- Allow returning a dictionary from a view function. Similar to how - returning a string will produce a ``text/html`` response, returning - a dict will call ``jsonify`` to produce a ``application/json`` - response. :pr:`3111` -- Blueprints have a ``cli`` Click group like ``app.cli``. CLI commands - registered with a blueprint will be available as a group under the - ``flask`` command. :issue:`1357`. -- When using the test client as a context manager (``with client:``), - all preserved request contexts are popped when the block exits, - ensuring nested contexts are cleaned up correctly. :pr:`3157` -- Show a better error message when the view return type is not - supported. :issue:`3214` -- ``flask.testing.make_test_environ_builder()`` has been deprecated in - favour of a new class ``flask.testing.EnvironBuilder``. :pr:`3232` -- The ``flask run`` command no longer fails if Python is not built - with SSL support. Using the ``--cert`` option will show an - appropriate error message. :issue:`3211` -- URL matching now occurs after the request context is pushed, rather - than when it's created. This allows custom URL converters to access - the app and request contexts, such as to query a database for an id. - :issue:`3088` - - -Version 1.0.4 -------------- - -Released 2019-07-04 - -- The key information for ``BadRequestKeyError`` is no longer cleared - outside debug mode, so error handlers can still access it. This - requires upgrading to Werkzeug 0.15.5. :issue:`3249` -- ``send_file`` url quotes the ":" and "/" characters for more - compatible UTF-8 filename support in some browsers. :issue:`3074` -- Fixes for :pep:`451` import loaders and pytest 5.x. :issue:`3275` -- Show message about dotenv on stderr instead of stdout. :issue:`3285` - - -Version 1.0.3 -------------- - -Released 2019-05-17 - -- ``send_file`` encodes filenames as ASCII instead of Latin-1 - (ISO-8859-1). This fixes compatibility with Gunicorn, which is - stricter about header encodings than :pep:`3333`. :issue:`2766` -- Allow custom CLIs using ``FlaskGroup`` to set the debug flag without - it always being overwritten based on environment variables. - :pr:`2765` -- ``flask --version`` outputs Werkzeug's version and simplifies the - Python version. :pr:`2825` -- ``send_file`` handles an ``attachment_filename`` that is a native - Python 2 string (bytes) with UTF-8 coded bytes. :issue:`2933` -- A catch-all error handler registered for ``HTTPException`` will not - handle ``RoutingException``, which is used internally during - routing. This fixes the unexpected behavior that had been introduced - in 1.0. :pr:`2986` -- Passing the ``json`` argument to ``app.test_client`` does not - push/pop an extra app context. :issue:`2900` - - -Version 1.0.2 -------------- - -Released 2018-05-02 - -- Fix more backwards compatibility issues with merging slashes between - a blueprint prefix and route. :pr:`2748` -- Fix error with ``flask routes`` command when there are no routes. - :issue:`2751` - - -Version 1.0.1 -------------- - -Released 2018-04-29 - -- Fix registering partials (with no ``__name__``) as view functions. - :pr:`2730` -- Don't treat lists returned from view functions the same as tuples. - Only tuples are interpreted as response data. :issue:`2736` -- Extra slashes between a blueprint's ``url_prefix`` and a route URL - are merged. This fixes some backwards compatibility issues with the - change in 1.0. :issue:`2731`, :issue:`2742` -- Only trap ``BadRequestKeyError`` errors in debug mode, not all - ``BadRequest`` errors. This allows ``abort(400)`` to continue - working as expected. :issue:`2735` -- The ``FLASK_SKIP_DOTENV`` environment variable can be set to ``1`` - to skip automatically loading dotenv files. :issue:`2722` - - -Version 1.0 ------------ - -Released 2018-04-26 - -- Python 2.6 and 3.3 are no longer supported. -- Bump minimum dependency versions to the latest stable versions: - Werkzeug >= 0.14, Jinja >= 2.10, itsdangerous >= 0.24, Click >= 5.1. - :issue:`2586` -- Skip ``app.run`` when a Flask application is run from the command - line. This avoids some behavior that was confusing to debug. -- Change the default for ``JSONIFY_PRETTYPRINT_REGULAR`` to - ``False``. ``~json.jsonify`` returns a compact format by default, - and an indented format in debug mode. :pr:`2193` -- ``Flask.__init__`` accepts the ``host_matching`` argument and sets - it on ``Flask.url_map``. :issue:`1559` -- ``Flask.__init__`` accepts the ``static_host`` argument and passes - it as the ``host`` argument when defining the static route. - :issue:`1559` -- ``send_file`` supports Unicode in ``attachment_filename``. - :pr:`2223` -- Pass ``_scheme`` argument from ``url_for`` to - ``Flask.handle_url_build_error``. :pr:`2017` -- ``Flask.add_url_rule`` accepts the ``provide_automatic_options`` - argument to disable adding the ``OPTIONS`` method. :pr:`1489` -- ``MethodView`` subclasses inherit method handlers from base classes. - :pr:`1936` -- Errors caused while opening the session at the beginning of the - request are handled by the app's error handlers. :pr:`2254` -- Blueprints gained ``Blueprint.json_encoder`` and - ``Blueprint.json_decoder`` attributes to override the app's - encoder and decoder. :pr:`1898` -- ``Flask.make_response`` raises ``TypeError`` instead of - ``ValueError`` for bad response types. The error messages have been - improved to describe why the type is invalid. :pr:`2256` -- Add ``routes`` CLI command to output routes registered on the - application. :pr:`2259` -- Show warning when session cookie domain is a bare hostname or an IP - address, as these may not behave properly in some browsers, such as - Chrome. :pr:`2282` -- Allow IP address as exact session cookie domain. :pr:`2282` -- ``SESSION_COOKIE_DOMAIN`` is set if it is detected through - ``SERVER_NAME``. :pr:`2282` -- Auto-detect zero-argument app factory called ``create_app`` or - ``make_app`` from ``FLASK_APP``. :pr:`2297` -- Factory functions are not required to take a ``script_info`` - parameter to work with the ``flask`` command. If they take a single - parameter or a parameter named ``script_info``, the ``ScriptInfo`` - object will be passed. :pr:`2319` -- ``FLASK_APP`` can be set to an app factory, with arguments if - needed, for example ``FLASK_APP=myproject.app:create_app('dev')``. - :pr:`2326` -- ``FLASK_APP`` can point to local packages that are not installed in - editable mode, although ``pip install -e`` is still preferred. - :pr:`2414` -- The ``View`` class attribute - ``View.provide_automatic_options`` is set in ``View.as_view``, to be - detected by ``Flask.add_url_rule``. :pr:`2316` -- Error handling will try handlers registered for ``blueprint, code``, - ``app, code``, ``blueprint, exception``, ``app, exception``. - :pr:`2314` -- ``Cookie`` is added to the response's ``Vary`` header if the session - is accessed at all during the request (and not deleted). :pr:`2288` -- ``Flask.test_request_context`` accepts ``subdomain`` and - ``url_scheme`` arguments for use when building the base URL. - :pr:`1621` -- Set ``APPLICATION_ROOT`` to ``'/'`` by default. This was already the - implicit default when it was set to ``None``. -- ``TRAP_BAD_REQUEST_ERRORS`` is enabled by default in debug mode. - ``BadRequestKeyError`` has a message with the bad key in debug mode - instead of the generic bad request message. :pr:`2348` -- Allow registering new tags with ``TaggedJSONSerializer`` to support - storing other types in the session cookie. :pr:`2352` -- Only open the session if the request has not been pushed onto the - context stack yet. This allows ``stream_with_context`` generators to - access the same session that the containing view uses. :pr:`2354` -- Add ``json`` keyword argument for the test client request methods. - This will dump the given object as JSON and set the appropriate - content type. :pr:`2358` -- Extract JSON handling to a mixin applied to both the ``Request`` and - ``Response`` classes. This adds the ``Response.is_json`` and - ``Response.get_json`` methods to the response to make testing JSON - response much easier. :pr:`2358` -- Removed error handler caching because it caused unexpected results - for some exception inheritance hierarchies. Register handlers - explicitly for each exception if you want to avoid traversing the - MRO. :pr:`2362` -- Fix incorrect JSON encoding of aware, non-UTC datetimes. :pr:`2374` -- Template auto reloading will honor debug mode even if - ``Flask.jinja_env`` was already accessed. :pr:`2373` -- The following old deprecated code was removed. :issue:`2385` - - - ``flask.ext`` - import extensions directly by their name instead - of through the ``flask.ext`` namespace. For example, - ``import flask.ext.sqlalchemy`` becomes - ``import flask_sqlalchemy``. - - ``Flask.init_jinja_globals`` - extend - ``Flask.create_jinja_environment`` instead. - - ``Flask.error_handlers`` - tracked by - ``Flask.error_handler_spec``, use ``Flask.errorhandler`` - to register handlers. - - ``Flask.request_globals_class`` - use - ``Flask.app_ctx_globals_class`` instead. - - ``Flask.static_path`` - use ``Flask.static_url_path`` instead. - - ``Request.module`` - use ``Request.blueprint`` instead. - -- The ``Request.json`` property is no longer deprecated. :issue:`1421` -- Support passing a ``EnvironBuilder`` or ``dict`` to - ``test_client.open``. :pr:`2412` -- The ``flask`` command and ``Flask.run`` will load environment - variables from ``.env`` and ``.flaskenv`` files if python-dotenv is - installed. :pr:`2416` -- When passing a full URL to the test client, the scheme in the URL is - used instead of ``PREFERRED_URL_SCHEME``. :pr:`2430` -- ``Flask.logger`` has been simplified. ``LOGGER_NAME`` and - ``LOGGER_HANDLER_POLICY`` config was removed. The logger is always - named ``flask.app``. The level is only set on first access, it - doesn't check ``Flask.debug`` each time. Only one format is used, - not different ones depending on ``Flask.debug``. No handlers are - removed, and a handler is only added if no handlers are already - configured. :pr:`2436` -- Blueprint view function names may not contain dots. :pr:`2450` -- Fix a ``ValueError`` caused by invalid ``Range`` requests in some - cases. :issue:`2526` -- The development server uses threads by default. :pr:`2529` -- Loading config files with ``silent=True`` will ignore ``ENOTDIR`` - errors. :pr:`2581` -- Pass ``--cert`` and ``--key`` options to ``flask run`` to run the - development server over HTTPS. :pr:`2606` -- Added ``SESSION_COOKIE_SAMESITE`` to control the ``SameSite`` - attribute on the session cookie. :pr:`2607` -- Added ``Flask.test_cli_runner`` to create a Click runner that can - invoke Flask CLI commands for testing. :pr:`2636` -- Subdomain matching is disabled by default and setting - ``SERVER_NAME`` does not implicitly enable it. It can be enabled by - passing ``subdomain_matching=True`` to the ``Flask`` constructor. - :pr:`2635` -- A single trailing slash is stripped from the blueprint - ``url_prefix`` when it is registered with the app. :pr:`2629` -- ``Request.get_json`` doesn't cache the result if parsing fails when - ``silent`` is true. :issue:`2651` -- ``Request.get_json`` no longer accepts arbitrary encodings. Incoming - JSON should be encoded using UTF-8 per :rfc:`8259`, but Flask will - autodetect UTF-8, -16, or -32. :pr:`2691` -- Added ``MAX_COOKIE_SIZE`` and ``Response.max_cookie_size`` to - control when Werkzeug warns about large cookies that browsers may - ignore. :pr:`2693` -- Updated documentation theme to make docs look better in small - windows. :pr:`2709` -- Rewrote the tutorial docs and example project to take a more - structured approach to help new users avoid common pitfalls. - :pr:`2676` - - -Version 0.12.5 --------------- - -Released 2020-02-10 - -- Pin Werkzeug to < 1.0.0. :issue:`3497` - - -Version 0.12.4 --------------- - -Released 2018-04-29 - -- Repackage 0.12.3 to fix package layout issue. :issue:`2728` - - -Version 0.12.3 --------------- - -Released 2018-04-26 - -- ``Request.get_json`` no longer accepts arbitrary encodings. - Incoming JSON should be encoded using UTF-8 per :rfc:`8259`, but - Flask will autodetect UTF-8, -16, or -32. :issue:`2692` -- Fix a Python warning about imports when using ``python -m flask``. - :issue:`2666` -- Fix a ``ValueError`` caused by invalid ``Range`` requests in some - cases. - - -Version 0.12.2 --------------- - -Released 2017-05-16 - -- Fix a bug in ``safe_join`` on Windows. - - -Version 0.12.1 --------------- - -Released 2017-03-31 - -- Prevent ``flask run`` from showing a ``NoAppException`` when an - ``ImportError`` occurs within the imported application module. -- Fix encoding behavior of ``app.config.from_pyfile`` for Python 3. - :issue:`2118` -- Use the ``SERVER_NAME`` config if it is present as default values - for ``app.run``. :issue:`2109`, :pr:`2152` -- Call ``ctx.auto_pop`` with the exception object instead of ``None``, - in the event that a ``BaseException`` such as ``KeyboardInterrupt`` - is raised in a request handler. - - -Version 0.12 ------------- - -Released 2016-12-21, codename Punsch - -- The cli command now responds to ``--version``. -- Mimetype guessing and ETag generation for file-like objects in - ``send_file`` has been removed. :issue:`104`, :pr`1849` -- Mimetype guessing in ``send_file`` now fails loudly and doesn't fall - back to ``application/octet-stream``. :pr:`1988` -- Make ``flask.safe_join`` able to join multiple paths like - ``os.path.join`` :pr:`1730` -- Revert a behavior change that made the dev server crash instead of - returning an Internal Server Error. :pr:`2006` -- Correctly invoke response handlers for both regular request - dispatching as well as error handlers. -- Disable logger propagation by default for the app logger. -- Add support for range requests in ``send_file``. -- ``app.test_client`` includes preset default environment, which can - now be directly set, instead of per ``client.get``. -- Fix crash when running under PyPy3. :pr:`1814` - - -Version 0.11.1 --------------- - -Released 2016-06-07 - -- Fixed a bug that prevented ``FLASK_APP=foobar/__init__.py`` from - working. :pr:`1872` - - -Version 0.11 ------------- - -Released 2016-05-29, codename Absinthe - -- Added support to serializing top-level arrays to ``jsonify``. This - introduces a security risk in ancient browsers. -- Added before_render_template signal. -- Added ``**kwargs`` to ``Flask.test_client`` to support passing - additional keyword arguments to the constructor of - ``Flask.test_client_class``. -- Added ``SESSION_REFRESH_EACH_REQUEST`` config key that controls the - set-cookie behavior. If set to ``True`` a permanent session will be - refreshed each request and get their lifetime extended, if set to - ``False`` it will only be modified if the session actually modifies. - Non permanent sessions are not affected by this and will always - expire if the browser window closes. -- Made Flask support custom JSON mimetypes for incoming data. -- Added support for returning tuples in the form ``(response, - headers)`` from a view function. -- Added ``Config.from_json``. -- Added ``Flask.config_class``. -- Added ``Config.get_namespace``. -- Templates are no longer automatically reloaded outside of debug - mode. This can be configured with the new ``TEMPLATES_AUTO_RELOAD`` - config key. -- Added a workaround for a limitation in Python 3.3's namespace - loader. -- Added support for explicit root paths when using Python 3.3's - namespace packages. -- Added ``flask`` and the ``flask.cli`` module to start the - local debug server through the click CLI system. This is recommended - over the old ``flask.run()`` method as it works faster and more - reliable due to a different design and also replaces - ``Flask-Script``. -- Error handlers that match specific classes are now checked first, - thereby allowing catching exceptions that are subclasses of HTTP - exceptions (in ``werkzeug.exceptions``). This makes it possible for - an extension author to create exceptions that will by default result - in the HTTP error of their choosing, but may be caught with a custom - error handler if desired. -- Added ``Config.from_mapping``. -- Flask will now log by default even if debug is disabled. The log - format is now hardcoded but the default log handling can be disabled - through the ``LOGGER_HANDLER_POLICY`` configuration key. -- Removed deprecated module functionality. -- Added the ``EXPLAIN_TEMPLATE_LOADING`` config flag which when - enabled will instruct Flask to explain how it locates templates. - This should help users debug when the wrong templates are loaded. -- Enforce blueprint handling in the order they were registered for - template loading. -- Ported test suite to py.test. -- Deprecated ``request.json`` in favour of ``request.get_json()``. -- Add "pretty" and "compressed" separators definitions in jsonify() - method. Reduces JSON response size when - ``JSONIFY_PRETTYPRINT_REGULAR=False`` by removing unnecessary white - space included by default after separators. -- JSON responses are now terminated with a newline character, because - it is a convention that UNIX text files end with a newline and some - clients don't deal well when this newline is missing. :pr:`1262` -- The automatically provided ``OPTIONS`` method is now correctly - disabled if the user registered an overriding rule with the - lowercase-version ``options``. :issue:`1288` -- ``flask.json.jsonify`` now supports the ``datetime.date`` type. - :pr:`1326` -- Don't leak exception info of already caught exceptions to context - teardown handlers. :pr:`1393` -- Allow custom Jinja environment subclasses. :pr:`1422` -- Updated extension dev guidelines. -- ``flask.g`` now has ``pop()`` and ``setdefault`` methods. -- Turn on autoescape for ``flask.templating.render_template_string`` - by default. :pr:`1515` -- ``flask.ext`` is now deprecated. :pr:`1484` -- ``send_from_directory`` now raises BadRequest if the filename is - invalid on the server OS. :pr:`1763` -- Added the ``JSONIFY_MIMETYPE`` configuration variable. :pr:`1728` -- Exceptions during teardown handling will no longer leave bad - application contexts lingering around. -- Fixed broken ``test_appcontext_signals()`` test case. -- Raise an ``AttributeError`` in ``helpers.find_package`` with a - useful message explaining why it is raised when a :pep:`302` import - hook is used without an ``is_package()`` method. -- Fixed an issue causing exceptions raised before entering a request - or app context to be passed to teardown handlers. -- Fixed an issue with query parameters getting removed from requests - in the test client when absolute URLs were requested. -- Made ``@before_first_request`` into a decorator as intended. -- Fixed an etags bug when sending a file streams with a name. -- Fixed ``send_from_directory`` not expanding to the application root - path correctly. -- Changed logic of before first request handlers to flip the flag - after invoking. This will allow some uses that are potentially - dangerous but should probably be permitted. -- Fixed Python 3 bug when a handler from - ``app.url_build_error_handlers`` reraises the ``BuildError``. - - -Version 0.10.1 --------------- - -Released 2013-06-14 - -- Fixed an issue where ``|tojson`` was not quoting single quotes which - made the filter not work properly in HTML attributes. Now it's - possible to use that filter in single quoted attributes. This should - make using that filter with angular.js easier. -- Added support for byte strings back to the session system. This - broke compatibility with the common case of people putting binary - data for token verification into the session. -- Fixed an issue where registering the same method twice for the same - endpoint would trigger an exception incorrectly. - - -Version 0.10 ------------- - -Released 2013-06-13, codename Limoncello - -- Changed default cookie serialization format from pickle to JSON to - limit the impact an attacker can do if the secret key leaks. -- Added ``template_test`` methods in addition to the already existing - ``template_filter`` method family. -- Added ``template_global`` methods in addition to the already - existing ``template_filter`` method family. -- Set the content-length header for x-sendfile. -- ``tojson`` filter now does not escape script blocks in HTML5 - parsers. -- ``tojson`` used in templates is now safe by default. This was - allowed due to the different escaping behavior. -- Flask will now raise an error if you attempt to register a new - function on an already used endpoint. -- Added wrapper module around simplejson and added default - serialization of datetime objects. This allows much easier - customization of how JSON is handled by Flask or any Flask - extension. -- Removed deprecated internal ``flask.session`` module alias. Use - ``flask.sessions`` instead to get the session module. This is not to - be confused with ``flask.session`` the session proxy. -- Templates can now be rendered without request context. The behavior - is slightly different as the ``request``, ``session`` and ``g`` - objects will not be available and blueprint's context processors are - not called. -- The config object is now available to the template as a real global - and not through a context processor which makes it available even in - imported templates by default. -- Added an option to generate non-ascii encoded JSON which should - result in less bytes being transmitted over the network. It's - disabled by default to not cause confusion with existing libraries - that might expect ``flask.json.dumps`` to return bytes by default. -- ``flask.g`` is now stored on the app context instead of the request - context. -- ``flask.g`` now gained a ``get()`` method for not erroring out on - non existing items. -- ``flask.g`` now can be used with the ``in`` operator to see what's - defined and it now is iterable and will yield all attributes stored. -- ``flask.Flask.request_globals_class`` got renamed to - ``flask.Flask.app_ctx_globals_class`` which is a better name to what - it does since 0.10. -- ``request``, ``session`` and ``g`` are now also added as proxies to - the template context which makes them available in imported - templates. One has to be very careful with those though because - usage outside of macros might cause caching. -- Flask will no longer invoke the wrong error handlers if a proxy - exception is passed through. -- Added a workaround for chrome's cookies in localhost not working as - intended with domain names. -- Changed logic for picking defaults for cookie values from sessions - to work better with Google Chrome. -- Added ``message_flashed`` signal that simplifies flashing testing. -- Added support for copying of request contexts for better working - with greenlets. -- Removed custom JSON HTTP exception subclasses. If you were relying - on them you can reintroduce them again yourself trivially. Using - them however is strongly discouraged as the interface was flawed. -- Python requirements changed: requiring Python 2.6 or 2.7 now to - prepare for Python 3.3 port. -- Changed how the teardown system is informed about exceptions. This - is now more reliable in case something handles an exception halfway - through the error handling process. -- Request context preservation in debug mode now keeps the exception - information around which means that teardown handlers are able to - distinguish error from success cases. -- Added the ``JSONIFY_PRETTYPRINT_REGULAR`` configuration variable. -- Flask now orders JSON keys by default to not trash HTTP caches due - to different hash seeds between different workers. -- Added ``appcontext_pushed`` and ``appcontext_popped`` signals. -- The builtin run method now takes the ``SERVER_NAME`` into account - when picking the default port to run on. -- Added ``flask.request.get_json()`` as a replacement for the old - ``flask.request.json`` property. - - -Version 0.9 ------------ - -Released 2012-07-01, codename Campari - -- The ``Request.on_json_loading_failed`` now returns a JSON formatted - response by default. -- The ``url_for`` function now can generate anchors to the generated - links. -- The ``url_for`` function now can also explicitly generate URL rules - specific to a given HTTP method. -- Logger now only returns the debug log setting if it was not set - explicitly. -- Unregister a circular dependency between the WSGI environment and - the request object when shutting down the request. This means that - environ ``werkzeug.request`` will be ``None`` after the response was - returned to the WSGI server but has the advantage that the garbage - collector is not needed on CPython to tear down the request unless - the user created circular dependencies themselves. -- Session is now stored after callbacks so that if the session payload - is stored in the session you can still modify it in an after request - callback. -- The ``Flask`` class will avoid importing the provided import name if - it can (the required first parameter), to benefit tools which build - Flask instances programmatically. The Flask class will fall back to - using import on systems with custom module hooks, e.g. Google App - Engine, or when the import name is inside a zip archive (usually an - egg) prior to Python 2.7. -- Blueprints now have a decorator to add custom template filters - application wide, ``Blueprint.app_template_filter``. -- The Flask and Blueprint classes now have a non-decorator method for - adding custom template filters application wide, - ``Flask.add_template_filter`` and - ``Blueprint.add_app_template_filter``. -- The ``get_flashed_messages`` function now allows rendering flashed - message categories in separate blocks, through a ``category_filter`` - argument. -- The ``Flask.run`` method now accepts ``None`` for ``host`` and - ``port`` arguments, using default values when ``None``. This allows - for calling run using configuration values, e.g. - ``app.run(app.config.get('MYHOST'), app.config.get('MYPORT'))``, - with proper behavior whether or not a config file is provided. -- The ``render_template`` method now accepts a either an iterable of - template names or a single template name. Previously, it only - accepted a single template name. On an iterable, the first template - found is rendered. -- Added ``Flask.app_context`` which works very similar to the request - context but only provides access to the current application. This - also adds support for URL generation without an active request - context. -- View functions can now return a tuple with the first instance being - an instance of ``Response``. This allows for returning - ``jsonify(error="error msg"), 400`` from a view function. -- ``Flask`` and ``Blueprint`` now provide a ``get_send_file_max_age`` - hook for subclasses to override behavior of serving static files - from Flask when using ``Flask.send_static_file`` (used for the - default static file handler) and ``helpers.send_file``. This hook is - provided a filename, which for example allows changing cache - controls by file extension. The default max-age for ``send_file`` - and static files can be configured through a new - ``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable, which is used - in the default ``get_send_file_max_age`` implementation. -- Fixed an assumption in sessions implementation which could break - message flashing on sessions implementations which use external - storage. -- Changed the behavior of tuple return values from functions. They are - no longer arguments to the response object, they now have a defined - meaning. -- Added ``Flask.request_globals_class`` to allow a specific class to - be used on creation of the ``g`` instance of each request. -- Added ``required_methods`` attribute to view functions to force-add - methods on registration. -- Added ``flask.after_this_request``. -- Added ``flask.stream_with_context`` and the ability to push contexts - multiple times without producing unexpected behavior. - - -Version 0.8.1 -------------- - -Released 2012-07-01 - -- Fixed an issue with the undocumented ``flask.session`` module to not - work properly on Python 2.5. It should not be used but did cause - some problems for package managers. - - -Version 0.8 ------------ - -Released 2011-09-29, codename Rakija - -- Refactored session support into a session interface so that the - implementation of the sessions can be changed without having to - override the Flask class. -- Empty session cookies are now deleted properly automatically. -- View functions can now opt out of getting the automatic OPTIONS - implementation. -- HTTP exceptions and Bad Request errors can now be trapped so that - they show up normally in the traceback. -- Flask in debug mode is now detecting some common problems and tries - to warn you about them. -- Flask in debug mode will now complain with an assertion error if a - view was attached after the first request was handled. This gives - earlier feedback when users forget to import view code ahead of - time. -- Added the ability to register callbacks that are only triggered once - at the beginning of the first request with - ``Flask.before_first_request``. -- Malformed JSON data will now trigger a bad request HTTP exception - instead of a value error which usually would result in a 500 - internal server error if not handled. This is a backwards - incompatible change. -- Applications now not only have a root path where the resources and - modules are located but also an instance path which is the - designated place to drop files that are modified at runtime (uploads - etc.). Also this is conceptually only instance depending and outside - version control so it's the perfect place to put configuration files - etc. -- Added the ``APPLICATION_ROOT`` configuration variable. -- Implemented ``TestClient.session_transaction`` to easily modify - sessions from the test environment. -- Refactored test client internally. The ``APPLICATION_ROOT`` - configuration variable as well as ``SERVER_NAME`` are now properly - used by the test client as defaults. -- Added ``View.decorators`` to support simpler decorating of pluggable - (class-based) views. -- Fixed an issue where the test client if used with the "with" - statement did not trigger the execution of the teardown handlers. -- Added finer control over the session cookie parameters. -- HEAD requests to a method view now automatically dispatch to the - ``get`` method if no handler was implemented. -- Implemented the virtual ``flask.ext`` package to import extensions - from. -- The context preservation on exceptions is now an integral component - of Flask itself and no longer of the test client. This cleaned up - some internal logic and lowers the odds of runaway request contexts - in unittests. -- Fixed the Jinja environment's ``list_templates`` method not - returning the correct names when blueprints or modules were - involved. - - -Version 0.7.2 -------------- - -Released 2011-07-06 - -- Fixed an issue with URL processors not properly working on - blueprints. - - -Version 0.7.1 -------------- - -Released 2011-06-29 - -- Added missing future import that broke 2.5 compatibility. -- Fixed an infinite redirect issue with blueprints. - - -Version 0.7 ------------ - -Released 2011-06-28, codename Grappa - -- Added ``Flask.make_default_options_response`` which can be used by - subclasses to alter the default behavior for ``OPTIONS`` responses. -- Unbound locals now raise a proper ``RuntimeError`` instead of an - ``AttributeError``. -- Mimetype guessing and etag support based on file objects is now - deprecated for ``send_file`` because it was unreliable. Pass - filenames instead or attach your own etags and provide a proper - mimetype by hand. -- Static file handling for modules now requires the name of the static - folder to be supplied explicitly. The previous autodetection was not - reliable and caused issues on Google's App Engine. Until 1.0 the old - behavior will continue to work but issue dependency warnings. -- Fixed a problem for Flask to run on jython. -- Added a ``PROPAGATE_EXCEPTIONS`` configuration variable that can be - used to flip the setting of exception propagation which previously - was linked to ``DEBUG`` alone and is now linked to either ``DEBUG`` - or ``TESTING``. -- Flask no longer internally depends on rules being added through the - ``add_url_rule`` function and can now also accept regular werkzeug - rules added to the url map. -- Added an ``endpoint`` method to the flask application object which - allows one to register a callback to an arbitrary endpoint with a - decorator. -- Use Last-Modified for static file sending instead of Date which was - incorrectly introduced in 0.6. -- Added ``create_jinja_loader`` to override the loader creation - process. -- Implemented a silent flag for ``config.from_pyfile``. -- Added ``teardown_request`` decorator, for functions that should run - at the end of a request regardless of whether an exception occurred. - Also the behavior for ``after_request`` was changed. It's now no - longer executed when an exception is raised. -- Implemented ``has_request_context``. -- Deprecated ``init_jinja_globals``. Override the - ``Flask.create_jinja_environment`` method instead to achieve the - same functionality. -- Added ``safe_join``. -- The automatic JSON request data unpacking now looks at the charset - mimetype parameter. -- Don't modify the session on ``get_flashed_messages`` if there are no - messages in the session. -- ``before_request`` handlers are now able to abort requests with - errors. -- It is not possible to define user exception handlers. That way you - can provide custom error messages from a central hub for certain - errors that might occur during request processing (for instance - database connection errors, timeouts from remote resources etc.). -- Blueprints can provide blueprint specific error handlers. -- Implemented generic class-based views. - - -Version 0.6.1 -------------- - -Released 2010-12-31 - -- Fixed an issue where the default ``OPTIONS`` response was not - exposing all valid methods in the ``Allow`` header. -- Jinja template loading syntax now allows "./" in front of a - template load path. Previously this caused issues with module - setups. -- Fixed an issue where the subdomain setting for modules was ignored - for the static folder. -- Fixed a security problem that allowed clients to download arbitrary - files if the host server was a windows based operating system and - the client uses backslashes to escape the directory the files where - exposed from. - - -Version 0.6 ------------ - -Released 2010-07-27, codename Whisky - -- After request functions are now called in reverse order of - registration. -- OPTIONS is now automatically implemented by Flask unless the - application explicitly adds 'OPTIONS' as method to the URL rule. In - this case no automatic OPTIONS handling kicks in. -- Static rules are now even in place if there is no static folder for - the module. This was implemented to aid GAE which will remove the - static folder if it's part of a mapping in the .yml file. -- ``Flask.config`` is now available in the templates as ``config``. -- Context processors will no longer override values passed directly to - the render function. -- Added the ability to limit the incoming request data with the new - ``MAX_CONTENT_LENGTH`` configuration value. -- The endpoint for the ``Module.add_url_rule`` method is now optional - to be consistent with the function of the same name on the - application object. -- Added a ``make_response`` function that simplifies creating response - object instances in views. -- Added signalling support based on blinker. This feature is currently - optional and supposed to be used by extensions and applications. If - you want to use it, make sure to have ``blinker`` installed. -- Refactored the way URL adapters are created. This process is now - fully customizable with the ``Flask.create_url_adapter`` method. -- Modules can now register for a subdomain instead of just an URL - prefix. This makes it possible to bind a whole module to a - configurable subdomain. - - -Version 0.5.2 -------------- - -Released 2010-07-15 - -- Fixed another issue with loading templates from directories when - modules were used. - - -Version 0.5.1 -------------- - -Released 2010-07-06 - -- Fixes an issue with template loading from directories when modules - where used. - - -Version 0.5 ------------ - -Released 2010-07-06, codename Calvados - -- Fixed a bug with subdomains that was caused by the inability to - specify the server name. The server name can now be set with the - ``SERVER_NAME`` config key. This key is now also used to set the - session cookie cross-subdomain wide. -- Autoescaping is no longer active for all templates. Instead it is - only active for ``.html``, ``.htm``, ``.xml`` and ``.xhtml``. Inside - templates this behavior can be changed with the ``autoescape`` tag. -- Refactored Flask internally. It now consists of more than a single - file. -- ``send_file`` now emits etags and has the ability to do conditional - responses builtin. -- (temporarily) dropped support for zipped applications. This was a - rarely used feature and led to some confusing behavior. -- Added support for per-package template and static-file directories. -- Removed support for ``create_jinja_loader`` which is no longer used - in 0.5 due to the improved module support. -- Added a helper function to expose files from any directory. - - -Version 0.4 ------------ - -Released 2010-06-18, codename Rakia - -- Added the ability to register application wide error handlers from - modules. -- ``Flask.after_request`` handlers are now also invoked if the request - dies with an exception and an error handling page kicks in. -- Test client has not the ability to preserve the request context for - a little longer. This can also be used to trigger custom requests - that do not pop the request stack for testing. -- Because the Python standard library caches loggers, the name of the - logger is configurable now to better support unittests. -- Added ``TESTING`` switch that can activate unittesting helpers. -- The logger switches to ``DEBUG`` mode now if debug is enabled. - - -Version 0.3.1 -------------- - -Released 2010-05-28 - -- Fixed a error reporting bug with ``Config.from_envvar``. -- Removed some unused code. -- Release does no longer include development leftover files (.git - folder for themes, built documentation in zip and pdf file and some - .pyc files) - - -Version 0.3 ------------ - -Released 2010-05-28, codename Schnaps - -- Added support for categories for flashed messages. -- The application now configures a ``logging.Handler`` and will log - request handling exceptions to that logger when not in debug mode. - This makes it possible to receive mails on server errors for - example. -- Added support for context binding that does not require the use of - the with statement for playing in the console. -- The request context is now available within the with statement - making it possible to further push the request context or pop it. -- Added support for configurations. - - -Version 0.2 ------------ - -Released 2010-05-12, codename J?germeister - -- Various bugfixes -- Integrated JSON support -- Added ``get_template_attribute`` helper function. -- ``Flask.add_url_rule`` can now also register a view function. -- Refactored internal request dispatching. -- Server listens on 127.0.0.1 by default now to fix issues with - chrome. -- Added external URL support. -- Added support for ``send_file``. -- Module support and internal request handling refactoring to better - support pluggable applications. -- Sessions can be set to be permanent now on a per-session basis. -- Better error reporting on missing secret keys. -- Added support for Google Appengine. - - -Version 0.1 ------------ - -Released 2010-04-16 - -- First public preview release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..5d269389 --- /dev/null +++ b/LICENSE @@ -0,0 +1,33 @@ +Copyright (c) 2010 by Armin Ronacher and contributors. See AUTHORS +for more details. + +Some rights reserved. + +Redistribution and use in source and binary forms of the software as well +as documentation, with or without modification, are permitted provided +that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +* The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 9d227a0c..00000000 --- a/LICENSE.txt +++ /dev/null @@ -1,28 +0,0 @@ -Copyright 2010 Pallets - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..f82ed054 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,16 @@ +include Makefile CHANGES LICENSE AUTHORS run-tests.py +recursive-include artwork * +recursive-include tests * +recursive-include examples * +recursive-include docs * +recursive-exclude docs *.pyc +recursive-exclude docs *.pyo +recursive-exclude tests *.pyc +recursive-exclude tests *.pyo +recursive-exclude examples *.pyc +recursive-exclude examples *.pyo +recursive-include flask/testsuite/static * +recursive-include flask/testsuite/templates * +recursive-include flask/testsuite/test_apps * +prune docs/_build +prune docs/_themes/.git diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..43f47275 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +.PHONY: clean-pyc ext-test test upload-docs docs audit + +all: clean-pyc test + +test: + python run-tests.py + +audit: + python setup.py audit + +release: + python scripts/make-release.py + +tox-test: + PYTHONDONTWRITEBYTECODE= tox + +ext-test: + python tests/flaskext_test.py --browse + +clean-pyc: + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + +upload-docs: + $(MAKE) -C docs html dirhtml latex + $(MAKE) -C docs/_build/latex all-pdf + cd docs/_build/; mv html flask-docs; zip -r flask-docs.zip flask-docs; mv flask-docs html + rsync -a docs/_build/dirhtml/ pocoo.org:/var/www/flask.pocoo.org/docs/ + rsync -a docs/_build/latex/Flask.pdf pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.pdf + rsync -a docs/_build/flask-docs.zip pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.zip + +docs: + $(MAKE) -C docs html diff --git a/README b/README new file mode 100644 index 00000000..7d5ada23 --- /dev/null +++ b/README @@ -0,0 +1,52 @@ + // Flask // + + web development, one drop at a time + + + ~ What is Flask? + + Flask is a microframework for Python based on Werkzeug + and Jinja2. It's intended for small scale applications + and was developed with best intentions in mind. + + ~ Is it ready? + + It's still not 1.0 but it's shaping up nicely and is + already widely used. Consider the API to slightly + improve over time but we don't plan on breaking it. + + ~ What do I need? + + Jinja 2.4 and Werkzeug 0.6.1. `pip` or `easy_install` will + install them for you if you do `easy_install Flask`. + I encourage you to use a virtualenv. Check the docs for + complete installation and usage instructions. + + ~ Where are the docs? + + Go to http://flask.pocoo.org/docs/ for a prebuilt version + of the current documentation. Otherwise build them yourself + from the sphinx sources in the docs folder. + + ~ Where are the tests? + + Good that you're asking. The tests are in the + flask/testsuite package. To run the tests use the + `run-tests.py` file: + + $ python run-tests.py + + If it's not enough output for you, you can use the + `--verbose` flag: + + $ python run-tests.py --verbose + + If you just want one particular testcase to run you can + provide it on the command line: + + $ python run-tests.py test_to_run + + ~ Where can I get help? + + Either use the #pocoo IRC channel on irc.freenode.net or + ask on the mailinglist: http://flask.pocoo.org/mailinglist/ diff --git a/README.md b/README.md deleted file mode 100644 index 64f56cac..00000000 --- a/README.md +++ /dev/null @@ -1,53 +0,0 @@ -
- -# Flask - -Flask is a lightweight [WSGI] web application framework. It is designed -to make getting started quick and easy, with the ability to scale up to -complex applications. It began as a simple wrapper around [Werkzeug] -and [Jinja], and has become one of the most popular Python web -application frameworks. - -Flask offers suggestions, but doesn't enforce any dependencies or -project layout. It is up to the developer to choose the tools and -libraries they want to use. There are many extensions provided by the -community that make adding new functionality easy. - -[WSGI]: https://wsgi.readthedocs.io/ -[Werkzeug]: https://werkzeug.palletsprojects.com/ -[Jinja]: https://jinja.palletsprojects.com/ - -## A Simple Example - -```python -# save this as app.py -from flask import Flask - -app = Flask(__name__) - -@app.route("/") -def hello(): - return "Hello, World!" -``` - -``` -$ flask run - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) -``` - -## Donate - -The Pallets organization develops and supports Flask and the libraries -it uses. In order to grow the community of contributors and users, and -allow the maintainers to devote more time to the projects, [please -donate today]. - -[please donate today]: https://palletsprojects.com/donate - -## Contributing - -See our [detailed contributing documentation][contrib] for many ways to -contribute, including reporting issues, requesting features, asking or answering -questions, and making PRs. - -[contrib]: https://palletsprojects.com/contributing/ diff --git a/artwork/LICENSE b/artwork/LICENSE new file mode 100644 index 00000000..c6df416c --- /dev/null +++ b/artwork/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2010 by Armin Ronacher. + +Some rights reserved. + +This logo or a modified version may be used by anyone to refer to the +Flask project, but does not indicate endorsement by the project. + +Redistribution and use in source (the SVG file) and binary forms (rendered +PNG files etc.) of the image, with or without modification, are permitted +provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice and this list of conditions. + +* The names of the contributors to the Flask software (see AUTHORS) may + not be used to endorse or promote products derived from this software + without specific prior written permission. + +Note: we would appreciate that you make the image a link to +http://flask.pocoo.org/ if you use it on a web page. diff --git a/artwork/logo-full.svg b/artwork/logo-full.svg new file mode 100644 index 00000000..8c0748a2 --- /dev/null +++ b/artwork/logo-full.svg @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..e35d8850 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +_build diff --git a/docs/Makefile b/docs/Makefile index d4bb2cbb..52d78d9e 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,20 +1,118 @@ -# Minimal makefile for Sphinx documentation +# Makefile for Sphinx documentation # -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = BUILDDIR = _build -# Put it first so that "make" without argument is like "make help". +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp epub latex changes linkcheck doctest + help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" -.PHONY: help Makefile +clean: + -rm -rf $(BUILDDIR)/* -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) _build/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask" + @echo "# ln -s _build/devhelp $$HOME/.local/share/devhelp/Flask" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +latexpdf: latex + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex + @echo "Running LaTeX files through pdflatex..." + make -C _build/latex all-pdf + @echo "pdflatex finished; the PDF files are in _build/latex." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/_static/debugger.png b/docs/_static/debugger.png index 7d4181f6..4f47229d 100644 Binary files a/docs/_static/debugger.png and b/docs/_static/debugger.png differ diff --git a/docs/_static/flask-icon.svg b/docs/_static/flask-icon.svg deleted file mode 100644 index c802da9a..00000000 --- a/docs/_static/flask-icon.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/docs/_static/flask-logo.svg b/docs/_static/flask-logo.svg deleted file mode 100644 index c216b617..00000000 --- a/docs/_static/flask-logo.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/docs/_static/flask-name.svg b/docs/_static/flask-name.svg deleted file mode 100644 index b46782d2..00000000 --- a/docs/_static/flask-name.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/docs/_static/flask.png b/docs/_static/flask.png new file mode 100644 index 00000000..5c603cc2 Binary files /dev/null and b/docs/_static/flask.png differ diff --git a/docs/_static/flaskr.png b/docs/_static/flaskr.png new file mode 100644 index 00000000..07d027dd Binary files /dev/null and b/docs/_static/flaskr.png differ diff --git a/docs/_static/logo-full.png b/docs/_static/logo-full.png new file mode 100644 index 00000000..5deaf1b8 Binary files /dev/null and b/docs/_static/logo-full.png differ diff --git a/docs/_static/no.png b/docs/_static/no.png new file mode 100644 index 00000000..4ac1083d Binary files /dev/null and b/docs/_static/no.png differ diff --git a/docs/_static/pycharm-run-config.png b/docs/_static/pycharm-run-config.png deleted file mode 100644 index ad025545..00000000 Binary files a/docs/_static/pycharm-run-config.png and /dev/null differ diff --git a/docs/_static/touch-icon.png b/docs/_static/touch-icon.png new file mode 100644 index 00000000..cd1e91e1 Binary files /dev/null and b/docs/_static/touch-icon.png differ diff --git a/docs/_static/yes.png b/docs/_static/yes.png new file mode 100644 index 00000000..ac27c4e1 Binary files /dev/null and b/docs/_static/yes.png differ diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html new file mode 100644 index 00000000..164c8745 --- /dev/null +++ b/docs/_templates/sidebarintro.html @@ -0,0 +1,22 @@ +

About Flask

+

+ Flask is a micro webdevelopment framework for Python. You are currently + looking at the documentation of the development version. Things are + not stable yet, but if you have some feedback, + let me know. +

+

Other Formats

+

+ You can download the documentation in other formats as well: +

+ +

Useful Links

+ diff --git a/docs/_templates/sidebarlogo.html b/docs/_templates/sidebarlogo.html new file mode 100644 index 00000000..3bc7f762 --- /dev/null +++ b/docs/_templates/sidebarlogo.html @@ -0,0 +1,3 @@ + diff --git a/docs/_themes b/docs/_themes new file mode 160000 index 00000000..3d964b66 --- /dev/null +++ b/docs/_themes @@ -0,0 +1 @@ +Subproject commit 3d964b660442e23faedf801caed6e3c7bd42d5c9 diff --git a/docs/api.rst b/docs/api.rst index 52b25376..7695788e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,9 +1,11 @@ +.. _api: + API === .. module:: flask -This part of the documentation covers all the interfaces of Flask. For +This part of the documentation covers all the interfaces of Flask. For parts where Flask depends on external libraries, we document the most important right here and provide links to the canonical documentation. @@ -27,66 +29,170 @@ Incoming Request Data --------------------- .. autoclass:: Request - :members: - :inherited-members: - :exclude-members: json_module + :members: -.. data:: request + .. attribute:: form - A proxy to the request data for the current request, an instance of - :class:`.Request`. + A :class:`~werkzeug.datastructures.MultiDict` with the parsed form data from `POST` + or `PUT` requests. Please keep in mind that file uploads will not + end up here, but instead in the :attr:`files` attribute. - This is only available when a :doc:`request context ` is - active. + .. attribute:: args - This is a proxy. See :ref:`context-visibility` for more information. + A :class:`~werkzeug.datastructures.MultiDict` with the parsed contents of the query + string. (The part in the URL after the question mark). + + .. attribute:: values + + A :class:`~werkzeug.datastructures.CombinedMultiDict` with the contents of both + :attr:`form` and :attr:`args`. + + .. attribute:: cookies + + A :class:`dict` with the contents of all cookies transmitted with + the request. + + .. attribute:: stream + + If the incoming form data was not encoded with a known mimetype + the data is stored unmodified in this stream for consumption. Most + of the time it is a better idea to use :attr:`data` which will give + you that data as a string. The stream only returns the data once. + + .. attribute:: headers + + The incoming request headers as a dictionary like object. + + .. attribute:: data + + Contains the incoming request data as string in case it came with + a mimetype Flask does not handle. + + .. attribute:: files + + A :class:`~werkzeug.datastructures.MultiDict` with files uploaded as part of a + `POST` or `PUT` request. Each file is stored as + :class:`~werkzeug.datastructures.FileStorage` object. It basically behaves like a + standard file object you know from Python, with the difference that + it also has a :meth:`~werkzeug.datastructures.FileStorage.save` function that can + store the file on the filesystem. + + .. attribute:: environ + + The underlying WSGI environment. + + .. attribute:: method + + The current request method (``POST``, ``GET`` etc.) + + .. attribute:: path + .. attribute:: script_root + .. attribute:: url + .. attribute:: base_url + .. attribute:: url_root + + Provides different ways to look at the current URL. Imagine your + application is listening on the following URL:: + + http://www.example.com/myapplication + + And a user requests the following URL:: + + http://www.example.com/myapplication/page.html?x=y + + In this case the values of the above mentioned attributes would be + the following: + + ============= ====================================================== + `path` ``/page.html`` + `script_root` ``/myapplication`` + `base_url` ``http://www.example.com/myapplication/page.html`` + `url` ``http://www.example.com/myapplication/page.html?x=y`` + `url_root` ``http://www.example.com/myapplication/`` + ============= ====================================================== + + .. attribute:: is_xhr + + `True` if the request was triggered via a JavaScript + `XMLHttpRequest`. This only works with libraries that support the + ``X-Requested-With`` header and set it to `XMLHttpRequest`. + Libraries that do that are prototype, jQuery and Mochikit and + probably some more. + +.. class:: request + + To access incoming request data, you can use the global `request` + object. Flask parses incoming request data for you and gives you + access to it through that global object. Internally Flask makes + sure that you always get the correct data for the active thread if you + are in a multithreaded environment. + + This is a proxy. See :ref:`notes-on-proxies` for more information. + + The request object is an instance of a :class:`~werkzeug.wrappers.Request` + subclass and provides all of the attributes Werkzeug defines. This + just shows a quick overview of the most important ones. Response Objects ---------------- .. autoclass:: flask.Response - :members: - :inherited-members: - :exclude-members: json_module + :members: set_cookie, data, mimetype + + .. attribute:: headers + + A :class:`Headers` object representing the response headers. + + .. attribute:: status_code + + The response status as integer. + Sessions -------- -If you have set :attr:`Flask.secret_key` (or configured it from -:data:`SECRET_KEY`) you can use sessions in Flask applications. A session makes -it possible to remember information from one request to another. The way Flask -does this is by using a signed cookie. The user can look at the session -contents, but can't modify it unless they know the secret key, so make sure to -set that to something complex and unguessable. +If you have the :attr:`Flask.secret_key` set you can use sessions in Flask +applications. A session basically makes it possible to remember +information from one request to another. The way Flask does this is by +using a signed cookie. So the user can look at the session contents, but +not modify it unless they know the secret key, so make sure to set that +to something complex and unguessable. -To access the current session you can use the :data:`.session` proxy. +To access the current session you can use the :class:`session` object: -.. data:: session +.. class:: session - A proxy to the session data for the current request, an instance of - :class:`.SessionMixin`. + The session object works pretty much like an ordinary dict, with the + difference that it keeps track on modifications. - This is only available when a :doc:`request context ` is - active. + This is a proxy. See :ref:`notes-on-proxies` for more information. - This is a proxy. See :ref:`context-visibility` for more information. + The following attributes are interesting: - The session object works like a dict but tracks assignment and access to its - keys. It cannot track modifications to mutable values, you need to set - :attr:`~.SessionMixin.modified` manually when modifying a list, dict, etc. + .. attribute:: new - .. code-block:: python + `True` if the session is new, `False` otherwise. - # appending to a list is not detected - session["numbers"].append(42) + .. attribute:: modified + + `True` if the session object detected a modification. Be advised + that modifications on mutable structures are not picked up + automatically, in that situation you have to explicitly set the + attribute to `True` yourself. Here an example:: + + # this change is not picked up because a mutable object (here + # a list) is changed. + session['objects'].append(42) # so mark it as modified yourself session.modified = True - The session is persisted across requests using a cookie. By default the - users's browser will clear the cookie when it is closed. Set - :attr:`~.SessionMixin.permanent` to ``True`` to persist the cookie for - :data:`PERMANENT_SESSION_LIFETIME`. + .. attribute:: permanent + + If set to `True` the session lives for + :attr:`~flask.Flask.permanent_session_lifetime` seconds. The + default is 31 days. If set to `False` (which is the default) the + session will be deleted when the user closes the browser. Session Interface @@ -105,9 +211,6 @@ implementation that Flask is using. .. autoclass:: SecureCookieSessionInterface :members: -.. autoclass:: SecureCookieSession - :members: - .. autoclass:: NullSession :members: @@ -116,9 +219,10 @@ implementation that Flask is using. .. admonition:: Notice - The :data:`PERMANENT_SESSION_LIFETIME` config can be an integer or ``timedelta``. - The :attr:`~flask.Flask.permanent_session_lifetime` attribute is always a - ``timedelta``. + The ``PERMANENT_SESSION_LIFETIME`` config key can also be an integer + starting with Flask 0.8. Either catch this down yourself or use + the :attr:`~flask.Flask.permanent_session_lifetime` attribute on the + app which converts the result to an integer automatically. Test Client @@ -130,15 +234,6 @@ Test Client :members: -Test CLI Runner ---------------- - -.. currentmodule:: flask.testing - -.. autoclass:: FlaskCliRunner - :members: - - Application Globals ------------------- @@ -146,30 +241,17 @@ Application Globals To share data that is valid for one request only from one function to another, a global variable is not good enough because it would break in -threaded environments. Flask provides you with a special object that +threaded environments. Flask provides you with a special object that ensures it is only valid for the active request and that will return -different values for each request. In a nutshell: it does the right -thing, like it does for :data:`.request` and :data:`.session`. +different values for each request. In a nutshell: it does the right +thing, like it does for :class:`request` and :class:`session`. .. data:: g - A proxy to a namespace object used to store data during a single request or - app context. An instance of :attr:`.Flask.app_ctx_globals_class`, which - defaults to :class:`._AppCtxGlobals`. + Just store on this whatever you want. For example a database + connection or the user that is currently logged in. - This is a good place to store resources during a request. For example, a - :meth:`~.Flask.before_request` function could load a user object from a - session id, then set ``g.user`` to be used in the view function. - - This is only available when an :doc:`app context ` is active. - - This is a proxy. See :ref:`context-visibility` for more information. - - .. versionchanged:: 0.10 - Bound to the application context instead of the request context. - -.. autoclass:: flask.ctx._AppCtxGlobals - :members: + This is a proxy. See :ref:`notes-on-proxies` for more information. Useful Functions and Classes @@ -177,37 +259,38 @@ Useful Functions and Classes .. data:: current_app - A proxy to the :class:`.Flask` application handling the current request or - other activity. + Points to the application handling the request. This is useful for + extensions that want to support multiple applications running side + by side. - This is useful to access the application without needing to import it, or if - it can't be imported, such as when using the application factory pattern or - in blueprints and extensions. - - This is only available when an :doc:`app context ` is active. - - This is a proxy. See :ref:`context-visibility` for more information. + This is a proxy. See :ref:`notes-on-proxies` for more information. .. autofunction:: has_request_context -.. autofunction:: copy_current_request_context - -.. autofunction:: has_app_context - .. autofunction:: url_for -.. autofunction:: abort +.. function:: abort(code) + + Raises an :exc:`~werkzeug.exceptions.HTTPException` for the given + status code. For example to abort request handling with a page not + found exception, you would call ``abort(404)``. + + :param code: the HTTP error code. .. autofunction:: redirect .. autofunction:: make_response -.. autofunction:: after_this_request - .. autofunction:: send_file .. autofunction:: send_from_directory +.. autofunction:: safe_join + +.. autofunction:: escape + +.. autoclass:: Markup + :members: escape, unescape, striptags Message Flashing ---------------- @@ -216,63 +299,47 @@ Message Flashing .. autofunction:: get_flashed_messages - -JSON Support ------------- - -.. module:: flask.json - -Flask uses Python's built-in :mod:`json` module for handling JSON by -default. The JSON implementation can be changed by assigning a different -provider to :attr:`flask.Flask.json_provider_class` or -:attr:`flask.Flask.json`. The functions provided by ``flask.json`` will -use methods on ``app.json`` if an app context is active. - -Jinja's ``|tojson`` filter is configured to use the app's JSON provider. -The filter marks the output with ``|safe``. Use it to render data inside -HTML `` +Returning JSON +-------------- .. autofunction:: jsonify -.. autofunction:: dumps +.. data:: json -.. autofunction:: dump + If JSON support is picked up, this will be the module that Flask is + using to parse and serialize JSON. So instead of doing this yourself:: -.. autofunction:: loads + try: + import simplejson as json + except ImportError: + import json -.. autofunction:: load + You can instead just do this:: -.. autoclass:: flask.json.provider.JSONProvider - :members: - :member-order: bysource + from flask import json -.. autoclass:: flask.json.provider.DefaultJSONProvider - :members: - :member-order: bysource + For usage examples, read the :mod:`json` documentation. -.. automodule:: flask.json.tag + The :func:`~json.dumps` function of this json module is also available + as filter called ``|tojson`` in Jinja2. Note that inside `script` + tags no escaping must take place, so make sure to disable escaping + with ``|safe`` if you intend to use it inside `script` tags: + .. sourcecode:: html+jinja + + + + Note that the ``|tojson`` filter escapes forward slashes properly. Template Rendering ------------------ -.. currentmodule:: flask - .. autofunction:: render_template .. autofunction:: render_template_string -.. autofunction:: stream_template - -.. autofunction:: stream_template_string - .. autofunction:: get_template_attribute Configuration @@ -281,237 +348,131 @@ Configuration .. autoclass:: Config :members: +Extensions +---------- -Stream Helpers --------------- +.. data:: flask.ext -.. autofunction:: stream_with_context + This module acts as redirect import module to Flask extensions. It was + added in 0.8 as the canonical way to import Flask extensions and makes + it possible for us to have more flexibility in how we distribute + extensions. + + If you want to use an extension named “Flask-Foo” you would import it + from :data:`~flask.ext` as follows:: + + from flask.ext import foo + + .. versionadded:: 0.8 Useful Internals ---------------- -.. autoclass:: flask.ctx.AppContext +.. autoclass:: flask.ctx.RequestContext :members: -.. data:: flask.globals.app_ctx +.. data:: _request_ctx_stack - A proxy to the active :class:`.AppContext`. + The internal :class:`~werkzeug.local.LocalStack` that is used to implement + all the context local objects used in Flask. This is a documented + instance and can be used by extensions and application code but the + use is discouraged in general. - This is an internal object that is essential to how Flask handles requests. - Accessing this should not be needed in most cases. Most likely you want - :data:`.current_app`, :data:`.g`, :data:`.request`, and :data:`.session` instead. + The following attributes are always present on each layer of the + stack: - This is only available when a :doc:`request context ` is - active. + `app` + the active Flask application. - This is a proxy. See :ref:`context-visibility` for more information. + `url_adapter` + the URL adapter that was used to match the request. -.. class:: flask.ctx.RequestContext + `request` + the current request object. - .. deprecated:: 3.2 - Merged with :class:`AppContext`. This alias will be removed in Flask 4.0. + `session` + the active session object. -.. data:: flask.globals.request_ctx + `g` + an object with all the attributes of the :data:`flask.g` object. - .. deprecated:: 3.2 - Merged with :data:`.app_ctx`. This alias will be removed in Flask 4.0. + `flashes` + an internal cache for the flashed messages. + + Example usage:: + + from flask import _request_ctx_stack + + def get_session(): + ctx = _request_ctx_stack.top + if ctx is not None: + return ctx.session .. autoclass:: flask.blueprints.BlueprintSetupState :members: -.. _core-signals-list: - Signals ------- -Signals are provided by the `Blinker`_ library. See :doc:`signals` for an introduction. +.. when modifying this list, also update the one in signals.rst -.. _blinker: https://blinker.readthedocs.io/ +.. versionadded:: 0.6 + +.. data:: signals_available + + `True` if the signalling system is available. This is the case + when `blinker`_ is installed. .. data:: template_rendered - This signal is sent when a template was successfully rendered. The + This signal is sent when a template was successfully rendered. The signal is invoked with the instance of the template as `template` and the context as dictionary (named `context`). - Example subscriber:: - - def log_template_renders(sender, template, context, **extra): - sender.logger.debug('Rendering template "%s" with context %s', - template.name or 'string template', - context) - - from flask import template_rendered - template_rendered.connect(log_template_renders, app) - -.. data:: flask.before_render_template - :noindex: - - This signal is sent before template rendering process. The - signal is invoked with the instance of the template as `template` - and the context as dictionary (named `context`). - - Example subscriber:: - - def log_template_renders(sender, template, context, **extra): - sender.logger.debug('Rendering template "%s" with context %s', - template.name or 'string template', - context) - - from flask import before_render_template - before_render_template.connect(log_template_renders, app) - .. data:: request_started - This signal is sent when the request context is set up, before - any request processing happens. Because the request context is already + This signal is sent before any request processing started but when the + request context was set up. Because the request context is already bound, the subscriber can access the request with the standard global proxies such as :class:`~flask.request`. - Example subscriber:: - - def log_request(sender, **extra): - sender.logger.debug('Request context is set up') - - from flask import request_started - request_started.connect(log_request, app) - .. data:: request_finished This signal is sent right before the response is sent to the client. It is passed the response to be sent named `response`. - Example subscriber:: - - def log_response(sender, response, **extra): - sender.logger.debug('Request context is about to close down. ' - 'Response: %s', response) - - from flask import request_finished - request_finished.connect(log_response, app) - .. data:: got_request_exception - This signal is sent when an unhandled exception happens during - request processing, including when debugging. The exception is - passed to the subscriber as ``exception``. - - This signal is not sent for - :exc:`~werkzeug.exceptions.HTTPException`, or other exceptions that - have error handlers registered, unless the exception was raised from - an error handler. - - This example shows how to do some extra logging if a theoretical - ``SecurityException`` was raised: - - .. code-block:: python - - from flask import got_request_exception - - def log_security_exception(sender, exception, **extra): - if not isinstance(exception, SecurityException): - return - - security_logger.exception( - f"SecurityException at {request.url!r}", - exc_info=exception, - ) - - got_request_exception.connect(log_security_exception, app) + This signal is sent when an exception happens during request processing. + It is sent *before* the standard exception handling kicks in and even + in debug mode, where no exception handling happens. The exception + itself is passed to the subscriber as `exception`. .. data:: request_tearing_down - This signal is sent when the request is tearing down. This is always - called, even if an exception is caused. Currently functions listening - to this signal are called after the regular teardown handlers, but this - is not something you can rely on. + This signal is sent when the application is tearing down the request. + This is always called, even if an error happened. No arguments are + provided. - Example subscriber:: +.. currentmodule:: None - def close_db_connection(sender, **extra): - session.close() +.. class:: flask.signals.Namespace - from flask import request_tearing_down - request_tearing_down.connect(close_db_connection, app) + An alias for :class:`blinker.base.Namespace` if blinker is available, + otherwise a dummy class that creates fake signals. This class is + available for Flask extensions that want to provide the same fallback + system as Flask itself. - As of Flask 0.9, this will also be passed an `exc` keyword argument - that has a reference to the exception that caused the teardown if - there was one. + .. method:: signal(name, doc=None) -.. data:: appcontext_tearing_down + Creates a new signal for this namespace if blinker is available, + otherwise returns a fake signal that has a send method that will + do nothing but will fail with a :exc:`RuntimeError` for all other + operations, including connecting. - This signal is sent when the app context is tearing down. This is always - called, even if an exception is caused. Currently functions listening - to this signal are called after the regular teardown handlers, but this - is not something you can rely on. +.. _blinker: http://pypi.python.org/pypi/blinker - Example subscriber:: - - def close_db_connection(sender, **extra): - session.close() - - from flask import appcontext_tearing_down - appcontext_tearing_down.connect(close_db_connection, app) - - This will also be passed an `exc` keyword argument that has a reference - to the exception that caused the teardown if there was one. - -.. data:: appcontext_pushed - - This signal is sent when an application context is pushed. The sender - is the application. This is usually useful for unittests in order to - temporarily hook in information. For instance it can be used to - set a resource early onto the `g` object. - - Example usage:: - - from contextlib import contextmanager - from flask import appcontext_pushed - - @contextmanager - def user_set(app, user): - def handler(sender, **kwargs): - g.user = user - with appcontext_pushed.connected_to(handler, app): - yield - - And in the testcode:: - - def test_user_me(self): - with user_set(app, 'john'): - c = app.test_client() - resp = c.get('/users/me') - assert resp.data == 'username=john' - - .. versionadded:: 0.10 - -.. data:: appcontext_popped - - This signal is sent when an application context is popped. The sender - is the application. This usually falls in line with the - :data:`appcontext_tearing_down` signal. - - .. versionadded:: 0.10 - -.. data:: message_flashed - - This signal is sent when the application is flashing a message. The - messages is sent as `message` keyword argument and the category as - `category`. - - Example subscriber:: - - recorded = [] - def record(sender, message, category, **extra): - recorded.append((message, category)) - - from flask import message_flashed - message_flashed.connect(record, app) - - .. versionadded:: 0.10 - - -Class-Based Views +Class Based Views ----------------- .. versionadded:: 0.7 @@ -537,7 +498,7 @@ Generally there are three ways to define rules for the routing system: which is exposed as :attr:`flask.Flask.url_map`. Variable parts in the route can be specified with angular brackets -(``/user/``). By default a variable part in the URL accepts any +(``/user/``). By default a variable part in the URL accepts any string without a slash however a different converter can be specified as well by using ````. @@ -546,16 +507,12 @@ Variable parts are passed to the view function as keyword arguments. The following converters are available: =========== =============================================== -`string` accepts any text without a slash (the default) +`unicode` accepts any text without a slash (the default) `int` accepts integers `float` like `int` but for floating point values `path` like the default but also accepts slashes -`any` matches one of the items provided -`uuid` accepts UUID strings =========== =============================================== -Custom converters can be defined using :attr:`flask.Flask.url_map`. - Here are some examples:: @app.route('/') @@ -571,7 +528,7 @@ Here are some examples:: pass An important detail to keep in mind is how Flask deals with trailing -slashes. The idea is to keep each URL unique so the following rules +slashes. The idea is to keep each URL unique so the following rules apply: 1. If a rule ends with a slash and is requested without a slash by the @@ -580,11 +537,11 @@ apply: 2. If a rule does not end with a trailing slash and the user requests the page with a trailing slash, a 404 not found is raised. -This is consistent with how web servers deal with static files. This +This is consistent with how web servers deal with static files. This also makes it possible to use relative link targets safely. -You can also define multiple rules for the same function. They have to be -unique however. Defaults can also be specified. Here for example is a +You can also define multiple rules for the same function. They have to be +unique however. Defaults can also be specified. Here for example is a definition for a URL that accepts an optional page:: @app.route('/users/', defaults={'page': 1}) @@ -593,49 +550,39 @@ definition for a URL that accepts an optional page:: pass This specifies that ``/users/`` will be the URL for page one and -``/users/page/N`` will be the URL for page ``N``. - -If a URL contains a default value, it will be redirected to its simpler -form with a 308 redirect. In the above example, ``/users/page/1`` will -be redirected to ``/users/``. If your route handles ``GET`` and ``POST`` -requests, make sure the default route only handles ``GET``, as redirects -can't preserve form data. :: - - @app.route('/region/', defaults={'id': 1}) - @app.route('/region/', methods=['GET', 'POST']) - def region(id): - pass +``/users/page/N`` will be the URL for page `N`. Here are the parameters that :meth:`~flask.Flask.route` and -:meth:`~flask.Flask.add_url_rule` accept. The only difference is that +:meth:`~flask.Flask.add_url_rule` accept. The only difference is that with the route parameter the view function is defined with the decorator instead of the `view_func` parameter. =============== ========================================================== -`rule` the URL rule as string -`endpoint` the endpoint for the registered URL rule. Flask itself +`rule` the URL roule as string +`endpoint` the endpoint for the registered URL rule. Flask itself assumes that the name of the view function is the name of the endpoint if not explicitly stated. `view_func` the function to call when serving a request to the - provided endpoint. If this is not provided one can + provided endpoint. If this is not provided one can specify the function later by storing it in the :attr:`~flask.Flask.view_functions` dictionary with the endpoint as key. -`defaults` A dictionary with defaults for this rule. See the +`defaults` A dictionary with defaults for this rule. See the example above for how defaults work. `subdomain` specifies the rule for the subdomain in case subdomain - matching is in use. If not specified the default + matching is in use. If not specified the default subdomain is assumed. `**options` the options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object. A change to - Werkzeug is handling of method options. methods is a list - of methods this rule should be limited to (``GET``, ``POST`` - etc.). By default a rule just listens for ``GET`` (and - implicitly ``HEAD``). Starting with Flask 0.6, ``OPTIONS`` is + :class:`~werkzeug.routing.Rule` object. A change to + Werkzeug is handling of method options. methods is a list + of methods this rule should be limited to (`GET`, `POST` + etc.). By default a rule just listens for `GET` (and + implicitly `HEAD`). Starting with Flask 0.6, `OPTIONS` is implicitly added and handled by the standard request - handling. They have to be specified as keyword arguments. + handling. They have to be specified as keyword arguments. =============== ========================================================== +.. _view-func-options: View Function Options --------------------- @@ -645,26 +592,22 @@ customize behavior the view function would normally not have control over. The following attributes can be provided optionally to either override some defaults to :meth:`~flask.Flask.add_url_rule` or general behavior: -- `__name__`: The name of a function is by default used as endpoint. If - endpoint is provided explicitly this value is used. Additionally this +- `__name__`: The name of a function is by default used as endpoint. If + endpoint is provided explicitly this value is used. Additionally this will be prefixed with the name of the blueprint by default which cannot be customized from the function itself. - `methods`: If methods are not provided when the URL rule is added, - Flask will look on the view function object itself if a `methods` - attribute exists. If it does, it will pull the information for the + Flask will look on the view function object itself is an `methods` + attribute exists. If it does, it will pull the information for the methods from there. - `provide_automatic_options`: if this attribute is set Flask will either force enable or disable the automatic implementation of the - HTTP ``OPTIONS`` response. This can be useful when working with - decorators that want to customize the ``OPTIONS`` response on a per-view + HTTP `OPTIONS` response. This can be useful when working with + decorators that want to customize the `OPTIONS` response on a per-view basis. -- `required_methods`: if this attribute is set, Flask will always add - these methods when registering a URL rule even if the methods were - explicitly overridden in the ``route()`` call. - Full example:: def index(): @@ -679,30 +622,3 @@ Full example:: .. versionadded:: 0.8 The `provide_automatic_options` functionality was added. - -Command Line Interface ----------------------- - -.. currentmodule:: flask.cli - -.. autoclass:: FlaskGroup - :members: - -.. autoclass:: AppGroup - :members: - -.. autoclass:: ScriptInfo - :members: - -.. autofunction:: load_dotenv - -.. autofunction:: with_appcontext - -.. autofunction:: pass_script_info - - Marks a function so that an instance of :class:`ScriptInfo` is passed - as first argument to the click callback. - -.. autodata:: run_command - -.. autodata:: shell_command diff --git a/docs/appcontext.rst b/docs/appcontext.rst deleted file mode 100644 index 883d8cd2..00000000 --- a/docs/appcontext.rst +++ /dev/null @@ -1,187 +0,0 @@ -The App and Request Context -=========================== - -The context keeps track of data and objects during a request, CLI command, or -other activity. Rather than passing this data around to every function, the -:data:`.current_app`, :data:`.g`, :data:`.request`, and :data:`.session` proxies -are accessed instead. - -When handling a request, the context is referred to as the "request context" -because it contains request data in addition to application data. Otherwise, -such as during a CLI command, it is referred to as the "app context". During an -app context, :data:`.current_app` and :data:`.g` are available, while during a -request context :data:`.request` and :data:`.session` are also available. - - -Purpose of the Context ----------------------- - -The context and proxies help solve two development issues: circular imports, and -passing around global data during a request. - -The :class:`.Flask` application object has attributes, such as -:attr:`~.Flask.config`, that are useful to access within views and other -functions. However, importing the ``app`` instance within the modules in your -project is prone to circular import issues. When using the -:doc:`app factory pattern ` or writing reusable -:doc:`blueprints ` or :doc:`extensions ` there won't -be an ``app`` instance to import at all. - -When the application handles a request, it creates a :class:`.Request` object. -Because a *worker* handles only one request at a time, the request data can be -considered global to that worker during that request. Passing it as an argument -through every function during the request becomes verbose and redundant. - -Flask solves these issues with the *active context* pattern. Rather than -importing an ``app`` directly, or having to pass it and the request through to -every single function, you import and access the proxies, which point to the -currently active application and request data. This is sometimes referred to -as "context local" data. - - -Context During Setup --------------------- - -If you try to access :data:`.current_app`, :data:`.g`, or anything that uses it, -outside an app context, you'll get this error message: - -.. code-block:: pytb - - RuntimeError: Working outside of application context. - - Attempted to use functionality that expected a current application to be - set. To solve this, set up an app context using 'with app.app_context()'. - See the documentation on app context for more information. - -If you see that error while configuring your application, such as when -initializing an extension, you can push a context manually since you have direct -access to the ``app``. Use :meth:`.Flask.app_context` in a ``with`` block. - -.. code-block:: python - - def create_app(): - app = Flask(__name__) - - with app.app_context(): - init_db() - - return app - -If you see that error somewhere else in your code not related to setting up the -application, it most likely indicates that you should move that code into a view -function or CLI command. - - -Context During Testing ----------------------- - -See :doc:`/testing` for detailed information about managing the context during -tests. - -If you try to access :data:`.request`, :data:`.session`, or anything that uses -it, outside a request context, you'll get this error message: - -.. code-block:: pytb - - RuntimeError: Working outside of request context. - - Attempted to use functionality that expected an active HTTP request. See the - documentation on request context for more information. - -This will probably only happen during tests. If you see that error somewhere -else in your code not related to testing, it most likely indicates that you -should move that code into a view function. - -The primary way to solve this is to use :meth:`.Flask.test_client` to simulate -a full request. - -If you only want to unit test one function, rather than a full request, use -:meth:`.Flask.test_request_context` in a ``with`` block. - -.. code-block:: python - - def generate_report(year): - format = request.args.get("format") - ... - - with app.test_request_context( - "/make_report/2017", query_string={"format": "short"} - ): - generate_report() - - -.. _context-visibility: - -Visibility of the Context -------------------------- - -The context will have the same lifetime as an activity, such as a request, CLI -command, or ``with`` block. Various callbacks and signals registered with the -app will be run during the context. - -When a Flask application handles a request, it pushes a request context -to set the active application and request data. When it handles a CLI command, -it pushes an app context to set the active application. When the activity ends, -it pops that context. Proxy objects like :data:`.request`, :data:`.session`, -:data:`.g`, and :data:`.current_app`, are accessible while the context is pushed -and active, and are not accessible after the context is popped. - -The context is unique to each thread (or other worker type). The proxies cannot -be passed to another worker, which has a different context space and will not -know about the active context in the parent's space. - -Besides being scoped to each worker, the proxy object has a separate type and -identity than the proxied real object. In some cases you'll need access to the -real object, rather than the proxy. Use the -:meth:`~.LocalProxy._get_current_object` method in those cases. - -.. code-block:: python - - app = current_app._get_current_object() - my_signal.send(app) - - -Lifecycle of the Context ------------------------- - -Flask dispatches a request in multiple stages which can affect the request, -response, and how errors are handled. See :doc:`/lifecycle` for a list of all -the steps, callbacks, and signals during each request. The following are the -steps directly related to the context. - -- The app context is pushed, the proxies are available. -- The :data:`.appcontext_pushed` signal is sent. -- The request is dispatched. -- Any :meth:`.Flask.teardown_request` decorated functions are called. -- The :data:`.request_tearing_down` signal is sent. -- Any :meth:`.Flask.teardown_appcontext` decorated functions are called. -- The :data:`.appcontext_tearing_down` signal is sent. -- The app context is popped, the proxies are no longer available. -- The :data:`.appcontext_popped` signal is sent. - -The teardown callbacks are called by the context when it is popped. They are -called even if there is an unhandled exception during dispatch. They may be -called multiple times in some test scenarios. This means there is no guarantee -that any other parts of the request dispatch have run. Be sure to write these -functions in a way that does not depend on other callbacks. All callbacks are -called even if any raise an error. - - -How the Context Works ---------------------- - -Context locals are implemented using Python's :mod:`contextvars` and Werkzeug's -:class:`~werkzeug.local.LocalProxy`. Python's contextvars are a low level -structure to manage data local to a thread or coroutine. ``LocalProxy`` wraps -the contextvar so that access to any attributes and methods is forwarded to the -object stored in the contextvar. - -The context is tracked like a stack, with the active context at the top of the -stack. Flask manages pushing and popping contexts during requests, CLI commands, -testing, ``with`` blocks, etc. The proxies access attributes on the active -context. - -Because it is a stack, other contexts may be pushed to change the proxies during -an already active context. This is not a common pattern, but can be used in -advanced use cases. For example, a Flask application can be used as WSGI -middleware, calling another wrapped Flask app from a view. diff --git a/docs/async-await.rst b/docs/async-await.rst deleted file mode 100644 index bb333802..00000000 --- a/docs/async-await.rst +++ /dev/null @@ -1,119 +0,0 @@ -.. _async_await: - -Using ``async`` and ``await`` -============================= - -.. versionadded:: 2.0 - -Routes, error handlers, before request, after request, and teardown -functions can all be coroutine functions if Flask is installed with the -``async`` extra (``pip install flask[async]``). This allows views to be -defined with ``async def`` and use ``await``. - -.. code-block:: python - - @app.route("/get-data") - async def get_data(): - data = await async_db_query(...) - return jsonify(data) - -Pluggable class-based views also support handlers that are implemented as -coroutines. This applies to the :meth:`~flask.views.View.dispatch_request` -method in views that inherit from the :class:`flask.views.View` class, as -well as all the HTTP method handlers in views that inherit from the -:class:`flask.views.MethodView` class. - - -Performance ------------ - -Async functions require an event loop to run. Flask, as a WSGI -application, uses one worker to handle one request/response cycle. -When a request comes in to an async view, Flask will start an event loop -in a thread, run the view function there, then return the result. - -Each request still ties up one worker, even for async views. The upside -is that you can run async code within a view, for example to make -multiple concurrent database queries, HTTP requests to an external API, -etc. However, the number of requests your application can handle at one -time will remain the same. - -**Async is not inherently faster than sync code.** Async is beneficial -when performing concurrent IO-bound tasks, but will probably not improve -CPU-bound tasks. Traditional Flask views will still be appropriate for -most use cases, but Flask's async support enables writing and using -code that wasn't possible natively before. - - -Background tasks ----------------- - -Async functions will run in an event loop until they complete, at -which stage the event loop will stop. This means any additional -spawned tasks that haven't completed when the async function completes -will be cancelled. Therefore you cannot spawn background tasks, for -example via ``asyncio.create_task``. - -If you wish to use background tasks it is best to use a task queue to -trigger background work, rather than spawn tasks in a view -function. With that in mind you can spawn asyncio tasks by serving -Flask with an ASGI server and utilising the asgiref WsgiToAsgi adapter -as described in :doc:`deploying/asgi`. This works as the adapter creates -an event loop that runs continually. - - -When to use Quart instead -------------------------- - -Flask's async support is less performant than async-first frameworks due -to the way it is implemented. If you have a mainly async codebase it -would make sense to consider `Quart`_. Quart is a reimplementation of -Flask based on the `ASGI`_ standard instead of WSGI. This allows it to -handle many concurrent requests, long running requests, and websockets -without requiring multiple worker processes or threads. - -It has also already been possible to :doc:`run Flask with Gevent ` to -get many of the benefits of async request handling. Gevent patches low-level -Python functions to accomplish this, whereas ``async``/``await`` and ASGI use -standard, modern Python capabilities. Deciding whether you should use gevent -with Flask, or Quart, or something else is ultimately up to understanding the -specific needs of your project. - -.. _Quart: https://quart.palletsprojects.com -.. _ASGI: https://asgi.readthedocs.io - - -Extensions ----------- - -Flask extensions predating Flask's async support do not expect async views. -If they provide decorators to add functionality to views, those will probably -not work with async views because they will not await the function or be -awaitable. Other functions they provide will not be awaitable either and -will probably be blocking if called within an async view. - -Extension authors can support async functions by utilising the -:meth:`flask.Flask.ensure_sync` method. For example, if the extension -provides a view function decorator add ``ensure_sync`` before calling -the decorated function, - -.. code-block:: python - - def extension(func): - @wraps(func) - def wrapper(*args, **kwargs): - ... # Extension logic - return current_app.ensure_sync(func)(*args, **kwargs) - - return wrapper - -Check the changelog of the extension you want to use to see if they've -implemented async support, or make a feature request or PR to them. - - -Other event loops ------------------ - -At the moment Flask only supports :mod:`asyncio`. It's possible to override -:meth:`flask.Flask.ensure_sync` to change how async functions are wrapped to use -a different library. See :ref:`gevent-asyncio` for an example. diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst new file mode 100644 index 00000000..20a0186e --- /dev/null +++ b/docs/becomingbig.rst @@ -0,0 +1,88 @@ +.. _becomingbig: + +Becoming Big +============ + +Your application is becoming more and more complex? If you suddenly +realize that Flask does things in a way that does not work out for your +application there are ways to deal with that. + +Flask is powered by Werkzeug and Jinja2, two libraries that are in use at +a number of large websites out there and all Flask does is bring those +two together. Being a microframework Flask does not do much more than +combining existing libraries - there is not a lot of code involved. +What that means for large applications is that it's very easy to take the +code from Flask and put it into a new module within the applications and +expand on that. + +Flask is designed to be extended and modified in a couple of different +ways: + +- Flask extensions. For a lot of reusable functionality you can create + extensions. For extensions a number of hooks exist throughout Flask + with signals and callback functions. + +- Subclassing. The majority of functionality can be changed by creating + a new subclass of the :class:`~flask.Flask` class and overriding + methods provided for this exact purpose. + +- Forking. If nothing else works out you can just take the Flask + codebase at a given point and copy/paste it into your application + and change it. Flask is designed with that in mind and makes this + incredible easy. You just have to take the package and copy it + into your application's code and rename it (for example to + `framework`). Then you can start modifying the code in there. + +Why consider Forking? +--------------------- + +The majority of code of Flask is within Werkzeug and Jinja2. These +libraries do the majority of the work. Flask is just the paste that glues +those together. For every project there is the point where the underlying +framework gets in the way (due to assumptions the original developers +had). This is natural because if this would not be the case, the +framework would be a very complex system to begin with which causes a +steep learning curve and a lot of user frustration. + +This is not unique to Flask. Many people use patched and modified +versions of their framework to counter shortcomings. This idea is also +reflected in the license of Flask. You don't have to contribute any +changes back if you decide to modify the framework. + +The downside of forking is of course that Flask extensions will most +likely break because the new framework has a different import name. +Furthermore integrating upstream changes can be a complex process, +depending on the number of changes. Because of that, forking should be +the very last resort. + +Scaling like a Pro +------------------ + +For many web applications the complexity of the code is less an issue than +the scaling for the number of users or data entries expected. Flask by +itself is only limited in terms of scaling by your application code, the +data store you want to use and the Python implementation and webserver you +are running on. + +Scaling well means for example that if you double the amount of servers +you get about twice the performance. Scaling bad means that if you add a +new server the application won't perform any better or would not even +support a second server. + +There is only one limiting factor regarding scaling in Flask which are +the context local proxies. They depend on context which in Flask is +defined as being either a thread, process or greenlet. If your server +uses some kind of concurrency that is not based on threads or greenlets, +Flask will no longer be able to support these global proxies. However the +majority of servers are using either threads, greenlets or separate +processes to achieve concurrency which are all methods well supported by +the underlying Werkzeug library. + +Dialogue with the Community +--------------------------- + +The Flask developers are very interested to keep everybody happy, so as +soon as you find an obstacle in your way, caused by Flask, don't hesitate +to contact the developers on the mailinglist or IRC channel. The best way +for the Flask and Flask-extension developers to improve it for larger +applications is getting feedback from users. diff --git a/docs/blueprints.rst b/docs/blueprints.rst index d5cf3d82..9422fd02 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -1,8 +1,8 @@ +.. _blueprints: + Modular Applications with Blueprints ==================================== -.. currentmodule:: flask - .. versionadded:: 0.7 Flask uses a concept of *blueprints* for making application components and @@ -35,9 +35,8 @@ Blueprints in Flask are intended for these cases: A blueprint in Flask is not a pluggable app because it is not actually an application -- it's a set of operations which can be registered on an application, even multiple times. Why not have multiple application -objects? You can do that (see :doc:`/patterns/appdispatch`), but your -applications will have separate configs and will be managed at the WSGI -layer. +objects? You can do that (see :ref:`app-dispatch`), but your applications +will have separate configs and will be managed at the WSGI layer. Blueprints instead provide separation at the Flask level, share application config, and can change an application object as necessary with @@ -62,24 +61,22 @@ implement a blueprint that does simple rendering of static templates:: from flask import Blueprint, render_template, abort from jinja2 import TemplateNotFound - simple_page = Blueprint('simple_page', __name__, - template_folder='templates') + simple_page = Blueprint('simple_page', __name__) @simple_page.route('/', defaults={'page': 'index'}) @simple_page.route('/') def show(page): try: - return render_template(f'pages/{page}.html') + return render_template('pages/%s.html' % page) except TemplateNotFound: abort(404) When you bind a function with the help of the ``@simple_page.route`` -decorator, the blueprint will record the intention of registering the -function ``show`` on the application when it's later registered. +decorator the blueprint will record the intention of registering the +function `show` on the application when it's later registered. Additionally it will prefix the endpoint of the function with the name of the blueprint which was given to the :class:`Blueprint` -constructor (in this case also ``simple_page``). The blueprint's name -does not modify the URL, only the endpoint. +constructor (in this case also ``simple_page``). Registering Blueprints ---------------------- @@ -95,12 +92,11 @@ So how do you register that blueprint? Like this:: If you check the rules registered on the application, you will find these:: - >>> app.url_map - Map([' (HEAD, OPTIONS, GET) -> static>, + [' (HEAD, OPTIONS, GET) -> static>, ' (HEAD, OPTIONS, GET) -> simple_page.show>, - simple_page.show>]) + simple_page.show>] -The first one is obviously from the application itself for the static +The first one is obviously from the application ifself for the static files. The other two are for the `show` function of the ``simple_page`` blueprint. As you can see, they are also prefixed with the name of the blueprint and separated by a dot (``.``). @@ -111,53 +107,14 @@ Blueprints however can also be mounted at different locations:: And sure enough, these are the generated rules:: - >>> app.url_map - Map([' (HEAD, OPTIONS, GET) -> static>, + [' (HEAD, OPTIONS, GET) -> static>, ' (HEAD, OPTIONS, GET) -> simple_page.show>, - simple_page.show>]) + simple_page.show>] On top of that you can register blueprints multiple times though not every blueprint might respond properly to that. In fact it depends on how the blueprint is implemented if it can be mounted more than once. -Nesting Blueprints ------------------- - -It is possible to register a blueprint on another blueprint. - -.. code-block:: python - - parent = Blueprint('parent', __name__, url_prefix='/parent') - child = Blueprint('child', __name__, url_prefix='/child') - parent.register_blueprint(child) - app.register_blueprint(parent) - -The child blueprint will gain the parent's name as a prefix to its -name, and child URLs will be prefixed with the parent's URL prefix. - -.. code-block:: python - - url_for('parent.child.create') - /parent/child/create - -In addition a child blueprint's will gain their parent's subdomain, -with their subdomain as prefix if present i.e. - -.. code-block:: python - - parent = Blueprint('parent', __name__, subdomain='parent') - child = Blueprint('child', __name__, subdomain='child') - parent.register_blueprint(child) - app.register_blueprint(parent) - - url_for('parent.child.create', _external=True) - "child.parent.domain.tld" - -Blueprint-specific before request functions, etc. registered with the -parent will trigger for the child. If a child does not have an error -handler that can handle a given exception, the parent's will be tried. - - Blueprint Resources ------------------- @@ -191,31 +148,23 @@ To quickly open sources from this folder you can use the Static Files ```````````` -A blueprint can expose a folder with static files by providing the path -to the folder on the filesystem with the ``static_folder`` argument. -It is either an absolute path or relative to the blueprint's location:: +A blueprint can expose a folder with static files by providing a path to a +folder on the filesystem via the `static_folder` keyword argument. It can +either be an absolute path or one relative to the folder of the +blueprint:: admin = Blueprint('admin', __name__, static_folder='static') By default the rightmost part of the path is where it is exposed on the -web. This can be changed with the ``static_url_path`` argument. Because the -folder is called ``static`` here it will be available at the -``url_prefix`` of the blueprint + ``/static``. If the blueprint -has the prefix ``/admin``, the static URL will be ``/admin/static``. +web. Because the folder is called ``static`` here it will be available at +the location of the blueprint + ``/static``. Say the blueprint is +registered for ``/admin`` the static folder will be at ``/admin/static``. -The endpoint is named ``blueprint_name.static``. You can generate URLs -to it with :func:`url_for` like you would with the static folder of the -application:: +The endpoint is named `blueprint_name.static` so you can generate URLs to +it like you would do to the static folder of the application:: url_for('admin.static', filename='style.css') -However, if the blueprint does not have a ``url_prefix``, it is not -possible to access the blueprint's static folder. This is because the -URL would be ``/static`` in this case, and the application's ``/static`` -route takes precedence. Unlike template folders, blueprint static -folders are not searched if the file does not exist in the application -static folder. - Templates ````````` @@ -224,43 +173,16 @@ the `template_folder` parameter to the :class:`Blueprint` constructor:: admin = Blueprint('admin', __name__, template_folder='templates') -For static files, the path can be absolute or relative to the blueprint -resource folder. - -The template folder is added to the search path of templates but with a lower -priority than the actual application's template folder. That way you can -easily override templates that a blueprint provides in the actual application. -This also means that if you don't want a blueprint template to be accidentally -overridden, make sure that no other blueprint or actual application template -has the same relative path. When multiple blueprints provide the same relative -template path the first blueprint registered takes precedence over the others. - +As for static files, the path can be absolute or relative to the blueprint +resource folder. The template folder is added to the searchpath of +templates but with a lower priority than the actual application's template +folder. That way you can easily override templates that a blueprint +provides in the actual application. So if you have a blueprint in the folder ``yourapplication/admin`` and you want to render the template ``'admin/index.html'`` and you have provided ``templates`` as a `template_folder` you will have to create a file like -this: :file:`yourapplication/admin/templates/admin/index.html`. The reason -for the extra ``admin`` folder is to avoid getting our template overridden -by a template named ``index.html`` in the actual application template -folder. - -To further reiterate this: if you have a blueprint named ``admin`` and you -want to render a template called :file:`index.html` which is specific to this -blueprint, the best idea is to lay out your templates like this:: - - yourpackage/ - blueprints/ - admin/ - templates/ - admin/ - index.html - __init__.py - -And then when you want to render the template, use :file:`admin/index.html` as -the name to look up the template by. If you encounter problems loading -the correct templates enable the ``EXPLAIN_TEMPLATE_LOADING`` config -variable which will instruct Flask to print out the steps it goes through -to locate templates on every ``render_template`` call. +this: ``yourapplication/admin/templates/admin/index.html``. Building URLs ------------- @@ -279,37 +201,3 @@ you can use relative redirects by prefixing the endpoint with a dot only:: This will link to ``admin.index`` for instance in case the current request was dispatched to any other admin blueprint endpoint. - - -Blueprint Error Handlers ------------------------- - -Blueprints support the ``errorhandler`` decorator just like the :class:`Flask` -application object, so it is easy to make Blueprint-specific custom error -pages. - -Here is an example for a "404 Page Not Found" exception:: - - @simple_page.errorhandler(404) - def page_not_found(e): - return render_template('pages/404.html') - -Most errorhandlers will simply work as expected; however, there is a caveat -concerning handlers for 404 and 405 exceptions. These errorhandlers are only -invoked from an appropriate ``raise`` statement or a call to ``abort`` in another -of the blueprint's view functions; they are not invoked by, e.g., an invalid URL -access. This is because the blueprint does not "own" a certain URL space, so -the application instance has no way of knowing which blueprint error handler it -should run if given an invalid URL. If you would like to execute different -handling strategies for these errors based on URL prefixes, they may be defined -at the application level using the ``request`` proxy object:: - - @app.errorhandler(404) - @app.errorhandler(405) - def _handle_api_error(ex): - if request.path.startswith('/api/'): - return jsonify(error=str(ex)), ex.code - else: - return ex - -See :doc:`/errorhandling`. diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 00000000..d6c5f48c --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1 @@ +.. include:: ../CHANGES diff --git a/docs/changes.rst b/docs/changes.rst deleted file mode 100644 index 955deaf2..00000000 --- a/docs/changes.rst +++ /dev/null @@ -1,4 +0,0 @@ -Changes -======= - -.. include:: ../CHANGES.rst diff --git a/docs/cli.rst b/docs/cli.rst deleted file mode 100644 index a72e6d51..00000000 --- a/docs/cli.rst +++ /dev/null @@ -1,556 +0,0 @@ -.. currentmodule:: flask - -Command Line Interface -====================== - -Installing Flask installs the ``flask`` script, a `Click`_ command line -interface, in your virtualenv. Executed from the terminal, this script gives -access to built-in, extension, and application-defined commands. The ``--help`` -option will give more information about any commands and options. - -.. _Click: https://click.palletsprojects.com/ - - -Application Discovery ---------------------- - -The ``flask`` command is installed by Flask, not your application; it must be -told where to find your application in order to use it. The ``--app`` -option is used to specify how to load the application. - -While ``--app`` supports a variety of options for specifying your -application, most use cases should be simple. Here are the typical values: - -(nothing) - The name "app" or "wsgi" is imported (as a ".py" file, or package), - automatically detecting an app (``app`` or ``application``) or - factory (``create_app`` or ``make_app``). - -``--app hello`` - The given name is imported, automatically detecting an app (``app`` - or ``application``) or factory (``create_app`` or ``make_app``). - ----- - -``--app`` has three parts: an optional path that sets the current working -directory, a Python file or dotted import path, and an optional variable -name of the instance or factory. If the name is a factory, it can optionally -be followed by arguments in parentheses. The following values demonstrate these -parts: - -``--app src/hello`` - Sets the current working directory to ``src`` then imports ``hello``. - -``--app hello.web`` - Imports the path ``hello.web``. - -``--app hello:app2`` - Uses the ``app2`` Flask instance in ``hello``. - -``--app 'hello:create_app("dev")'`` - The ``create_app`` factory in ``hello`` is called with the string ``'dev'`` - as the argument. - -If ``--app`` is not set, the command will try to import "app" or -"wsgi" (as a ".py" file, or package) and try to detect an application -instance or factory. - -Within the given import, the command looks for an application instance named -``app`` or ``application``, then any application instance. If no instance is -found, the command looks for a factory function named ``create_app`` or -``make_app`` that returns an instance. - -If parentheses follow the factory name, their contents are parsed as -Python literals and passed as arguments and keyword arguments to the -function. This means that strings must still be in quotes. - - -Run the Development Server --------------------------- - -The :func:`run ` command will start the development server. It -replaces the :meth:`Flask.run` method in most cases. :: - - $ flask --app hello run - * Serving Flask app "hello" - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) - -.. warning:: Do not use this command to run your application in production. - Only use the development server during development. The development server - is provided for convenience, but is not designed to be particularly secure, - stable, or efficient. See :doc:`/deploying/index` for how to run in production. - -If another program is already using port 5000, you'll see -``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the -server tries to start. See :ref:`address-already-in-use` for how to -handle that. - - -Debug Mode -~~~~~~~~~~ - -In debug mode, the ``flask run`` command will enable the interactive debugger and the -reloader by default, and make errors easier to see and debug. To enable debug mode, use -the ``--debug`` option. - -.. code-block:: console - - $ flask --app hello run --debug - * Serving Flask app "hello" - * Debug mode: on - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) - * Restarting with inotify reloader - * Debugger is active! - * Debugger PIN: 223-456-919 - -The ``--debug`` option can also be passed to the top level ``flask`` command to enable -debug mode for any command. The following two ``run`` calls are equivalent. - -.. code-block:: console - - $ flask --app hello --debug run - $ flask --app hello run --debug - - -Watch and Ignore Files with the Reloader -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When using debug mode, the reloader will trigger whenever your Python code or imported -modules change. The reloader can watch additional files with the ``--extra-files`` -option. Multiple paths are separated with ``:``, or ``;`` on Windows. - -.. code-block:: text - - $ flask run --extra-files file1:dirA/file2:dirB/ - * Running on http://127.0.0.1:8000/ - * Detected change in '/path/to/file1', reloading - -The reloader can also ignore files using :mod:`fnmatch` patterns with the -``--exclude-patterns`` option. Multiple patterns are separated with ``:``, or ``;`` on -Windows. - - -Open a Shell ------------- - -To explore the data in your application, you can start an interactive Python -shell with the :func:`shell ` command. An application -context will be active, and the app instance will be imported. :: - - $ flask shell - Python 3.10.0 (default, Oct 27 2021, 06:59:51) [GCC 11.1.0] on linux - App: example [production] - Instance: /home/david/Projects/pallets/flask/instance - >>> - -Use :meth:`~Flask.shell_context_processor` to add other automatic imports. - - -.. _dotenv: - -Environment Variables From dotenv ---------------------------------- - -The ``flask`` command supports setting any option for any command with -environment variables. The variables are named like ``FLASK_OPTION`` or -``FLASK_COMMAND_OPTION``, for example ``FLASK_APP`` or -``FLASK_RUN_PORT``. - -Rather than passing options every time you run a command, or environment -variables every time you open a new terminal, you can use Flask's dotenv -support to set environment variables automatically. - -If `python-dotenv`_ is installed, running the ``flask`` command will set -environment variables defined in the files ``.env`` and ``.flaskenv``. -You can also specify an extra file to load with the ``--env-file`` -option. Dotenv files can be used to avoid having to set ``--app`` or -``FLASK_APP`` manually, and to set configuration using environment -variables similar to how some deployment services work. - -Variables set on the command line are used over those set in :file:`.env`, -which are used over those set in :file:`.flaskenv`. :file:`.flaskenv` should be -used for public variables, such as ``FLASK_APP``, while :file:`.env` should not -be committed to your repository so that it can set private variables. - -Directories are scanned upwards from the directory you call ``flask`` -from to locate the files. - -The files are only loaded by the ``flask`` command or calling -:meth:`~Flask.run`. If you would like to load these files when running in -production, you should call :func:`~cli.load_dotenv` manually. - -.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme - - -Setting Command Options -~~~~~~~~~~~~~~~~~~~~~~~ - -Click is configured to load default values for command options from -environment variables. The variables use the pattern -``FLASK_COMMAND_OPTION``. For example, to set the port for the run -command, instead of ``flask run --port 8000``: - -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_RUN_PORT=8000 - $ flask run - * Running on http://127.0.0.1:8000/ - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_RUN_PORT 8000 - $ flask run - * Running on http://127.0.0.1:8000/ - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_RUN_PORT=8000 - > flask run - * Running on http://127.0.0.1:8000/ - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_RUN_PORT = 8000 - > flask run - * Running on http://127.0.0.1:8000/ - -These can be added to the ``.flaskenv`` file just like ``FLASK_APP`` to -control default command options. - - -Disable dotenv -~~~~~~~~~~~~~~ - -The ``flask`` command will show a message if it detects dotenv files but -python-dotenv is not installed. - -.. code-block:: bash - - $ flask run - * Tip: There are .env files present. Do "pip install python-dotenv" to use them. - -You can tell Flask not to load dotenv files even when python-dotenv is -installed by setting the ``FLASK_SKIP_DOTENV`` environment variable. -This can be useful if you want to load them manually, or if you're using -a project runner that loads them already. Keep in mind that the -environment variables must be set before the app loads or it won't -configure as expected. - -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_SKIP_DOTENV=1 - $ flask run - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_SKIP_DOTENV 1 - $ flask run - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_SKIP_DOTENV=1 - > flask run - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_SKIP_DOTENV = 1 - > flask run - - -Environment Variables From virtualenv -------------------------------------- - -If you do not want to install dotenv support, you can still set environment -variables by adding them to the end of the virtualenv's :file:`activate` -script. Activating the virtualenv will set the variables. - -.. tabs:: - - .. group-tab:: Bash - - Unix Bash, :file:`.venv/bin/activate`:: - - $ export FLASK_APP=hello - - .. group-tab:: Fish - - Fish, :file:`.venv/bin/activate.fish`:: - - $ set -x FLASK_APP hello - - .. group-tab:: CMD - - Windows CMD, :file:`.venv\\Scripts\\activate.bat`:: - - > set FLASK_APP=hello - - .. group-tab:: Powershell - - Windows Powershell, :file:`.venv\\Scripts\\activate.ps1`:: - - > $env:FLASK_APP = "hello" - -It is preferred to use dotenv support over this, since :file:`.flaskenv` can be -committed to the repository so that it works automatically wherever the project -is checked out. - - -Custom Commands ---------------- - -The ``flask`` command is implemented using `Click`_. See that project's -documentation for full information about writing commands. - -This example adds the command ``create-user`` that takes the argument -``name``. :: - - import click - from flask import Flask - - app = Flask(__name__) - - @app.cli.command("create-user") - @click.argument("name") - def create_user(name): - ... - -:: - - $ flask create-user admin - -This example adds the same command, but as ``user create``, a command in a -group. This is useful if you want to organize multiple related commands. :: - - import click - from flask import Flask - from flask.cli import AppGroup - - app = Flask(__name__) - user_cli = AppGroup('user') - - @user_cli.command('create') - @click.argument('name') - def create_user(name): - ... - - app.cli.add_command(user_cli) - -:: - - $ flask user create demo - -See :ref:`testing-cli` for an overview of how to test your custom -commands. - - -Registering Commands with Blueprints -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If your application uses blueprints, you can optionally register CLI -commands directly onto them. When your blueprint is registered onto your -application, the associated commands will be available to the ``flask`` -command. By default, those commands will be nested in a group matching -the name of the blueprint. - -.. code-block:: python - - from flask import Blueprint - - bp = Blueprint('students', __name__) - - @bp.cli.command('create') - @click.argument('name') - def create(name): - ... - - app.register_blueprint(bp) - -.. code-block:: text - - $ flask students create alice - -You can alter the group name by specifying the ``cli_group`` parameter -when creating the :class:`Blueprint` object, or later with -:meth:`app.register_blueprint(bp, cli_group='...') `. -The following are equivalent: - -.. code-block:: python - - bp = Blueprint('students', __name__, cli_group='other') - # or - app.register_blueprint(bp, cli_group='other') - -.. code-block:: text - - $ flask other create alice - -Specifying ``cli_group=None`` will remove the nesting and merge the -commands directly to the application's level: - -.. code-block:: python - - bp = Blueprint('students', __name__, cli_group=None) - # or - app.register_blueprint(bp, cli_group=None) - -.. code-block:: text - - $ flask create alice - - -Application Context -~~~~~~~~~~~~~~~~~~~ - -Commands added using the Flask app's :attr:`~Flask.cli` or -:class:`~flask.cli.FlaskGroup` :meth:`~cli.AppGroup.command` decorator -will be executed with an application context pushed, so your custom -commands and parameters have access to the app and its configuration. The -:func:`~cli.with_appcontext` decorator can be used to get the same -behavior, but is not needed in most cases. - -.. code-block:: python - - import click - from flask.cli import with_appcontext - - @click.command() - @with_appcontext - def do_work(): - ... - - app.cli.add_command(do_work) - - -Plugins -------- - -Flask will automatically load commands specified in the ``flask.commands`` -`entry point`_. This is useful for extensions that want to add commands when -they are installed. Entry points are specified in :file:`pyproject.toml`: - -.. code-block:: toml - - [project.entry-points."flask.commands"] - my-command = "my_extension.commands:cli" - -.. _entry point: https://packaging.python.org/tutorials/packaging-projects/#entry-points - -Inside :file:`my_extension/commands.py` you can then export a Click -object:: - - import click - - @click.command() - def cli(): - ... - -Once that package is installed in the same virtualenv as your Flask project, -you can run ``flask my-command`` to invoke the command. - - -.. _custom-scripts: - -Custom Scripts --------------- - -When you are using the app factory pattern, it may be more convenient to define -your own Click script. Instead of using ``--app`` and letting Flask load -your application, you can create your own Click object and export it as a -`console script`_ entry point. - -Create an instance of :class:`~cli.FlaskGroup` and pass it the factory:: - - import click - from flask import Flask - from flask.cli import FlaskGroup - - def create_app(): - app = Flask('wiki') - # other setup - return app - - @click.group(cls=FlaskGroup, create_app=create_app) - def cli(): - """Management script for the Wiki application.""" - -Define the entry point in :file:`pyproject.toml`: - -.. code-block:: toml - - [project.scripts] - wiki = "wiki:cli" - -Install the application in the virtualenv in editable mode and the custom -script is available. Note that you don't need to set ``--app``. :: - - $ pip install -e . - $ wiki run - -.. admonition:: Errors in Custom Scripts - - When using a custom script, if you introduce an error in your - module-level code, the reloader will fail because it can no longer - load the entry point. - - The ``flask`` command, being separate from your code, does not have - this issue and is recommended in most cases. - -.. _console script: https://packaging.python.org/tutorials/packaging-projects/#console-scripts - - -PyCharm Integration -------------------- - -PyCharm Professional provides a special Flask run configuration to run the development -server. For the Community Edition, and for other commands besides ``run``, you need to -create a custom run configuration. These instructions should be similar for any other -IDE you use. - -In PyCharm, with your project open, click on *Run* from the menu bar and go to *Edit -Configurations*. You'll see a screen similar to this: - -.. image:: _static/pycharm-run-config.png - :align: center - :class: screenshot - :alt: Screenshot of PyCharm run configuration. - -Once you create a configuration for the ``flask run``, you can copy and change it to -call any other command. - -Click the *+ (Add New Configuration)* button and select *Python*. Give the configuration -a name such as "flask run". - -Click the *Script path* dropdown and change it to *Module name*, then input ``flask``. - -The *Parameters* field is set to the CLI command to execute along with any arguments. -This example uses ``--app hello run --debug``, which will run the development server in -debug mode. ``--app hello`` should be the import or file with your Flask app. - -If you installed your project as a package in your virtualenv, you may uncheck the -*PYTHONPATH* options. This will more accurately match how you deploy later. - -Click *OK* to save and close the configuration. Select the configuration in the main -PyCharm window and click the play button next to it to run the server. - -Now that you have a configuration for ``flask run``, you can copy that configuration and -change the *Parameters* argument to run a different CLI command. diff --git a/docs/conf.py b/docs/conf.py index af8adf01..16d7e670 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,101 +1,269 @@ -import packaging.version -from pallets_sphinx_themes import get_version -from pallets_sphinx_themes import ProjectLink +# -*- coding: utf-8 -*- +# +# Flask documentation build configuration file, created by +# sphinx-quickstart on Tue Apr 6 15:24:58 2010. +# +# 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. -# Project -------------------------------------------------------------- +import sys, os -project = "Flask" -copyright = "2010 Pallets" -author = "Pallets" -release, version = get_version("Flask") +# 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. +sys.path.append(os.path.abspath('_themes')) +sys.path.append(os.path.abspath('.')) -# General -------------------------------------------------------------- +# -- General configuration ----------------------------------------------------- -default_role = "code" -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.extlinks", - "sphinx.ext.intersphinx", - "sphinxcontrib.log_cabinet", - "sphinx_tabs.tabs", - "pallets_sphinx_themes", -] -autodoc_member_order = "bysource" -autodoc_typehints = "description" -autodoc_preserve_defaults = True -extlinks = { - "issue": ("https://github.com/pallets/flask/issues/%s", "#%s"), - "pr": ("https://github.com/pallets/flask/pull/%s", "#%s"), - "ghsa": ("https://github.com/pallets/flask/security/advisories/GHSA-%s", "GHSA-%s"), -} -intersphinx_mapping = { - "python": ("https://docs.python.org/3/", None), - "werkzeug": ("https://werkzeug.palletsprojects.com/", None), - "click": ("https://click.palletsprojects.com/", None), - "jinja": ("https://jinja.palletsprojects.com/", None), - "itsdangerous": ("https://itsdangerous.palletsprojects.com/", None), - "sqlalchemy": ("https://docs.sqlalchemy.org/", None), - "wtforms": ("https://wtforms.readthedocs.io/", None), - "blinker": ("https://blinker.readthedocs.io/", None), +# 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.intersphinx', + 'flaskdocext'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Flask' +copyright = u'2010, Armin Ronacher' + +# 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. +import pkg_resources +try: + release = pkg_resources.get_distribution('Flask').version +except pkg_resources.DistributionNotFound: + print 'To build the documentation, The distribution information of Flask' + print 'Has to be available. Either install the package into your' + print 'development environment or run "setup.py develop" to setup the' + print 'metadata. A virtualenv is recommended!' + sys.exit(1) +del pkg_resources + +if 'dev' in release: + release = release.split('dev')[0] + 'dev' +version = '.'.join(release.split('.')[:2]) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +html_theme = 'flask' + +# 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 = { + 'touch_icon': 'touch-icon.png' } -# HTML ----------------------------------------------------------------- +# Add any paths that contain custom themes here, relative to this directory. +html_theme_path = ['_themes'] -html_theme = "flask" -html_theme_options = {"index_sidebar_logo": False} -html_context = { - "project_links": [ - ProjectLink("Donate", "https://palletsprojects.com/donate"), - ProjectLink("PyPI Releases", "https://pypi.org/project/Flask/"), - ProjectLink("Source Code", "https://github.com/pallets/flask/"), - ProjectLink("Issue Tracker", "https://github.com/pallets/flask/issues/"), - ProjectLink("Chat", "https://discord.gg/pallets"), - ] -} +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. Do not set, template magic! +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# 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'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. html_sidebars = { - "index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"], - "**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"], + 'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'], + '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', + 'sourcelink.html', 'searchbox.html'] } -singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]} -html_static_path = ["_static"] -html_favicon = "_static/flask-icon.svg" -html_logo = "_static/flask-logo.svg" -html_title = f"Flask Documentation ({version})" -html_show_sourcelink = False -gettext_uuid = True -gettext_compact = False +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} -# Local Extensions ----------------------------------------------------- +# If false, no module index is generated. +html_use_modindex = False + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +html_show_sphinx = False + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Flaskdoc' -def github_link(name, rawtext, text, lineno, inliner, options=None, content=None): - app = inliner.document.settings.env.app - release = app.config.release - base_url = "https://github.com/pallets/flask/tree/" +# -- Options for LaTeX output -------------------------------------------------- - if text.endswith(">"): - words, text = text[:-1].rsplit("<", 1) - words = words.strip() - else: - words = None +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('latexindex', 'Flask.tex', u'Flask Documentation', + u'Armin Ronacher', 'manual'), +] - if packaging.version.parse(release).is_devrelease: - url = f"{base_url}main/{text}" - else: - url = f"{base_url}{release}/{text}" +# Documents to append as an appendix to all manuals. +#latex_appendices = [] - if words is None: - words = url +# If false, no module index is generated. +latex_use_modindex = False - from docutils.nodes import reference - from docutils.parsers.rst.roles import set_classes +latex_elements = { + 'fontpkg': r'\usepackage{mathpazo}', + 'papersize': 'a4paper', + 'pointsize': '12pt', + 'preamble': r'\usepackage{flaskstyle}' +} +latex_use_parts = True - options = options or {} - set_classes(options) - node = reference(rawtext, words, refuri=url, **options) - return [node], [] +latex_additional_files = ['flaskstyle.sty', 'logo.pdf'] -def setup(app): - app.add_role("gh", github_link) +# -- Options for Epub output --------------------------------------------------- + +# Bibliographic Dublin Core info. +#epub_title = '' +#epub_author = '' +#epub_publisher = '' +#epub_copyright = '' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +#epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +intersphinx_mapping = { + 'http://docs.python.org/dev': None, + 'http://werkzeug.pocoo.org/docs/': None, + 'http://www.sqlalchemy.org/docs/': None, + 'http://wtforms.simplecodes.com/docs/0.5/': None, + 'http://discorporate.us/projects/Blinker/docs/1.1/': None +} + +pygments_style = 'flask_theme_support.FlaskyStyle' + +# fall back if theme is not there +try: + __import__('flask_theme_support') +except ImportError, e: + print '-' * 74 + print 'Warning: Flask themes unavailable. Building with default theme' + print 'If you want the Flask themes, run this command and build again:' + print + print ' git submodule update --init' + print '-' * 74 + + pygments_style = 'tango' + html_theme = 'default' + html_theme_options = {} diff --git a/docs/config.rst b/docs/config.rst index 7138222d..ca724dce 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1,13 +1,17 @@ +.. _config: + Configuration Handling ====================== +.. versionadded:: 0.3 + Applications need some kind of configuration. There are different settings you might want to change depending on the application environment like toggling the debug mode, setting the secret key, and other such environment-specific things. The way Flask is designed usually requires the configuration to be -available when the application starts up. You can hard code the +available when the application starts up. You can hardcode the configuration in the code, which for many small applications is not actually that bad, but there are better ways. @@ -18,7 +22,6 @@ object. This is the place where Flask itself puts certain configuration values and also where extensions can put their configuration values. But this is also where you can have your own configuration. - Configuration Basics -------------------- @@ -26,373 +29,123 @@ The :attr:`~flask.Flask.config` is actually a subclass of a dictionary and can be modified just like any dictionary:: app = Flask(__name__) - app.config['TESTING'] = True + app.config['DEBUG'] = True Certain configuration values are also forwarded to the :attr:`~flask.Flask` object so you can read and write them from there:: - app.testing = True + app.debug = True To update multiple keys at once you can use the :meth:`dict.update` method:: app.config.update( - TESTING=True, - SECRET_KEY='192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' + DEBUG=True, + SECRET_KEY='...' ) - -Debug Mode ----------- - -The :data:`DEBUG` config value is special because it may behave inconsistently if -changed after the app has begun setting up. In order to set debug mode reliably, use the -``--debug`` option on the ``flask`` or ``flask run`` command. ``flask run`` will use the -interactive debugger and reloader by default in debug mode. - -.. code-block:: text - - $ flask --app hello run --debug - -Using the option is recommended. While it is possible to set :data:`DEBUG` in your -config or code, this is strongly discouraged. It can't be read early by the -``flask run`` command, and some systems or extensions may have already configured -themselves based on a previous value. - - Builtin Configuration Values ---------------------------- The following configuration values are used internally by Flask: -.. py:data:: DEBUG - - Whether debug mode is enabled. When using ``flask run`` to start the development - server, an interactive debugger will be shown for unhandled exceptions, and the - server will be reloaded when code changes. The :attr:`~flask.Flask.debug` attribute - maps to this config key. This is set with the ``FLASK_DEBUG`` environment variable. - It may not behave as expected if set in code. - - **Do not enable debug mode when deploying in production.** - - Default: ``False`` - -.. py:data:: TESTING - - Enable testing mode. Exceptions are propagated rather than handled by - the app's error handlers. Extensions may also change their behavior to - facilitate easier testing. You should enable this in your own tests. - - Default: ``False`` - -.. py:data:: PROPAGATE_EXCEPTIONS - - Exceptions are re-raised rather than being handled by the app's error - handlers. If not set, this is implicitly true if ``TESTING`` or ``DEBUG`` - is enabled. - - Default: ``None`` - -.. py:data:: TRAP_HTTP_EXCEPTIONS - - If there is no handler for an ``HTTPException``-type exception, re-raise it - to be handled by the interactive debugger instead of returning it as a - simple error response. - - Default: ``False`` - -.. py:data:: TRAP_BAD_REQUEST_ERRORS - - Trying to access a key that doesn't exist from request dicts like ``args`` - and ``form`` will return a 400 Bad Request error page. Enable this to treat - the error as an unhandled exception instead so that you get the interactive - debugger. This is a more specific version of ``TRAP_HTTP_EXCEPTIONS``. If - unset, it is enabled in debug mode. - - Default: ``None`` - -.. py:data:: SECRET_KEY - - A secret key that will be used for securely signing the session cookie - and can be used for any other security related needs by extensions or your - application. It should be a long random ``bytes`` or ``str``. For - example, copy the output of this to your config:: - - $ python -c 'import secrets; print(secrets.token_hex())' - '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' - - **Do not reveal the secret key when posting questions or committing code.** - - Default: ``None`` - -.. py:data:: SECRET_KEY_FALLBACKS - - A list of old secret keys that can still be used for unsigning. This allows - a project to implement key rotation without invalidating active sessions or - other recently-signed secrets. - - Keys should be removed after an appropriate period of time, as checking each - additional key adds some overhead. - - Order should not matter, but the default implementation will test the last - key in the list first, so it might make sense to order oldest to newest. - - Flask's built-in secure cookie session supports this. Extensions that use - :data:`SECRET_KEY` may not support this yet. - - Default: ``None`` - - .. versionadded:: 3.1 - -.. py:data:: SESSION_COOKIE_NAME - - The name of the session cookie. Can be changed in case you already have a - cookie with the same name. - - Default: ``'session'`` - -.. py:data:: SESSION_COOKIE_DOMAIN - - The value of the ``Domain`` parameter on the session cookie. If not set, browsers - will only send the cookie to the exact domain it was set from. Otherwise, they - will send it to any subdomain of the given value as well. - - Not setting this value is more restricted and secure than setting it. - - Default: ``None`` - - .. warning:: - If this is changed after the browser created a cookie is created with - one setting, it may result in another being created. Browsers may send - send both in an undefined order. In that case, you may want to change - :data:`SESSION_COOKIE_NAME` as well or otherwise invalidate old sessions. - - .. versionchanged:: 2.3 - Not set by default, does not fall back to ``SERVER_NAME``. - -.. py:data:: SESSION_COOKIE_PATH - - The path that the session cookie will be valid for. If not set, the cookie - will be valid underneath ``APPLICATION_ROOT`` or ``/`` if that is not set. - - Default: ``None`` - -.. py:data:: SESSION_COOKIE_HTTPONLY - - Browsers will not allow JavaScript access to cookies marked as "HTTP only" - for security. - - Default: ``True`` - -.. py:data:: SESSION_COOKIE_SECURE - - Browsers will only send cookies with requests over HTTPS if the cookie is - marked "secure". The application must be served over HTTPS for this to make - sense. - - Default: ``False`` - -.. py:data:: SESSION_COOKIE_PARTITIONED - - Browsers will send cookies based on the top-level document's domain, rather - than only the domain of the document setting the cookie. This prevents third - party cookies set in iframes from "leaking" between separate sites. - - Browsers are beginning to disallow non-partitioned third party cookies, so - you need to mark your cookies partitioned if you expect them to work in such - embedded situations. - - Enabling this implicitly enables :data:`SESSION_COOKIE_SECURE` as well, as - it is only valid when served over HTTPS. - - Default: ``False`` - - .. versionadded:: 3.1 - -.. py:data:: SESSION_COOKIE_SAMESITE - - Restrict how cookies are sent with requests from external sites. Can - be set to ``'Lax'`` (recommended) or ``'Strict'``. - See :ref:`security-cookie`. - - Default: ``None`` - - .. versionadded:: 1.0 - -.. py:data:: PERMANENT_SESSION_LIFETIME - - If ``session.permanent`` is true, the cookie's expiration will be set this - number of seconds in the future. Can either be a - :class:`datetime.timedelta` or an ``int``. - - Flask's default cookie implementation validates that the cryptographic - signature is not older than this value. - - Default: ``timedelta(days=31)`` (``2678400`` seconds) - -.. py:data:: SESSION_REFRESH_EACH_REQUEST - - Control whether the cookie is sent with every response when - ``session.permanent`` is true. Sending the cookie every time (the default) - can more reliably keep the session from expiring, but uses more bandwidth. - Non-permanent sessions are not affected. - - Default: ``True`` - -.. py:data:: USE_X_SENDFILE - - When serving files, set the ``X-Sendfile`` header instead of serving the - data with Flask. Some web servers, such as Apache, recognize this and serve - the data more efficiently. This only makes sense when using such a server. - - Default: ``False`` - -.. py:data:: SEND_FILE_MAX_AGE_DEFAULT - - When serving files, set the cache control max age to this number of - seconds. Can be a :class:`datetime.timedelta` or an ``int``. - Override this value on a per-file basis using - :meth:`~flask.Flask.get_send_file_max_age` on the application or - blueprint. - - If ``None``, ``send_file`` tells the browser to use conditional - requests will be used instead of a timed cache, which is usually - preferable. - - Default: ``None`` - -.. py:data:: TRUSTED_HOSTS - - Validate :attr:`.Request.host` and other attributes that use it against - these trusted values. Raise a :exc:`~werkzeug.exceptions.SecurityError` if - the host is invalid, which results in a 400 error. If it is ``None``, all - hosts are valid. Each value is either an exact match, or can start with - a dot ``.`` to match any subdomain. - - Validation is done during routing against this value. ``before_request`` and - ``after_request`` callbacks will still be called. - - Default: ``None`` - - .. versionadded:: 3.1 - -.. py:data:: SERVER_NAME - - Inform the application what host and port it is bound to. - - Must be set if ``subdomain_matching`` is enabled, to be able to extract the - subdomain from the request. - - Must be set for ``url_for`` to generate external URLs outside of a - request context. - - Default: ``None`` - - .. versionchanged:: 3.1 - Does not restrict requests to only this domain, for both - ``subdomain_matching`` and ``host_matching``. - - .. versionchanged:: 1.0 - Does not implicitly enable ``subdomain_matching``. - - .. versionchanged:: 2.3 - Does not affect ``SESSION_COOKIE_DOMAIN``. - -.. py:data:: APPLICATION_ROOT - - Inform the application what path it is mounted under by the application / - web server. This is used for generating URLs outside the context of a - request (inside a request, the dispatcher is responsible for setting - ``SCRIPT_NAME`` instead; see :doc:`/patterns/appdispatch` - for examples of dispatch configuration). - - Will be used for the session cookie path if ``SESSION_COOKIE_PATH`` is not - set. - - Default: ``'/'`` - -.. py:data:: PREFERRED_URL_SCHEME - - Use this scheme for generating external URLs when not in a request context. - - Default: ``'http'`` - -.. py:data:: MAX_CONTENT_LENGTH - - The maximum number of bytes that will be read during this request. If - this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge` - error is raised. If it is set to ``None``, no limit is enforced at the - Flask application level. However, if it is ``None`` and the request has no - ``Content-Length`` header and the WSGI server does not indicate that it - terminates the stream, then no data is read to avoid an infinite stream. - - Each request defaults to this config. It can be set on a specific - :attr:`.Request.max_content_length` to apply the limit to that specific - view. This should be set appropriately based on an application's or view's - specific needs. - - Default: ``None`` - - .. versionadded:: 0.6 - -.. py:data:: MAX_FORM_MEMORY_SIZE - - The maximum size in bytes any non-file form field may be in a - ``multipart/form-data`` body. If this limit is exceeded, a 413 - :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it is - set to ``None``, no limit is enforced at the Flask application level. - - Each request defaults to this config. It can be set on a specific - :attr:`.Request.max_form_memory_parts` to apply the limit to that specific - view. This should be set appropriately based on an application's or view's - specific needs. - - Default: ``500_000`` - - .. versionadded:: 3.1 - -.. py:data:: MAX_FORM_PARTS - - The maximum number of fields that may be present in a - ``multipart/form-data`` body. If this limit is exceeded, a 413 - :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it - is set to ``None``, no limit is enforced at the Flask application level. - - Each request defaults to this config. It can be set on a specific - :attr:`.Request.max_form_parts` to apply the limit to that specific view. - This should be set appropriately based on an application's or view's - specific needs. - - Default: ``1_000`` - - .. versionadded:: 3.1 - -.. py:data:: TEMPLATES_AUTO_RELOAD - - Reload templates when they are changed. If not set, it will be enabled in - debug mode. - - Default: ``None`` - -.. py:data:: EXPLAIN_TEMPLATE_LOADING - - Log debugging information tracing how a template file was loaded. This can - be useful to figure out why a template was not loaded or the wrong file - appears to be loaded. - - Default: ``False`` - -.. py:data:: MAX_COOKIE_SIZE - - Warn if cookie headers are larger than this many bytes. Defaults to - ``4093``. Larger cookies may be silently ignored by browsers. Set to - ``0`` to disable the warning. - -.. py:data:: PROVIDE_AUTOMATIC_OPTIONS - - Set to ``False`` to disable the automatic addition of OPTIONS - responses. This can be overridden per route by altering the - ``provide_automatic_options`` attribute. +.. tabularcolumns:: |p{6.5cm}|p{8.5cm}| + +================================= ========================================= +``DEBUG`` enable/disable debug mode +``TESTING`` enable/disable testing mode +``PROPAGATE_EXCEPTIONS`` explicitly enable or disable the + propagation of exceptions. If not set or + explicitly set to `None` this is + implicitly true if either `TESTING` or + `DEBUG` is true. +``PRESERVE_CONTEXT_ON_EXCEPTION`` By default if the application is in + debug mode the request context is not + popped on exceptions to enable debuggers + to introspect the data. This can be + disabled by this key. You can also use + this setting to force-enable it for non + debug execution which might be useful to + debug production applications (but also + very risky). +``SECRET_KEY`` the secret key +``SESSION_COOKIE_NAME`` the name of the session cookie +``SESSION_COOKIE_DOMAIN`` the domain for the session cookie. If + this is not set, the cookie will be + valid for all subdomains of + ``SERVER_NAME``. +``SESSION_COOKIE_PATH`` the path for the session cookie. If + this is not set the cookie will be valid + for all of ``APPLICATION_ROOT`` or if + that is not set for ``'/'``. +``SESSION_COOKIE_HTTPONLY`` controls if the cookie should be set + with the httponly flag. Defaults to + `True`. +``SESSION_COOKIE_SECURE`` controls if the cookie should be set + with the secure flag. Defaults to + `False`. +``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as + :class:`datetime.timedelta` object. + Starting with Flask 0.8 this can also be + an integer representing seconds. +``USE_X_SENDFILE`` enable/disable x-sendfile +``LOGGER_NAME`` the name of the logger +``SERVER_NAME`` the name and port number of the server. + Required for subdomain support (e.g.: + ``'myapp.dev:5000'``) Note that + localhost does not support subdomains so + setting this to “localhost” does not + help. +``APPLICATION_ROOT`` If the application does not occupy + a whole domain or subdomain this can + be set to the path where the application + is configured to live. This is for + session cookie as path value. If + domains are used, this should be + ``None``. +``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will + reject incoming requests with a + content length greater than this by + returning a 413 status code. +``TRAP_HTTP_EXCEPTIONS`` If this is set to ``True`` Flask will + not execute the error handlers of HTTP + exceptions but instead treat the + exception like any other and bubble it + through the exception stack. This is + helpful for hairy debugging situations + where you have to find out where an HTTP + exception is coming from. +``TRAP_BAD_REQUEST_ERRORS`` Werkzeug's internal data structures that + deal with request specific data will + raise special key errors that are also + bad request exceptions. Likewise many + operations can implicitly fail with a + BadRequest exception for consistency. + Since it's nice for debugging to know + why exactly it failed this flag can be + used to debug those situations. If this + config is set to ``True`` you will get + a regular traceback instead. +================================= ========================================= + +.. admonition:: More on ``SERVER_NAME`` + + The ``SERVER_NAME`` key is used for the subdomain support. Because + Flask cannot guess the subdomain part without the knowledge of the + actual server name, this is required if you want to work with + subdomains. This is also used for the session cookie. + + Please keep in mind that not only Flask has the problem of not knowing + what subdomains are, your web browser does as well. Most modern web + browsers will not allow cross-subdomain cookies to be set on a + server name without dots in it. So if your server name is + ``'localhost'`` you will not be able to set a cookie for + ``'localhost'`` and every subdomain of it. Please chose a different + server name in that case, like ``'myapplication.local'`` and add + this name + the subdomains you want to use into your host config + or setup a local `bind`_. + +.. _bind: https://www.isc.org/software/bind .. versionadded:: 0.4 ``LOGGER_NAME`` @@ -412,52 +165,16 @@ The following configuration values are used internally by Flask: ``SESSION_COOKIE_PATH``, ``SESSION_COOKIE_HTTPONLY``, ``SESSION_COOKIE_SECURE`` -.. versionadded:: 0.9 - ``PREFERRED_URL_SCHEME`` +Configuring from Files +---------------------- -.. versionadded:: 0.10 - ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_PRETTYPRINT_REGULAR`` +Configuration becomes more useful if you can store it in a separate file, +ideally located outside the actual application package. This makes +packaging and distributing your application possible via various package +handling tools (:ref:`distribute-deployment`) and finally modifying the +configuration file afterwards. -.. versionadded:: 0.11 - ``SESSION_REFRESH_EACH_REQUEST``, ``TEMPLATES_AUTO_RELOAD``, - ``LOGGER_HANDLER_POLICY``, ``EXPLAIN_TEMPLATE_LOADING`` - -.. versionchanged:: 1.0 - ``LOGGER_NAME`` and ``LOGGER_HANDLER_POLICY`` were removed. See - :doc:`/logging` for information about configuration. - - Added :data:`ENV` to reflect the :envvar:`FLASK_ENV` environment - variable. - - Added :data:`SESSION_COOKIE_SAMESITE` to control the session - cookie's ``SameSite`` option. - - Added :data:`MAX_COOKIE_SIZE` to control a warning from Werkzeug. - -.. versionchanged:: 2.2 - Removed ``PRESERVE_CONTEXT_ON_EXCEPTION``. - -.. versionchanged:: 2.3 - ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_MIMETYPE``, and - ``JSONIFY_PRETTYPRINT_REGULAR`` were removed. The default ``app.json`` provider has - equivalent attributes instead. - -.. versionchanged:: 2.3 - ``ENV`` was removed. - -.. versionadded:: 3.1 - Added :data:`PROVIDE_AUTOMATIC_OPTIONS` to control the default - addition of autogenerated OPTIONS responses. - - -Configuring from Python Files ------------------------------ - -Configuration becomes more useful if you can store it in a separate file, ideally -located outside the actual application package. You can deploy your application, then -separately configure it for the specific deployment. - -A common pattern is this:: +So a common pattern is this:: app = Flask(__name__) app.config.from_object('yourapplication.default_settings') @@ -466,42 +183,18 @@ A common pattern is this:: This first loads the configuration from the `yourapplication.default_settings` module and then overrides the values with the contents of the file the :envvar:`YOURAPPLICATION_SETTINGS` -environment variable points to. This environment variable can be set -in the shell before starting the server: +environment variable points to. This environment variable can be set on +Linux or OS X with the export command in the shell before starting the +server:: -.. tabs:: + $ export YOURAPPLICATION_SETTINGS=/path/to/settings.cfg + $ python run-app.py + * Running on http://127.0.0.1:5000/ + * Restarting with reloader... - .. group-tab:: Bash +On Windows systems use the `set` builtin instead:: - .. code-block:: text - - $ export YOURAPPLICATION_SETTINGS=/path/to/settings.cfg - $ flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x YOURAPPLICATION_SETTINGS /path/to/settings.cfg - $ flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: CMD - - .. code-block:: text - - > set YOURAPPLICATION_SETTINGS=\path\to\settings.cfg - > flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:YOURAPPLICATION_SETTINGS = "\path\to\settings.cfg" - > flask run - * Running on http://127.0.0.1:5000/ + >set YOURAPPLICATION_SETTINGS=\path\to\settings.cfg The configuration files themselves are actual Python files. Only values in uppercase are actually stored in the config object later on. So make @@ -510,7 +203,8 @@ sure to use uppercase letters for your config keys. Here is an example of a configuration file:: # Example configuration - SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' + DEBUG = False + SECRET_KEY = '?\xbf,\xb4\x8d\xa3"<\x9c\xb0@\x0f5\xab,w\xee\x8d$0\x13\x8b83' Make sure to load the configuration very early on, so that extensions have the ability to access the configuration when starting up. There are other @@ -519,118 +213,6 @@ complete reference, read the :class:`~flask.Config` object's documentation. -Configuring from Data Files ---------------------------- - -It is also possible to load configuration from a file in a format of -your choice using :meth:`~flask.Config.from_file`. For example to load -from a TOML file: - -.. code-block:: python - - import tomllib - app.config.from_file("config.toml", load=tomllib.load, text=False) - -Or from a JSON file: - -.. code-block:: python - - import json - app.config.from_file("config.json", load=json.load) - - -Configuring from Environment Variables --------------------------------------- - -In addition to pointing to configuration files using environment -variables, you may find it useful (or necessary) to control your -configuration values directly from the environment. Flask can be -instructed to load all environment variables starting with a specific -prefix into the config using :meth:`~flask.Config.from_prefixed_env`. - -Environment variables can be set in the shell before starting the -server: - -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_SECRET_KEY="5f352379324c22463451387a0aec5d2f" - $ export FLASK_MAIL_ENABLED=false - $ flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_SECRET_KEY "5f352379324c22463451387a0aec5d2f" - $ set -x FLASK_MAIL_ENABLED false - $ flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_SECRET_KEY="5f352379324c22463451387a0aec5d2f" - > set FLASK_MAIL_ENABLED=false - > flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_SECRET_KEY = "5f352379324c22463451387a0aec5d2f" - > $env:FLASK_MAIL_ENABLED = "false" - > flask run - * Running on http://127.0.0.1:5000/ - -The variables can then be loaded and accessed via the config with a key -equal to the environment variable name without the prefix i.e. - -.. code-block:: python - - app.config.from_prefixed_env() - app.config["SECRET_KEY"] # Is "5f352379324c22463451387a0aec5d2f" - -The prefix is ``FLASK_`` by default. This is configurable via the -``prefix`` argument of :meth:`~flask.Config.from_prefixed_env`. - -Values will be parsed to attempt to convert them to a more specific type -than strings. By default :func:`json.loads` is used, so any valid JSON -value is possible, including lists and dicts. This is configurable via -the ``loads`` argument of :meth:`~flask.Config.from_prefixed_env`. - -When adding a boolean value with the default JSON parsing, only "true" -and "false", lowercase, are valid values. Keep in mind that any -non-empty string is considered ``True`` by Python. - -It is possible to set keys in nested dictionaries by separating the -keys with double underscore (``__``). Any intermediate keys that don't -exist on the parent dict will be initialized to an empty dict. - -.. code-block:: text - - $ export FLASK_MYAPI__credentials__username=user123 - -.. code-block:: python - - app.config["MYAPI"]["credentials"]["username"] # Is "user123" - -On Windows, environment variable keys are always uppercase, therefore -the above example would end up as ``MYAPI__CREDENTIALS__USERNAME``. - -For even more config loading features, including merging and -case-insensitive Windows support, try a dedicated library such as -Dynaconf_, which includes integration with Flask. - -.. _Dynaconf: https://www.dynaconf.com/ - - Configuration Best Practices ---------------------------- @@ -639,20 +221,15 @@ a little harder. There is no single 100% solution for this problem in general, but there are a couple of things you can keep in mind to improve that experience: -1. Create your application in a function and register blueprints on it. +1. create your application in a function and register blueprints on it. That way you can create multiple instances of your application with - different configurations attached which makes unit testing a lot + different configurations attached which makes unittesting a lot easier. You can use this to pass in configuration as needed. 2. Do not write code that needs the configuration at import time. If you limit yourself to request-only accesses to the configuration you can reconfigure the object later on as needed. -3. Make sure to load the configuration very early on, so that - extensions can access the configuration when calling ``init_app``. - - -.. _config-dev-prod: Development / Production ------------------------ @@ -668,32 +245,33 @@ in the example above:: app.config.from_object('yourapplication.default_settings') app.config.from_envvar('YOURAPPLICATION_SETTINGS') -Then you just have to add a separate :file:`config.py` file and export +Then you just have to add a separate `config.py` file and export ``YOURAPPLICATION_SETTINGS=/path/to/config.py`` and you are done. However there are alternative ways as well. For example you could use imports or subclassing. What is very popular in the Django world is to make the import explicit in -the config file by adding ``from yourapplication.default_settings +the config file by adding an ``from yourapplication.default_settings import *`` to the top of the file and then overriding the changes by hand. You could also inspect an environment variable like ``YOURAPPLICATION_MODE`` and set that to `production`, `development` etc -and import different hard-coded files based on that. +and import different hardcoded files based on that. An interesting pattern is also to use classes and inheritance for configuration:: class Config(object): + DEBUG = False TESTING = False + DATABASE_URI = 'sqlite://:memory:' class ProductionConfig(Config): DATABASE_URI = 'mysql://user@localhost/foo' - + class DevelopmentConfig(Config): - DATABASE_URI = "sqlite:////tmp/foo.db" + DEBUG = True class TestingConfig(Config): - DATABASE_URI = 'sqlite:///:memory:' TESTING = True To enable such a config you just have to call into @@ -701,58 +279,25 @@ To enable such a config you just have to call into app.config.from_object('configmodule.ProductionConfig') -Note that :meth:`~flask.Config.from_object` does not instantiate the class -object. If you need to instantiate the class, such as to access a property, -then you must do so before calling :meth:`~flask.Config.from_object`:: - - from configmodule import ProductionConfig - app.config.from_object(ProductionConfig()) - - # Alternatively, import via string: - from werkzeug.utils import import_string - cfg = import_string('configmodule.ProductionConfig')() - app.config.from_object(cfg) - -Instantiating the configuration object allows you to use ``@property`` in -your configuration classes:: - - class Config(object): - """Base config, uses staging database server.""" - TESTING = False - DB_SERVER = '192.168.1.56' - - @property - def DATABASE_URI(self): # Note: all caps - return f"mysql://user@{self.DB_SERVER}/foo" - - class ProductionConfig(Config): - """Uses production database server.""" - DB_SERVER = '192.168.19.32' - - class DevelopmentConfig(Config): - DB_SERVER = 'localhost' - - class TestingConfig(Config): - DB_SERVER = 'localhost' - DATABASE_URI = 'sqlite:///:memory:' - There are many different ways and it's up to you how you want to manage your configuration files. However here a list of good recommendations: -- Keep a default configuration in version control. Either populate the +- keep a default configuration in version control. Either populate the config with this default configuration or import it in your own configuration files before overriding values. -- Use an environment variable to switch between the configurations. +- use an environment variable to switch between the configurations. This can be done from outside the Python interpreter and makes development and deployment much easier because you can quickly and easily switch between different configs without having to touch the code at all. If you are working often on different projects you can even create your own script for sourcing that activates a virtualenv and exports the development configuration for you. -- Use a tool like `fabric`_ to push code and configuration separately - to the production server(s). +- Use a tool like `fabric`_ in production to push code and + configurations separately to the production server(s). For some + details about how to do that, head over to the + :ref:`fabric-deployment` pattern. -.. _fabric: https://www.fabfile.org/ +.. _fabric: http://fabfile.org/ .. _instance-folders: @@ -800,7 +345,7 @@ locations are used: - Installed module or package:: - $PREFIX/lib/pythonX.Y/site-packages/myapp + $PREFIX/lib/python2.X/site-packages/myapp $PREFIX/var/myapp-instance ``$PREFIX`` is the prefix of your Python installation. This can be @@ -817,7 +362,7 @@ root” (the default) to “relative to instance folder” via the app = Flask(__name__, instance_relative_config=True) Here is a full example of how to configure Flask to preload the config -from a module and then override the config from a file in the instance +from a module and then override the config from a file in the config folder if it exists:: app = Flask(__name__, instance_relative_config=True) @@ -830,7 +375,7 @@ file from the instance folder with :meth:`Flask.open_instance_resource`. Example usage for both:: - filename = os.path.join(app.instance_path, 'application.cfg') + filename = os.path.join(app.instance_root, 'application.cfg') with open(filename) as f: config = f.read() diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc new file mode 100644 index 00000000..a8ebc0d7 --- /dev/null +++ b/docs/contents.rst.inc @@ -0,0 +1,56 @@ +User's Guide +------------ + +This part of the documentation, which is mostly prose, begins with some +background information about Flask, then focuses on step-by-step +instructions for web development with Flask. + +.. toctree:: + :maxdepth: 2 + + foreword + installation + quickstart + tutorial/index + templating + testing + errorhandling + config + signals + views + reqcontext + blueprints + extensions + shell + patterns/index + deploying/index + becomingbig + +API Reference +------------- + +If you are looking for information on a specific function, class or +method, this part of the documentation is for you. + +.. toctree:: + :maxdepth: 2 + + api + +Additional Notes +---------------- + +Design notes, legal information and changelog are here for the interested. + +.. toctree:: + :maxdepth: 2 + + design + htmlfaq + security + unicode + extensiondev + styleguide + upgrading + changelog + license diff --git a/docs/contributing.rst b/docs/contributing.rst deleted file mode 100644 index ca4b3aee..00000000 --- a/docs/contributing.rst +++ /dev/null @@ -1,8 +0,0 @@ -Contributing -============ - -See the Pallets `detailed contributing documentation `_ for many ways -to contribute, including reporting issues, requesting features, asking or -answering questions, and making PRs. - -.. _contrib: https://palletsprojects.com/contributing/ diff --git a/docs/debugging.rst b/docs/debugging.rst deleted file mode 100644 index f6b56cab..00000000 --- a/docs/debugging.rst +++ /dev/null @@ -1,99 +0,0 @@ -Debugging Application Errors -============================ - - -In Production -------------- - -**Do not run the development server, or enable the built-in debugger, in -a production environment.** The debugger allows executing arbitrary -Python code from the browser. It's protected by a pin, but that should -not be relied on for security. - -Use an error logging tool, such as Sentry, as described in -:ref:`error-logging-tools`, or enable logging and notifications as -described in :doc:`/logging`. - -If you have access to the server, you could add some code to start an -external debugger if ``request.remote_addr`` matches your IP. Some IDE -debuggers also have a remote mode so breakpoints on the server can be -interacted with locally. Only enable a debugger temporarily. - - -The Built-In Debugger ---------------------- - -The built-in Werkzeug development server provides a debugger which shows -an interactive traceback in the browser when an unhandled error occurs -during a request. This debugger should only be used during development. - -.. image:: _static/debugger.png - :align: center - :class: screenshot - :alt: screenshot of debugger in action - -.. warning:: - - The debugger allows executing arbitrary Python code from the - browser. It is protected by a pin, but still represents a major - security risk. Do not run the development server or debugger in a - production environment. - -The debugger is enabled by default when the development server is run in debug mode. - -.. code-block:: text - - $ flask --app hello run --debug - -When running from Python code, passing ``debug=True`` enables debug mode, which is -mostly equivalent. - -.. code-block:: python - - app.run(debug=True) - -:doc:`/server` and :doc:`/cli` have more information about running the debugger and -debug mode. More information about the debugger can be found in the `Werkzeug -documentation `__. - - -External Debuggers ------------------- - -External debuggers, such as those provided by IDEs, can offer a more -powerful debugging experience than the built-in debugger. They can also -be used to step through code during a request before an error is raised, -or if no error is raised. Some even have a remote mode so you can debug -code running on another machine. - -When using an external debugger, the app should still be in debug mode, otherwise Flask -turns unhandled errors into generic 500 error pages. However, the built-in debugger and -reloader should be disabled so they don't interfere with the external debugger. - -.. code-block:: text - - $ flask --app hello run --debug --no-debugger --no-reload - -When running from Python: - -.. code-block:: python - - app.run(debug=True, use_debugger=False, use_reloader=False) - -Disabling these isn't required, an external debugger will continue to work with the -following caveats. - -- If the built-in debugger is not disabled, it will catch unhandled exceptions before - the external debugger can. -- If the reloader is not disabled, it could cause an unexpected reload if code changes - during a breakpoint. -- The development server will still catch unhandled exceptions if the built-in - debugger is disabled, otherwise it would crash on any error. If you want that (and - usually you don't) pass ``passthrough_errors=True`` to ``app.run``. - - .. code-block:: python - - app.run( - debug=True, passthrough_errors=True, - use_debugger=False, use_reloader=False - ) diff --git a/docs/deploying/apache-httpd.rst b/docs/deploying/apache-httpd.rst deleted file mode 100644 index bdeaf626..00000000 --- a/docs/deploying/apache-httpd.rst +++ /dev/null @@ -1,66 +0,0 @@ -Apache httpd -============ - -`Apache httpd`_ is a fast, production level HTTP server. When serving -your application with one of the WSGI servers listed in :doc:`index`, it -is often good or necessary to put a dedicated HTTP server in front of -it. This "reverse proxy" can handle incoming requests, TLS, and other -security and performance concerns better than the WSGI server. - -httpd can be installed using your system package manager, or a pre-built -executable for Windows. Installing and running httpd itself is outside -the scope of this doc. This page outlines the basics of configuring -httpd to proxy your application. Be sure to read its documentation to -understand what features are available. - -.. _Apache httpd: https://httpd.apache.org/ - - -Domain Name ------------ - -Acquiring and configuring a domain name is outside the scope of this -doc. In general, you will buy a domain name from a registrar, pay for -server space with a hosting provider, and then point your registrar -at the hosting provider's name servers. - -To simulate this, you can also edit your ``hosts`` file, located at -``/etc/hosts`` on Linux. Add a line that associates a name with the -local IP. - -Modern Linux systems may be configured to treat any domain name that -ends with ``.localhost`` like this without adding it to the ``hosts`` -file. - -.. code-block:: python - :caption: ``/etc/hosts`` - - 127.0.0.1 hello.localhost - - -Configuration -------------- - -The httpd configuration is located at ``/etc/httpd/conf/httpd.conf`` on -Linux. It may be different depending on your operating system. Check the -docs and look for ``httpd.conf``. - -Remove or comment out any existing ``DocumentRoot`` directive. Add the -config lines below. We'll assume the WSGI server is listening locally at -``http://127.0.0.1:8000``. - -.. code-block:: apache - :caption: ``/etc/httpd/conf/httpd.conf`` - - LoadModule proxy_module modules/mod_proxy.so - LoadModule proxy_http_module modules/mod_proxy_http.so - ProxyPass / http://127.0.0.1:8000/ - RequestHeader set X-Forwarded-Proto http - RequestHeader set X-Forwarded-Prefix / - -The ``LoadModule`` lines might already exist. If so, make sure they are -uncommented instead of adding them manually. - -Then :doc:`proxy_fix` so that your application uses the ``X-Forwarded`` -headers. ``X-Forwarded-For`` and ``X-Forwarded-Host`` are automatically -set by ``ProxyPass``. diff --git a/docs/deploying/asgi.rst b/docs/deploying/asgi.rst deleted file mode 100644 index 1dc0aa24..00000000 --- a/docs/deploying/asgi.rst +++ /dev/null @@ -1,27 +0,0 @@ -ASGI -==== - -If you'd like to use an ASGI server you will need to utilise WSGI to -ASGI middleware. The asgiref -`WsgiToAsgi `_ -adapter is recommended as it integrates with the event loop used for -Flask's :ref:`async_await` support. You can use the adapter by -wrapping the Flask app, - -.. code-block:: python - - from asgiref.wsgi import WsgiToAsgi - from flask import Flask - - app = Flask(__name__) - - ... - - asgi_app = WsgiToAsgi(app) - -and then serving the ``asgi_app`` with the ASGI server, e.g. using -`Hypercorn `_, - -.. sourcecode:: text - - $ hypercorn module:asgi_app diff --git a/docs/deploying/cgi.rst b/docs/deploying/cgi.rst new file mode 100644 index 00000000..a2fba90d --- /dev/null +++ b/docs/deploying/cgi.rst @@ -0,0 +1,46 @@ +CGI +=== + +If all other deployment methods do not work, CGI will work for sure. +CGI is supported by all major servers but usually has a sub-optimal +performance. + +This is also the way you can use a Flask application on Google's `App +Engine`_, where execution happens in a CGI-like environment. + +.. admonition:: Watch Out + + Please make sure in advance that any ``app.run()`` calls you might + have in your application file are inside an ``if __name__ == + '__main__':`` block or moved to a separate file. Just make sure it's + not called because this will always start a local WSGI server which + we do not want if we deploy that application to CGI / app engine. + +Creating a `.cgi` file +---------------------- + +First you need to create the CGI application file. Let's call it +`yourapplication.cgi`:: + + #!/usr/bin/python + from wsgiref.handlers import CGIHandler + from yourapplication import app + + CGIHandler().run(app) + +Server Setup +------------ + +Usually there are two ways to configure the server. Either just copy the +`.cgi` into a `cgi-bin` (and use `mod_rewrite` or something similar to +rewrite the URL) or let the server point to the file directly. + +In Apache for example you can put a like like this into the config: + +.. sourcecode:: apache + + ScriptAlias /app /path/to/the/application.cgi + +For more information consult the documentation of your webserver. + +.. _App Engine: http://code.google.com/appengine/ diff --git a/docs/deploying/eventlet.rst b/docs/deploying/eventlet.rst deleted file mode 100644 index d4c9e817..00000000 --- a/docs/deploying/eventlet.rst +++ /dev/null @@ -1,8 +0,0 @@ -:orphan: - -eventlet -======== - -`Eventlet is no longer maintained.`__ Use :doc:`/deploying/gevent` instead. - -__ https://eventlet.readthedocs.io diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst new file mode 100644 index 00000000..6dace1a8 --- /dev/null +++ b/docs/deploying/fastcgi.rst @@ -0,0 +1,164 @@ +.. _deploying-fastcgi: + +FastCGI +======= + +FastCGI is a deployment option on servers like `nginx`_, `lighttpd`_, +and `cherokee`_; see :ref:`deploying-uwsgi` and +:ref:`deploying-other-servers` for other options. To use your WSGI +application with any of them you will need a FastCGI server first. The +most popular one is `flup`_ which we will use for this guide. Make sure +to have it installed to follow along. + +.. admonition:: Watch Out + + Please make sure in advance that any ``app.run()`` calls you might + have in your application file are inside an ``if __name__ == + '__main__':`` block or moved to a separate file. Just make sure it's + not called because this will always start a local WSGI server which + we do not want if we deploy that application to FastCGI. + +Creating a `.fcgi` file +----------------------- + +First you need to create the FastCGI server file. Let's call it +`yourapplication.fcgi`:: + + #!/usr/bin/python + from flup.server.fcgi import WSGIServer + from yourapplication import app + + if __name__ == '__main__': + WSGIServer(app).run() + +This is enough for Apache to work, however nginx and older versions of +lighttpd need a socket to be explicitly passed to communicate with the +FastCGI server. For that to work you need to pass the path to the +socket to the :class:`~flup.server.fcgi.WSGIServer`:: + + WSGIServer(application, bindAddress='/path/to/fcgi.sock').run() + +The path has to be the exact same path you define in the server +config. + +Save the `yourapplication.fcgi` file somewhere you will find it again. +It makes sense to have that in `/var/www/yourapplication` or something +similar. + +Make sure to set the executable bit on that file so that the servers +can execute it: + +.. sourcecode:: text + + # chmod +x /var/www/yourapplication/yourapplication.fcgi + +Configuring lighttpd +-------------------- + +A basic FastCGI configuration for lighttpd looks like that:: + + fastcgi.server = ("/yourapplication.fcgi" => + (( + "socket" => "/tmp/yourapplication-fcgi.sock", + "bin-path" => "/var/www/yourapplication/yourapplication.fcgi", + "check-local" => "disable", + "max-procs" => 1 + )) + ) + + alias.url = ( + "/static/" => "/path/to/your/static" + ) + + url.rewrite-once = ( + "^(/static.*)$" => "$1", + "^(/.*)$" => "/yourapplication.fcgi$1" + +Remember to enable the FastCGI, alias and rewrite modules. This +configuration binds the application to `/yourapplication`. If you want +the application to work in the URL root you have to work around a +lighttpd bug with the +:class:`~werkzeug.contrib.fixers.LighttpdCGIRootFix` middleware. + +Make sure to apply it only if you are mounting the application the URL +root. Also, see the Lighty docs for more information on `FastCGI and +Python `_ +(note that explicitly passing a socket to run() is no longer necessary). + + +Configuring nginx +----------------- + +Installing FastCGI applications on nginx is a bit different because by +default no FastCGI parameters are forwarded. + +A basic flask FastCGI configuration for nginx looks like this:: + + location = /yourapplication { rewrite ^ /yourapplication/ last; } + location /yourapplication { try_files $uri @yourapplication; } + location @yourapplication { + include fastcgi_params; + fastcgi_split_path_info ^(/yourapplication)(.*)$; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; + } + +This configuration binds the application to `/yourapplication`. If you +want to have it in the URL root it's a bit simpler because you don't +have to figure out how to calculate `PATH_INFO` and `SCRIPT_NAME`:: + + location / { try_files $uri @yourapplication; } + location @yourapplication { + include fastcgi_params; + fastcgi_param PATH_INFO $fastcgi_script_name; + fastcgi_param SCRIPT_NAME ""; + fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; + } + +Running FastCGI Processes +------------------------- + +Since Nginx and others do not load FastCGI apps, you have to do it by +yourself. `Supervisor can manage FastCGI processes. +`_ +You can look around for other FastCGI process managers or write a script +to run your `.fcgi` file at boot, e.g. using a SysV ``init.d`` script. +For a temporary solution, you can always run the ``.fcgi`` script inside +GNU screen. See ``man screen`` for details, and note that this is a +manual solution which does not persist across system restart:: + + $ screen + $ /var/www/yourapplication/yourapplication.fcgi + +Debugging +--------- + +FastCGI deployments tend to be hard to debug on most webservers. Very +often the only thing the server log tells you is something along the +lines of "premature end of headers". In order to debug the application +the only thing that can really give you ideas why it breaks is switching +to the correct user and executing the application by hand. + +This example assumes your application is called `application.fcgi` and +that your webserver user is `www-data`:: + + $ su www-data + $ cd /var/www/yourapplication + $ python application.fcgi + Traceback (most recent call last): + File "yourapplication.fcgi", line 4, in + ImportError: No module named yourapplication + +In this case the error seems to be "yourapplication" not being on the +python path. Common problems are: + +- Relative paths being used. Don't rely on the current working directory +- The code depending on environment variables that are not set by the + web server. +- Different python interpreters being used. + +.. _nginx: http://nginx.org/ +.. _lighttpd: http://www.lighttpd.net/ +.. _cherokee: http://www.cherokee-project.com/ +.. _flup: http://trac.saddi.com/flup diff --git a/docs/deploying/gevent.rst b/docs/deploying/gevent.rst deleted file mode 100644 index 8810d21c..00000000 --- a/docs/deploying/gevent.rst +++ /dev/null @@ -1,76 +0,0 @@ -gevent -====== - -Prefer using :doc:`gunicorn` or :doc:`uwsgi` with gevent workers rather -than using `gevent`_ directly. Gunicorn and uWSGI provide much more -configurable and production-tested servers. - -`gevent`_ allows writing asynchronous, coroutine-based code that looks -like standard synchronous Python. It uses `greenlet`_ to enable task -switching without writing ``async/await`` or using ``asyncio``. This is -not the same as Python's ``async/await``, or the ASGI server spec. - -gevent provides a WSGI server that can handle many connections at once -instead of one per worker process. See :doc:`/gevent` for more -information about enabling it in your application. - -.. _gevent: https://www.gevent.org/ -.. _greenlet: https://greenlet.readthedocs.io/en/latest/ - - -Installing ----------- - -When using gevent, greenlet>=1.0 is required. When using PyPy, -PyPy>=7.3.7 is required. - -Create a virtualenv, install your application, then install ``gevent``. - -.. code-block:: text - - $ cd hello-app - $ python -m venv .venv - $ . .venv/bin/activate - $ pip install . # install your application - $ pip install gevent - - -Running -------- - -To use gevent to serve your application, write a script that imports its -``WSGIServer``, as well as your app or app factory. - -.. code-block:: python - :caption: ``wsgi.py`` - - from gevent.pywsgi import WSGIServer - from hello import create_app - - app = create_app() - http_server = WSGIServer(("127.0.0.1", 8000), app) - http_server.serve_forever() - -.. code-block:: text - - $ python wsgi.py - -No output is shown when the server starts. - - -Binding Externally ------------------- - -gevent should not be run as root because it would cause your -application code to run as root, which is not secure. However, this -means it will not be possible to bind to port 80 or 443. Instead, a -reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used -in front of gevent. - -You can bind to all external IPs on a non-privileged port by using -``0.0.0.0`` in the server arguments shown in the previous section. Don't -do this when using a reverse proxy setup, otherwise it will be possible -to bypass the proxy. - -``0.0.0.0`` is not a valid address to navigate to, you'd use a specific -IP address in your browser. diff --git a/docs/deploying/gunicorn.rst b/docs/deploying/gunicorn.rst deleted file mode 100644 index 089cb914..00000000 --- a/docs/deploying/gunicorn.rst +++ /dev/null @@ -1,116 +0,0 @@ -Gunicorn -======== - -`Gunicorn`_ is a pure Python WSGI server with simple configuration and -multiple worker implementations for performance tuning. - -* It tends to integrate easily with hosting platforms. -* It does not support Windows (but does run on WSL). -* It is easy to install as it does not require additional dependencies - or compilation. -* It has built-in async worker support using gevent. - -This page outlines the basics of running Gunicorn. Be sure to read its -`documentation`_ and use ``gunicorn --help`` to understand what features -are available. - -.. _Gunicorn: https://gunicorn.org/ -.. _documentation: https://docs.gunicorn.org/ - - -Installing ----------- - -Gunicorn is easy to install, as it does not require external -dependencies or compilation. It runs on Windows only under WSL. - -Create a virtualenv, install your application, then install -``gunicorn``. - -.. code-block:: text - - $ cd hello-app - $ python -m venv .venv - $ . .venv/bin/activate - $ pip install . # install your application - $ pip install gunicorn - - -Running -------- - -The only required argument to Gunicorn tells it how to load your Flask -application. The syntax is ``{module_import}:{app_variable}``. -``module_import`` is the dotted import name to the module with your -application. ``app_variable`` is the variable with the application. It -can also be a function call (with any arguments) if you're using the -app factory pattern. - -.. code-block:: text - - # equivalent to 'from hello import app' - $ gunicorn -w 4 'hello:app' - - # equivalent to 'from hello import create_app; create_app()' - $ gunicorn -w 4 'hello:create_app()' - - Starting gunicorn 20.1.0 - Listening at: http://127.0.0.1:8000 (x) - Using worker: sync - Booting worker with pid: x - Booting worker with pid: x - Booting worker with pid: x - Booting worker with pid: x - -The ``-w`` option specifies the number of processes to run; a starting -value could be ``CPU * 2``. The default is only 1 worker, which is -probably not what you want for the default worker type. - -Logs for each request aren't shown by default, only worker info and -errors are shown. To show access logs on stdout, use the -``--access-logfile=-`` option. - - -Binding Externally ------------------- - -Gunicorn should not be run as root because it would cause your -application code to run as root, which is not secure. However, this -means it will not be possible to bind to port 80 or 443. Instead, a -reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used -in front of Gunicorn. - -You can bind to all external IPs on a non-privileged port using the -``-b 0.0.0.0`` option. Don't do this when using a reverse proxy setup, -otherwise it will be possible to bypass the proxy. - -.. code-block:: text - - $ gunicorn -w 4 -b 0.0.0.0 'hello:create_app()' - Listening at: http://0.0.0.0:8000 (x) - -``0.0.0.0`` is not a valid address to navigate to, you'd use a specific -IP address in your browser. - - -Async with gevent ------------------ - -The default sync worker is appropriate for most use cases. If you need numerous, -long running, concurrent connections, Gunicorn provides an asynchronous worker -using `gevent`_. This is not the same as Python's ``async/await``, or the ASGI -server spec. See :doc:`/gevent` for more information about enabling it in your -application. - -.. _gevent: https://www.gevent.org/ - -When using gevent, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is -required. - -.. code-block:: text - - $ gunicorn -k gevent 'hello:create_app()' - Starting gunicorn 20.1.0 - Listening at: http://127.0.0.1:8000 (x) - Using worker: gevent - Booting worker with pid: x diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index dcef8abe..d258df89 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -1,78 +1,23 @@ -Deploying to Production -======================= +.. _deployment: -After developing your application, you'll want to make it available -publicly to other users. When you're developing locally, you're probably -using the built-in development server, debugger, and reloader. These -should not be used in production. Instead, you should use a dedicated -WSGI server or hosting platform, some of which will be described here. +Deployment Options +================== -"Production" means "not development", which applies whether you're -serving your application publicly to millions of users or privately / -locally to a single user. **Do not use the development server when -deploying to production. It is intended for use only during local -development. It is not designed to be particularly secure, stable, or -efficient.** +Depending on what you have available there are multiple ways to run +Flask applications. You can use the builtin server during development, +but you should use a full deployment option for production applications. +(Do not use the builtin development server in production.) Several +options are available and documented here. -Self-Hosted Options -------------------- - -Flask is a WSGI *application*. A WSGI *server* is used to run the -application, converting incoming HTTP requests to the standard WSGI -environ, and converting outgoing WSGI responses to HTTP responses. - -The primary goal of these docs is to familiarize you with the concepts -involved in running a WSGI application using a production WSGI server -and HTTP server. There are many WSGI servers and HTTP servers, with many -configuration possibilities. The pages below discuss the most common -servers, and show the basics of running each one. The next section -discusses platforms that can manage this for you. +If you have a different WSGI server look up the server documentation +about how to use a WSGI app with it. Just remember that your +:class:`Flask` application object is the actual WSGI application. .. toctree:: - :maxdepth: 1 + :maxdepth: 2 - gunicorn - waitress - mod_wsgi - uwsgi - gevent - asgi - -WSGI servers have HTTP servers built-in. However, a dedicated HTTP -server may be safer, more efficient, or more capable. Putting an HTTP -server in front of the WSGI server is called a "reverse proxy." - -.. toctree:: - :maxdepth: 1 - - proxy_fix - nginx - apache-httpd - -This list is not exhaustive, and you should evaluate these and other -servers based on your application's needs. Different servers will have -different capabilities, configuration, and support. - - -Hosting Platforms ------------------ - -There are many services available for hosting web applications without -needing to maintain your own server, networking, domain, etc. Some -services may have a free tier up to a certain time or bandwidth. Many of -these services use one of the WSGI servers described above, or a similar -interface. The links below are for some of the most common platforms, -which have instructions for Flask, WSGI, or Python. - -- `PythonAnywhere `_ -- `Google App Engine `_ -- `Google Cloud Run `_ -- `AWS Elastic Beanstalk `_ -- `Microsoft Azure `_ - -This list is not exhaustive, and you should evaluate these and other -services based on your application's needs. Different services will have -different capabilities, configuration, pricing, and support. - -You'll probably need to :doc:`proxy_fix` when using most hosting -platforms. + mod_wsgi + cgi + fastcgi + uwsgi + others diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index 23e82279..c85ed64f 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -1,94 +1,167 @@ -mod_wsgi -======== +.. _mod_wsgi-deployment: -`mod_wsgi`_ is a WSGI server integrated with the `Apache httpd`_ server. -The modern `mod_wsgi-express`_ command makes it easy to configure and -start the server without needing to write Apache httpd configuration. +mod_wsgi (Apache) +================= -* Tightly integrated with Apache httpd. -* Supports Windows directly. -* Requires a compiler and the Apache development headers to install. -* Does not require a reverse proxy setup. +If you are using the `Apache`_ webserver, consider using `mod_wsgi`_. -This page outlines the basics of running mod_wsgi-express, not the more -complex installation and configuration with httpd. Be sure to read the -`mod_wsgi-express`_, `mod_wsgi`_, and `Apache httpd`_ documentation to -understand what features are available. +.. admonition:: Watch Out -.. _mod_wsgi-express: https://pypi.org/project/mod-wsgi/ -.. _mod_wsgi: https://modwsgi.readthedocs.io/ -.. _Apache httpd: https://httpd.apache.org/ + Please make sure in advance that any ``app.run()`` calls you might + have in your application file are inside an ``if __name__ == + '__main__':`` block or moved to a separate file. Just make sure it's + not called because this will always start a local WSGI server which + we do not want if we deploy that application to mod_wsgi. +.. _Apache: http://httpd.apache.org/ -Installing ----------- +Installing `mod_wsgi` +--------------------- -Installing mod_wsgi requires a compiler and the Apache server and -development headers installed. You will get an error if they are not. -How to install them depends on the OS and package manager that you use. +If you don't have `mod_wsgi` installed yet you have to either install it +using a package manager or compile it yourself. The mod_wsgi +`installation instructions`_ cover source installations on UNIX systems. -Create a virtualenv, install your application, then install -``mod_wsgi``. +If you are using Ubuntu/Debian you can apt-get it and activate it as +follows: -.. code-block:: text +.. sourcecode:: text - $ cd hello-app - $ python -m venv .venv - $ . .venv/bin/activate - $ pip install . # install your application - $ pip install mod_wsgi + # apt-get install libapache2-mod-wsgi +On FreeBSD install `mod_wsgi` by compiling the `www/mod_wsgi` port or by +using pkg_add: -Running -------- +.. sourcecode:: text -The only argument to ``mod_wsgi-express`` specifies a script containing -your Flask application, which must be called ``application``. You can -write a small script to import your app with this name, or to create it -if using the app factory pattern. + # pkg_add -r mod_wsgi -.. code-block:: python - :caption: ``wsgi.py`` +If you are using pkgsrc you can install `mod_wsgi` by compiling the +`www/ap2-wsgi` package. - from hello import app +If you encounter segfaulting child processes after the first apache +reload you can safely ignore them. Just restart the server. - application = app +Creating a `.wsgi` file +----------------------- -.. code-block:: python - :caption: ``wsgi.py`` +To run your application you need a `yourapplication.wsgi` file. This file +contains the code `mod_wsgi` is executing on startup to get the application +object. The object called `application` in that file is then used as +application. - from hello import create_app +For most applications the following file should be sufficient:: - application = create_app() + from yourapplication import app as application -Now run the ``mod_wsgi-express start-server`` command. +If you don't have a factory function for application creation but a singleton +instance you can directly import that one as `application`. -.. code-block:: text +Store that file somewhere that you will find it again (e.g.: +`/var/www/yourapplication`) and make sure that `yourapplication` and all +the libraries that are in use are on the python load path. If you don't +want to install it system wide consider using a `virtual python`_ +instance. - $ mod_wsgi-express start-server wsgi.py --processes 4 - -The ``--processes`` option specifies the number of worker processes to -run; a starting value could be ``CPU * 2``. - -Logs for each request aren't show in the terminal. If an error occurs, -its information is written to the error log file shown when starting the -server. - - -Binding Externally +Configuring Apache ------------------ -Unlike the other WSGI servers in these docs, mod_wsgi can be run as -root to bind to privileged ports like 80 and 443. However, it must be -configured to drop permissions to a different user and group for the -worker processes. +The last thing you have to do is to create an Apache configuration file +for your application. In this example we are telling `mod_wsgi` to +execute the application under a different user for security reasons: -For example, if you created a ``hello`` user and group, you should -install your virtualenv and application as that user, then tell -mod_wsgi to drop to that user after starting. +.. sourcecode:: apache -.. code-block:: text + + ServerName example.com - $ sudo /home/hello/.venv/bin/mod_wsgi-express start-server \ - /home/hello/wsgi.py \ - --user hello --group hello --port 80 --processes 4 + WSGIDaemonProcess yourapplication user=user1 group=group1 threads=5 + WSGIScriptAlias / /var/www/yourapplication/yourapplication.wsgi + + + WSGIProcessGroup yourapplication + WSGIApplicationGroup %{GLOBAL} + Order deny,allow + Allow from all + + + +For more information consult the `mod_wsgi wiki`_. + +.. _mod_wsgi: http://code.google.com/p/modwsgi/ +.. _installation instructions: http://code.google.com/p/modwsgi/wiki/QuickInstallationGuide +.. _virtual python: http://pypi.python.org/pypi/virtualenv +.. _mod_wsgi wiki: http://code.google.com/p/modwsgi/wiki/ + +Troubleshooting +--------------- + +If your application does not run, follow this guide to troubleshoot: + +**Problem:** application does not run, errorlog shows SystemExit ignored + You have a ``app.run()`` call in your application file that is not + guarded by an ``if __name__ == '__main__':`` condition. Either + remove that :meth:`~flask.Flask.run` call from the file and move it + into a separate `run.py` file or put it into such an if block. + +**Problem:** application gives permission errors + Probably caused by your application running as the wrong user. Make + sure the folders the application needs access to have the proper + privileges set and the application runs as the correct user + (``user`` and ``group`` parameter to the `WSGIDaemonProcess` + directive) + +**Problem:** application dies with an error on print + Keep in mind that mod_wsgi disallows doing anything with + :data:`sys.stdout` and :data:`sys.stderr`. You can disable this + protection from the config by setting the `WSGIRestrictStdout` to + ``off``: + + .. sourcecode:: apache + + WSGIRestrictStdout Off + + Alternatively you can also replace the standard out in the .wsgi file + with a different stream:: + + import sys + sys.stdout = sys.stderr + +**Problem:** accessing resources gives IO errors + Your application probably is a single .py file you symlinked into + the site-packages folder. Please be aware that this does not work, + instead you either have to put the folder into the pythonpath the + file is stored in, or convert your application into a package. + + The reason for this is that for non-installed packages, the module + filename is used to locate the resources and for symlinks the wrong + filename is picked up. + +Support for Automatic Reloading +------------------------------- + +To help deployment tools you can activate support for automatic +reloading. Whenever something changes the `.wsgi` file, `mod_wsgi` will +reload all the daemon processes for us. + +For that, just add the following directive to your `Directory` section: + +.. sourcecode:: apache + + WSGIScriptReloading On + +Working with Virtual Environments +--------------------------------- + +Virtual environments have the advantage that they never install the +required dependencies system wide so you have a better control over what +is used where. If you want to use a virtual environment with mod_wsgi +you have to modify your `.wsgi` file slightly. + +Add the following lines to the top of your `.wsgi` file:: + + activate_this = '/path/to/env/bin/activate_this.py' + execfile(activate_this, dict(__file__=activate_this)) + +This sets up the load paths according to the settings of the virtual +environment. Keep in mind that the path has to be absolute. diff --git a/docs/deploying/nginx.rst b/docs/deploying/nginx.rst deleted file mode 100644 index 6b25c073..00000000 --- a/docs/deploying/nginx.rst +++ /dev/null @@ -1,69 +0,0 @@ -nginx -===== - -`nginx`_ is a fast, production level HTTP server. When serving your -application with one of the WSGI servers listed in :doc:`index`, it is -often good or necessary to put a dedicated HTTP server in front of it. -This "reverse proxy" can handle incoming requests, TLS, and other -security and performance concerns better than the WSGI server. - -Nginx can be installed using your system package manager, or a pre-built -executable for Windows. Installing and running Nginx itself is outside -the scope of this doc. This page outlines the basics of configuring -Nginx to proxy your application. Be sure to read its documentation to -understand what features are available. - -.. _nginx: https://nginx.org/ - - -Domain Name ------------ - -Acquiring and configuring a domain name is outside the scope of this -doc. In general, you will buy a domain name from a registrar, pay for -server space with a hosting provider, and then point your registrar -at the hosting provider's name servers. - -To simulate this, you can also edit your ``hosts`` file, located at -``/etc/hosts`` on Linux. Add a line that associates a name with the -local IP. - -Modern Linux systems may be configured to treat any domain name that -ends with ``.localhost`` like this without adding it to the ``hosts`` -file. - -.. code-block:: python - :caption: ``/etc/hosts`` - - 127.0.0.1 hello.localhost - - -Configuration -------------- - -The nginx configuration is located at ``/etc/nginx/nginx.conf`` on -Linux. It may be different depending on your operating system. Check the -docs and look for ``nginx.conf``. - -Remove or comment out any existing ``server`` section. Add a ``server`` -section and use the ``proxy_pass`` directive to point to the address the -WSGI server is listening on. We'll assume the WSGI server is listening -locally at ``http://127.0.0.1:8000``. - -.. code-block:: nginx - :caption: ``/etc/nginx.conf`` - - server { - listen 80; - server_name _; - - location / { - proxy_pass http://127.0.0.1:8000/; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Prefix /; - } - } - -Then :doc:`proxy_fix` so that your application uses these headers. diff --git a/docs/deploying/others.rst b/docs/deploying/others.rst new file mode 100644 index 00000000..6f3e5cc6 --- /dev/null +++ b/docs/deploying/others.rst @@ -0,0 +1,102 @@ +.. _deploying-other-servers: + +Other Servers +============= + +There are popular servers written in Python that allow the execution of WSGI +applications as well. These servers stand alone when they run; you can proxy +to them from your web server. + +Tornado +-------- + +`Tornado`_ is an open source version of the scalable, non-blocking web +server and tools that power `FriendFeed`_. Because it is non-blocking and +uses epoll, it can handle thousands of simultaneous standing connections, +which means it is ideal for real-time web services. Integrating this +service with Flask is a trivial task:: + + from tornado.wsgi import WSGIContainer + from tornado.httpserver import HTTPServer + from tornado.ioloop import IOLoop + from yourapplication import app + + http_server = HTTPServer(WSGIContainer(app)) + http_server.listen(5000) + IOLoop.instance().start() + + +.. _Tornado: http://www.tornadoweb.org/ +.. _FriendFeed: http://friendfeed.com/ + +Gevent +------- + +`Gevent`_ is a coroutine-based Python networking library that uses +`greenlet`_ to provide a high-level synchronous API on top of `libevent`_ +event loop:: + + from gevent.wsgi import WSGIServer + from yourapplication import app + + http_server = WSGIServer(('', 5000), app) + http_server.serve_forever() + +.. _Gevent: http://www.gevent.org/ +.. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html +.. _libevent: http://monkey.org/~provos/libevent/ + +Gunicorn +-------- + +`Gunicorn`_ 'Green Unicorn' is a WSGI HTTP Server for UNIX. It's a pre-fork +worker model ported from Ruby's Unicorn project. It supports both `eventlet`_ +and `greenlet`_. Running a Flask application on this server is quite simple:: + + gunicorn myproject:app + +`Gunicorn`_ provides many command-line options -- see ``gunicorn -h``. +For example, to run a Flask application with 4 worker processes (``-w +4``) binding to localhost port 4000 (``-b 127.0.0.1:4000``):: + + gunicorn -w 4 -b 127.0.0.1:4000 myproject:app + +.. _Gunicorn: http://gunicorn.org/ +.. _eventlet: http://eventlet.net/ +.. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html + +Proxy Setups +------------ + +If you deploy your application using one of these servers behind an HTTP +proxy you will need to rewrite a few headers in order for the +application to work. The two problematic values in the WSGI environment +usually are `REMOTE_ADDR` and `HTTP_HOST`. Werkzeug ships a fixer that +will solve some common setups, but you might want to write your own WSGI +middleware for specific setups. + +The most common setup invokes the host being set from `X-Forwarded-Host` +and the remote address from `X-Forwarded-For`:: + + from werkzeug.contrib.fixers import ProxyFix + app.wsgi_app = ProxyFix(app.wsgi_app) + +Please keep in mind that it is a security issue to use such a middleware +in a non-proxy setup because it will blindly trust the incoming +headers which might be forged by malicious clients. + +If you want to rewrite the headers from another header, you might want to +use a fixer like this:: + + class CustomProxyFix(object): + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + host = environ.get('HTTP_X_FHOST', '') + if host: + environ['HTTP_HOST'] = host + return self.app(environ, start_response) + + app.wsgi_app = CustomProxyFix(app.wsgi_app) diff --git a/docs/deploying/proxy_fix.rst b/docs/deploying/proxy_fix.rst deleted file mode 100644 index e2c42e82..00000000 --- a/docs/deploying/proxy_fix.rst +++ /dev/null @@ -1,33 +0,0 @@ -Tell Flask it is Behind a Proxy -=============================== - -When using a reverse proxy, or many Python hosting platforms, the proxy -will intercept and forward all external requests to the local WSGI -server. - -From the WSGI server and Flask application's perspectives, requests are -now coming from the HTTP server to the local address, rather than from -the remote address to the external server address. - -HTTP servers should set ``X-Forwarded-`` headers to pass on the real -values to the application. The application can then be told to trust and -use those values by wrapping it with the -:doc:`werkzeug:middleware/proxy_fix` middleware provided by Werkzeug. - -This middleware should only be used if the application is actually -behind a proxy, and should be configured with the number of proxies that -are chained in front of it. Not all proxies set all the headers. Since -incoming headers can be faked, you must set how many proxies are setting -each header so the middleware knows what to trust. - -.. code-block:: python - - from werkzeug.middleware.proxy_fix import ProxyFix - - app.wsgi_app = ProxyFix( - app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 - ) - -Remember, only apply this middleware if you are behind a proxy, and set -the correct number of proxies that set each header. It can be a security -issue if you get this configuration wrong. diff --git a/docs/deploying/uwsgi.rst b/docs/deploying/uwsgi.rst index aa80b6fb..bdee15ba 100644 --- a/docs/deploying/uwsgi.rst +++ b/docs/deploying/uwsgi.rst @@ -1,143 +1,68 @@ +.. _deploying-uwsgi: + uWSGI ===== -`uWSGI`_ is a fast, compiled server suite with extensive configuration -and capabilities beyond a basic server. +uWSGI is a deployment option on servers like `nginx`_, `lighttpd`_, and +`cherokee`_; see :ref:`deploying-fastcgi` and +:ref:`deploying-other-servers` for other options. To use your WSGI +application with uWSGI protocol you will need a uWSGI server +first. uWSGI is both a protocol and an application server; the +application server can serve uWSGI, FastCGI, and HTTP protocols. -* It can be very performant due to being a compiled program. -* It is complex to configure beyond the basic application, and has so - many options that it can be difficult for beginners to understand. -* It does not support Windows (but does run on WSL). -* It requires a compiler to install in some cases. +The most popular uWSGI server is `uwsgi`_, which we will use for this +guide. Make sure to have it installed to follow along. -This page outlines the basics of running uWSGI. Be sure to read its -documentation to understand what features are available. +.. admonition:: Watch Out -.. _uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/ + Please make sure in advance that any ``app.run()`` calls you might + have in your application file are inside an ``if __name__ == + '__main__':`` block or moved to a separate file. Just make sure it's + not called because this will always start a local WSGI server which + we do not want if we deploy that application to uWSGI. +Starting your app with uwsgi +---------------------------- -Installing ----------- +`uwsgi` is designed to operate on WSGI callables found in python modules. -uWSGI has multiple ways to install it. The most straightforward is to -install the ``pyuwsgi`` package, which provides precompiled wheels for -common platforms. However, it does not provide SSL support, which can be -provided with a reverse proxy instead. +Given a flask application in myapp.py, use the following command: -Create a virtualenv, install your application, then install ``pyuwsgi``. +.. sourcecode:: text -.. code-block:: text + $ uwsgi -s /tmp/uwsgi.sock --module myapp --callable app - $ cd hello-app - $ python -m venv .venv - $ . .venv/bin/activate - $ pip install . # install your application - $ pip install pyuwsgi +Or, if you prefer: -If you have a compiler available, you can install the ``uwsgi`` package -instead. Or install the ``pyuwsgi`` package from sdist instead of wheel. -Either method will include SSL support. +.. sourcecode:: text -.. code-block:: text + $ uwsgi -s /tmp/uwsgi.sock -w myapp:app - $ pip install uwsgi - - # or - $ pip install --no-binary pyuwsgi pyuwsgi - - -Running -------- - -The most basic way to run uWSGI is to tell it to start an HTTP server -and import your application. - -.. code-block:: text - - $ uwsgi --http 127.0.0.1:8000 --master -p 4 -w hello:app - - *** Starting uWSGI 2.0.20 (64bit) on [x] *** - *** Operational MODE: preforking *** - mounting hello:app on / - spawned uWSGI master process (pid: x) - spawned uWSGI worker 1 (pid: x, cores: 1) - spawned uWSGI worker 2 (pid: x, cores: 1) - spawned uWSGI worker 3 (pid: x, cores: 1) - spawned uWSGI worker 4 (pid: x, cores: 1) - spawned uWSGI http 1 (pid: x) - -If you're using the app factory pattern, you'll need to create a small -Python file to create the app, then point uWSGI at that. - -.. code-block:: python - :caption: ``wsgi.py`` - - from hello import create_app - - app = create_app() - -.. code-block:: text - - $ uwsgi --http 127.0.0.1:8000 --master -p 4 -w wsgi:app - -The ``--http`` option starts an HTTP server at 127.0.0.1 port 8000. The -``--master`` option specifies the standard worker manager. The ``-p`` -option starts 4 worker processes; a starting value could be ``CPU * 2``. -The ``-w`` option tells uWSGI how to import your application - - -Binding Externally ------------------- - -uWSGI should not be run as root with the configuration shown in this doc -because it would cause your application code to run as root, which is -not secure. However, this means it will not be possible to bind to port -80 or 443. Instead, a reverse proxy such as :doc:`nginx` or -:doc:`apache-httpd` should be used in front of uWSGI. It is possible to -run uWSGI as root securely, but that is beyond the scope of this doc. - -uWSGI has optimized integration with `Nginx uWSGI`_ and -`Apache mod_proxy_uwsgi`_, and possibly other servers, instead of using -a standard HTTP proxy. That configuration is beyond the scope of this -doc, see the links for more information. - -.. _Nginx uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/Nginx.html -.. _Apache mod_proxy_uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/Apache.html#mod-proxy-uwsgi - -You can bind to all external IPs on a non-privileged port using the -``--http 0.0.0.0:8000`` option. Don't do this when using a reverse proxy -setup, otherwise it will be possible to bypass the proxy. - -.. code-block:: text - - $ uwsgi --http 0.0.0.0:8000 --master -p 4 -w wsgi:app - -``0.0.0.0`` is not a valid address to navigate to, you'd use a specific -IP address in your browser. - - -Async with gevent +Configuring nginx ----------------- -The default sync worker is appropriate for most use cases. If you need numerous, -long running, concurrent connections, uWSGI provides an asynchronous worker -using `gevent`_. This is not the same as Python's ``async/await``, or the ASGI -server spec. See :doc:`/gevent` for more information about enabling it in your -application. +A basic flask uWSGI configuration for nginx looks like this:: -.. _gevent: https://www.gevent.org/ + location = /yourapplication { rewrite ^ /yourapplication/; } + location /yourapplication { try_files $uri @yourapplication; } + location @yourapplication { + include uwsgi_params; + uwsgi_param SCRIPT_NAME /yourapplication; + uwsgi_modifier1 30; + uwsgi_pass unix:/tmp/uwsgi.sock; + } -When using gevent, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is -required. +This configuration binds the application to `/yourapplication`. If you want +to have it in the URL root it's a bit simpler because you don't have to tell +it the WSGI `SCRIPT_NAME` or set the uwsgi modifier to make use of it:: -.. code-block:: text + location / { try_files $uri @yourapplication; } + location @yourapplication { + include uwsgi_params; + uwsgi_pass unix:/tmp/uwsgi.sock; + } - $ uwsgi --http 127.0.0.1:8000 --master --gevent 100 -w wsgi:app - - *** Starting uWSGI 2.0.20 (64bit) on [x] *** - *** Operational MODE: async *** - mounting hello:app on / - spawned uWSGI master process (pid: x) - spawned uWSGI worker 1 (pid: x, cores: 100) - spawned uWSGI http 1 (pid: x) - *** running gevent loop engine [addr:x] *** +.. _nginx: http://nginx.org/ +.. _lighttpd: http://www.lighttpd.net/ +.. _cherokee: http://www.cherokee-project.com/ +.. _uwsgi: http://projects.unbit.it/uwsgi/ diff --git a/docs/deploying/waitress.rst b/docs/deploying/waitress.rst deleted file mode 100644 index 7bdd695b..00000000 --- a/docs/deploying/waitress.rst +++ /dev/null @@ -1,75 +0,0 @@ -Waitress -======== - -`Waitress`_ is a pure Python WSGI server. - -* It is easy to configure. -* It supports Windows directly. -* It is easy to install as it does not require additional dependencies - or compilation. -* It does not support streaming requests, full request data is always - buffered. -* It uses a single process with multiple thread workers. - -This page outlines the basics of running Waitress. Be sure to read its -documentation and ``waitress-serve --help`` to understand what features -are available. - -.. _Waitress: https://docs.pylonsproject.org/projects/waitress/ - - -Installing ----------- - -Create a virtualenv, install your application, then install -``waitress``. - -.. code-block:: text - - $ cd hello-app - $ python -m venv .venv - $ . .venv/bin/activate - $ pip install . # install your application - $ pip install waitress - - -Running -------- - -The only required argument to ``waitress-serve`` tells it how to load -your Flask application. The syntax is ``{module}:{app}``. ``module`` is -the dotted import name to the module with your application. ``app`` is -the variable with the application. If you're using the app factory -pattern, use ``--call {module}:{factory}`` instead. - -.. code-block:: text - - # equivalent to 'from hello import app' - $ waitress-serve --host 127.0.0.1 hello:app - - # equivalent to 'from hello import create_app; create_app()' - $ waitress-serve --host 127.0.0.1 --call hello:create_app - - Serving on http://127.0.0.1:8080 - -The ``--host`` option binds the server to local ``127.0.0.1`` only. - -Logs for each request aren't shown, only errors are shown. Logging can -be configured through the Python interface instead of the command line. - - -Binding Externally ------------------- - -Waitress should not be run as root because it would cause your -application code to run as root, which is not secure. However, this -means it will not be possible to bind to port 80 or 443. Instead, a -reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used -in front of Waitress. - -You can bind to all external IPs on a non-privileged port by not -specifying the ``--host`` option. Don't do this when using a reverse -proxy setup, otherwise it will be possible to bypass the proxy. - -``0.0.0.0`` is not a valid address to navigate to, you'd use a specific -IP address in your browser. diff --git a/docs/design.rst b/docs/design.rst index d8776a90..6ca363a6 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -1,3 +1,5 @@ +.. _design: + Design Decisions in Flask ========================= @@ -39,14 +41,14 @@ the time. There are ways to fake multiple applications with a single application object, like maintaining a stack of applications, but this causes some problems I won't outline here in detail. Now the question is: when does a microframework need more than one application at the same -time? A good example for this is unit testing. When you want to test +time? A good example for this is unittesting. When you want to test something it can be very helpful to create a minimal application to test specific behavior. When the application object is deleted everything it allocated will be freed again. Another thing that becomes possible when you have an explicit object lying around in your code is that you can subclass the base class -(:class:`~flask.Flask`) to alter specific behavior. This would not be +(:class:`~flask.Flask`) to alter specific behaviour. This would not be possible without hacks if the object were created ahead of time for you based on a class that is not exposed to you. @@ -74,13 +76,13 @@ there are better ways to do that so that you do not lose the reference to the application object :meth:`~flask.Flask.wsgi_app`). Furthermore this design makes it possible to use a factory function to -create the application which is very helpful for unit testing and similar -things (:doc:`/patterns/appfactories`). +create the application which is very helpful for unittesting and similar +things (:ref:`app-factories`). The Routing System ------------------ -Flask uses the Werkzeug routing system which was designed to +Flask uses the Werkzeug routing system which has was designed to automatically order routes by complexity. This means that you can declare routes in arbitrary order and they will still work as expected. This is a requirement if you want to properly implement decorator based routing @@ -88,18 +90,18 @@ since decorators could be fired in undefined order when the application is split into multiple modules. Another design decision with the Werkzeug routing system is that routes -in Werkzeug try to ensure that URLs are unique. Werkzeug will go quite far -with that in that it will automatically redirect to a canonical URL if a route -is ambiguous. +in Werkzeug try to ensure that there is that URLs are unique. Werkzeug +will go quite far with that in that it will automatically redirect to a +canonical URL if a route is ambiguous. One Template Engine ------------------- -Flask decides on one template engine: Jinja. Why doesn't Flask have a +Flask decides on one template engine: Jinja2. Why doesn't Flask have a pluggable template engine interface? You can obviously use a different -template engine, but Flask will still configure Jinja for you. While -that limitation that Jinja is *always* configured will probably go away, +template engine, but Flask will still configure Jinja2 for you. While +that limitation that Jinja2 is *always* configured will probably go away, the decision to bundle one template engine and use that will not. Template engines are like programming languages and each of those engines @@ -107,19 +109,19 @@ has a certain understanding about how things work. On the surface they all work the same: you tell the engine to evaluate a template with a set of variables and take the return value as string. -But that's about where similarities end. Jinja for example has an -extensive filter system, a certain way to do template inheritance, -support for reusable blocks (macros) that can be used from inside -templates and also from Python code, supports iterative template -rendering, configurable syntax and more. On the other hand an engine -like Genshi is based on XML stream evaluation, template inheritance by -taking the availability of XPath into account and more. Mako on the -other hand treats templates similar to Python modules. +But that's about where similarities end. Jinja2 for example has an +extensive filter system, a certain way to do template inheritance, support +for reusable blocks (macros) that can be used from inside templates and +also from Python code, uses Unicode for all operations, supports +iterative template rendering, configurable syntax and more. On the other +hand an engine like Genshi is based on XML stream evaluation, template +inheritance by taking the availability of XPath into account and more. +Mako on the other hand treats templates similar to Python modules. When it comes to connecting a template engine with an application or framework there is more than just rendering templates. For instance, -Flask uses Jinja's extensive autoescaping support. Also it provides -ways to access macros from Jinja templates. +Flask uses Jinja2's extensive autoescaping support. Also it provides +ways to access macros from Jinja2 templates. A template abstraction layer that would not take the unique features of the template engines away is a science on its own and a too large @@ -130,27 +132,11 @@ being present. You can easily use your own templating language, but an extension could still depend on Jinja itself. -What does "micro" mean? +Micro with Dependencies ----------------------- -“Micro” does not mean that your whole web application has to fit into a single -Python file (although it certainly can), nor does it mean that Flask is lacking -in functionality. The "micro" in microframework means Flask aims to keep the -core simple but extensible. Flask won't make many decisions for you, such as -what database to use. Those decisions that it does make, such as what -templating engine to use, are easy to change. Everything else is up to you, so -that Flask can be everything you need and nothing you don't. - -By default, Flask does not include a database abstraction layer, form -validation or anything else where different libraries already exist that can -handle that. Instead, Flask supports extensions to add such functionality to -your application as if it was implemented in Flask itself. Numerous extensions -provide database integration, form validation, upload handling, various open -authentication technologies, and more. Flask may be "micro", but it's ready for -production use on a variety of needs. - Why does Flask call itself a microframework and yet it depends on two -libraries (namely Werkzeug and Jinja). Why shouldn't it? If we look +libraries (namely Werkzeug and Jinja2). Why shouldn't it? If we look over to the Ruby side of web development there we have a protocol very similar to WSGI. Just that it's called Rack there, but besides that it looks very much like a WSGI rendition for Ruby. But nearly all @@ -169,39 +155,22 @@ infrastructure, packages with dependencies are no longer an issue and there are very few reasons against having libraries that depend on others. -Context Locals --------------- +Thread Locals +------------- -Flask uses special context locals and proxies to provide access to the -current app and request data to any code running during a request, CLI command, -etc. Context locals are specific to the worker handling the activity, such as a -thread, process, coroutine, or greenlet. +Flask uses thread local objects (context local objects in fact, they +support greenlet contexts as well) for request, session and an extra +object you can put your own things on (:data:`~flask.g`). Why is that and +isn't that a bad idea? -The context and proxies help solve two development issues: circular imports, and -passing around global data. :data:`.current_app` can be used to access the -application object without needing to import the app object directly, avoiding -circular import issues. :data:`.request`, :data:`.session`, and :data:`.g` can -be imported to access the current data for the request, rather than needing to -pass them as arguments through every single function in your project. +Yes it is usually not such a bright idea to use thread locals. They cause +troubles for servers that are not based on the concept of threads and make +large applications harder to maintain. However Flask is just not designed +for large applications or asynchronous servers. Flask wants to make it +quick and easy to write a traditional web application. - -Async/await and ASGI support ----------------------------- - -Flask supports ``async`` coroutines for view functions by executing the -coroutine on a separate thread instead of using an event loop on the -main thread as an async-first (ASGI) framework would. This is necessary -for Flask to remain backwards compatible with extensions and code built -before ``async`` was introduced into Python. This compromise introduces -a performance cost compared with the ASGI frameworks, due to the -overhead of the threads. - -Due to how tied to WSGI Flask's code is, it's not clear if it's possible -to make the ``Flask`` class support ASGI and WSGI at the same time. Work -is currently being done in Werkzeug to work with ASGI, which may -eventually enable support in Flask as well. - -See :doc:`/async-await` for more discussion. +Also see the :ref:`becomingbig` section of the documentation for some +inspiration for larger applications based on Flask. What Flask is, What Flask is Not @@ -209,7 +178,7 @@ What Flask is, What Flask is Not Flask will never have a database layer. It will not have a form library or anything else in that direction. Flask itself just bridges to Werkzeug -to implement a proper WSGI application and to Jinja to handle templating. +to implement a proper WSGI application and to Jinja2 to handle templating. It also binds to a few common standard library packages such as logging. Everything else is up for extensions. @@ -218,12 +187,5 @@ requirements and Flask could not meet those if it would force any of this into the core. The majority of web applications will need a template engine in some sort. However not every application needs a SQL database. -As your codebase grows, you are free to make the design decisions appropriate -for your project. Flask will continue to provide a very simple glue layer to -the best that Python has to offer. You can implement advanced patterns in -SQLAlchemy or another database tool, introduce non-relational data persistence -as appropriate, and take advantage of framework-agnostic tools built for WSGI, -the Python web interface. - The idea of Flask is to build a good foundation for all applications. Everything else is up to you or extensions. diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index faca58c2..debb9d75 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -1,523 +1,237 @@ +.. _application-errors: + Handling Application Errors =========================== -Applications fail, servers fail. Sooner or later you will see an exception -in production. Even if your code is 100% correct, you will still see -exceptions from time to time. Why? Because everything else involved will -fail. Here are some situations where perfectly fine code can lead to server +.. versionadded:: 0.3 + +Applications fail, servers fail. Sooner or later you will see an exception +in production. Even if your code is 100% correct, you will still see +exceptions from time to time. Why? Because everything else involved will +fail. Here some situations where perfectly fine code can lead to server errors: - the client terminated the request early and the application was still - reading from the incoming data -- the database server was overloaded and could not handle the query + reading from the incoming data. +- the database server was overloaded and could not handle the query. - a filesystem is full - a harddrive crashed - a backend server overloaded - a programming error in a library you are using -- network connection of the server to another system failed +- network connection of the server to another system failed. -And that's just a small sample of issues you could be facing. So how do we -deal with that sort of problem? By default if your application runs in -production mode, and an exception is raised Flask will display a very simple -page for you and log the exception to the :attr:`~flask.Flask.logger`. +And that's just a small sample of issues you could be facing. So how do we +deal with that sort of problem? By default if your application runs in +production mode, Flask will display a very simple page for you and log the +exception to the :attr:`~flask.Flask.logger`. But there is more you can do, and we will cover some better setups to deal -with errors including custom exceptions and 3rd party tools. - - -.. _error-logging-tools: - -Error Logging Tools -------------------- - -Sending error mails, even if just for critical ones, can become -overwhelming if enough users are hitting the error and log files are -typically never looked at. This is why we recommend using `Sentry -`_ for dealing with application errors. It's -available as a source-available project `on GitHub -`_ and is also available as a `hosted version -`_ which you can try for free. Sentry -aggregates duplicate errors, captures the full stack trace and local -variables for debugging, and sends you mails based on new errors or -frequency thresholds. - -To use Sentry you need to install the ``sentry-sdk`` client with extra -``flask`` dependencies. - -.. code-block:: text - - $ pip install sentry-sdk[flask] - -And then add this to your Flask app: - -.. code-block:: python - - import sentry_sdk - from sentry_sdk.integrations.flask import FlaskIntegration - - sentry_sdk.init('YOUR_DSN_HERE', integrations=[FlaskIntegration()]) - -The ``YOUR_DSN_HERE`` value needs to be replaced with the DSN value you -get from your Sentry installation. - -After installation, failures leading to an Internal Server Error -are automatically reported to Sentry and from there you can -receive error notifications. - -See also: - -- Sentry also supports catching errors from a worker queue - (RQ, Celery, etc.) in a similar fashion. See the `Python SDK docs - `__ for more information. -- `Flask-specific documentation `__ - - -Error Handlers --------------- - -When an error occurs in Flask, an appropriate `HTTP status code -`__ will be -returned. 400-499 indicate errors with the client's request data, or -about the data requested. 500-599 indicate errors with the server or -application itself. - -You might want to show custom error pages to the user when an error occurs. -This can be done by registering error handlers. - -An error handler is a function that returns a response when a type of error is -raised, similar to how a view is a function that returns a response when a -request URL is matched. It is passed the instance of the error being handled, -which is most likely a :exc:`~werkzeug.exceptions.HTTPException`. - -The status code of the response will not be set to the handler's code. Make -sure to provide the appropriate HTTP status code when returning a response from -a handler. - - -Registering -``````````` - -Register handlers by decorating a function with -:meth:`~flask.Flask.errorhandler`. Or use -:meth:`~flask.Flask.register_error_handler` to register the function later. -Remember to set the error code when returning the response. - -.. code-block:: python - - @app.errorhandler(werkzeug.exceptions.BadRequest) - def handle_bad_request(e): - return 'bad request!', 400 - - # or, without the decorator - app.register_error_handler(400, handle_bad_request) - -:exc:`werkzeug.exceptions.HTTPException` subclasses like -:exc:`~werkzeug.exceptions.BadRequest` and their HTTP codes are interchangeable -when registering handlers. (``BadRequest.code == 400``) - -Non-standard HTTP codes cannot be registered by code because they are not known -by Werkzeug. Instead, define a subclass of -:class:`~werkzeug.exceptions.HTTPException` with the appropriate code and -register and raise that exception class. - -.. code-block:: python - - class InsufficientStorage(werkzeug.exceptions.HTTPException): - code = 507 - description = 'Not enough storage space.' - - app.register_error_handler(InsufficientStorage, handle_507) - - raise InsufficientStorage() - -Handlers can be registered for any exception class, not just -:exc:`~werkzeug.exceptions.HTTPException` subclasses or HTTP status -codes. Handlers can be registered for a specific class, or for all subclasses -of a parent class. - - -Handling -```````` - -When building a Flask application you *will* run into exceptions. If some part -of your code breaks while handling a request (and you have no error handlers -registered), a "500 Internal Server Error" -(:exc:`~werkzeug.exceptions.InternalServerError`) will be returned by default. -Similarly, "404 Not Found" -(:exc:`~werkzeug.exceptions.NotFound`) error will occur if a request is sent to an unregistered route. -If a route receives an unallowed request method, a "405 Method Not Allowed" -(:exc:`~werkzeug.exceptions.MethodNotAllowed`) will be raised. These are all -subclasses of :class:`~werkzeug.exceptions.HTTPException` and are provided by -default in Flask. - -Flask gives you the ability to raise any HTTP exception registered by -Werkzeug. However, the default HTTP exceptions return simple exception -pages. You might want to show custom error pages to the user when an error occurs. -This can be done by registering error handlers. - -When Flask catches an exception while handling a request, it is first looked up by code. -If no handler is registered for the code, Flask looks up the error by its class hierarchy; the most specific handler is chosen. -If no handler is registered, :class:`~werkzeug.exceptions.HTTPException` subclasses show a -generic message about their code, while other exceptions are converted to a -generic "500 Internal Server Error". - -For example, if an instance of :exc:`ConnectionRefusedError` is raised, -and a handler is registered for :exc:`ConnectionError` and -:exc:`ConnectionRefusedError`, the more specific :exc:`ConnectionRefusedError` -handler is called with the exception instance to generate the response. - -Handlers registered on the blueprint take precedence over those registered -globally on the application, assuming a blueprint is handling the request that -raises the exception. However, the blueprint cannot handle 404 routing errors -because the 404 occurs at the routing level before the blueprint can be -determined. - - -Generic Exception Handlers -`````````````````````````` - -It is possible to register error handlers for very generic base classes -such as ``HTTPException`` or even ``Exception``. However, be aware that -these will catch more than you might expect. - -For example, an error handler for ``HTTPException`` might be useful for turning -the default HTML errors pages into JSON. However, this -handler will trigger for things you don't cause directly, such as 404 -and 405 errors during routing. Be sure to craft your handler carefully -so you don't lose information about the HTTP error. - -.. code-block:: python - - from flask import json - from werkzeug.exceptions import HTTPException - - @app.errorhandler(HTTPException) - def handle_exception(e): - """Return JSON instead of HTML for HTTP errors.""" - # start with the correct headers and status code from the error - response = e.get_response() - # replace the body with JSON - response.data = json.dumps({ - "code": e.code, - "name": e.name, - "description": e.description, - }) - response.content_type = "application/json" - return response - -An error handler for ``Exception`` might seem useful for changing how -all errors, even unhandled ones, are presented to the user. However, -this is similar to doing ``except Exception:`` in Python, it will -capture *all* otherwise unhandled errors, including all HTTP status -codes. - -In most cases it will be safer to register handlers for more -specific exceptions. Since ``HTTPException`` instances are valid WSGI -responses, you could also pass them through directly. - -.. code-block:: python - - from werkzeug.exceptions import HTTPException - - @app.errorhandler(Exception) - def handle_exception(e): - # pass through HTTP errors - if isinstance(e, HTTPException): - return e - - # now you're handling non-HTTP exceptions only - return render_template("500_generic.html", e=e), 500 - -Error handlers still respect the exception class hierarchy. If you -register handlers for both ``HTTPException`` and ``Exception``, the -``Exception`` handler will not handle ``HTTPException`` subclasses -because the ``HTTPException`` handler is more specific. - - -Unhandled Exceptions -```````````````````` - -When there is no error handler registered for an exception, a 500 -Internal Server Error will be returned instead. See -:meth:`flask.Flask.handle_exception` for information about this -behavior. - -If there is an error handler registered for ``InternalServerError``, -this will be invoked. As of Flask 1.1.0, this error handler will always -be passed an instance of ``InternalServerError``, not the original -unhandled error. - -The original error is available as ``e.original_exception``. - -An error handler for "500 Internal Server Error" will be passed uncaught -exceptions in addition to explicit 500 errors. In debug mode, a handler -for "500 Internal Server Error" will not be used. Instead, the -interactive debugger will be shown. - - -Custom Error Pages ------------------- - -Sometimes when building a Flask application, you might want to raise a -:exc:`~werkzeug.exceptions.HTTPException` to signal to the user that -something is wrong with the request. Fortunately, Flask comes with a handy -:func:`~flask.abort` function that aborts a request with a HTTP error from -werkzeug as desired. It will also provide a plain black and white error page -for you with a basic description, but nothing fancy. - -Depending on the error code it is less or more likely for the user to -actually see such an error. - -Consider the code below, we might have a user profile route, and if the user -fails to pass a username we can raise a "400 Bad Request". If the user passes a -username and we can't find it, we raise a "404 Not Found". - -.. code-block:: python - - from flask import abort, render_template, request - - # a username needs to be supplied in the query args - # a successful request would be like /profile?username=jack - @app.route("/profile") - def user_profile(): - username = request.arg.get("username") - # if a username isn't supplied in the request, return a 400 bad request - if username is None: - abort(400) - - user = get_user(username=username) - # if a user can't be found by their username, return 404 not found - if user is None: - abort(404) - - return render_template("profile.html", user=user) - -Here is another example implementation for a "404 Page Not Found" exception: - -.. code-block:: python - - from flask import render_template - - @app.errorhandler(404) - def page_not_found(e): - # note that we set the 404 status explicitly - return render_template('404.html'), 404 - -When using :doc:`/patterns/appfactories`: - -.. code-block:: python - - from flask import Flask, render_template - - def page_not_found(e): - return render_template('404.html'), 404 - - def create_app(config_filename): - app = Flask(__name__) - app.register_error_handler(404, page_not_found) - return app - -An example template might be this: - -.. code-block:: html+jinja - - {% extends "layout.html" %} - {% block title %}Page Not Found{% endblock %} - {% block body %} -

Page Not Found

-

What you were looking for is just not there. -

go somewhere nice - {% endblock %} - - -Further Examples -```````````````` - -The above examples wouldn't actually be an improvement on the default -exception pages. We can create a custom 500.html template like this: - -.. code-block:: html+jinja - - {% extends "layout.html" %} - {% block title %}Internal Server Error{% endblock %} - {% block body %} -

Internal Server Error

-

Oops... we seem to have made a mistake, sorry!

-

Go somewhere nice instead - {% endblock %} - -It can be implemented by rendering the template on "500 Internal Server Error": - -.. code-block:: python - - from flask import render_template - - @app.errorhandler(500) - def internal_server_error(e): - # note that we set the 500 status explicitly - return render_template('500.html'), 500 - -When using :doc:`/patterns/appfactories`: - -.. code-block:: python - - from flask import Flask, render_template - - def internal_server_error(e): - return render_template('500.html'), 500 - - def create_app(): - app = Flask(__name__) - app.register_error_handler(500, internal_server_error) - return app - -When using :doc:`/blueprints`: - -.. code-block:: python - - from flask import Blueprint - - blog = Blueprint('blog', __name__) - - # as a decorator - @blog.errorhandler(500) - def internal_server_error(e): - return render_template('500.html'), 500 - - # or with register_error_handler - blog.register_error_handler(500, internal_server_error) - - -Blueprint Error Handlers ------------------------- - -In :doc:`/blueprints`, most error handlers will work as expected. -However, there is a caveat concerning handlers for 404 and 405 -exceptions. These error handlers are only invoked from an appropriate -``raise`` statement or a call to ``abort`` in another of the blueprint's -view functions; they are not invoked by, e.g., an invalid URL access. - -This is because the blueprint does not "own" a certain URL space, so -the application instance has no way of knowing which blueprint error -handler it should run if given an invalid URL. If you would like to -execute different handling strategies for these errors based on URL -prefixes, they may be defined at the application level using the -``request`` proxy object. - -.. code-block:: python - - from flask import jsonify, render_template - - # at the application level - # not the blueprint level - @app.errorhandler(404) - def page_not_found(e): - # if a request is in our blog URL space - if request.path.startswith('/blog/'): - # we return a custom blog 404 page - return render_template("blog/404.html"), 404 - else: - # otherwise we return our generic site-wide 404 page - return render_template("404.html"), 404 - - @app.errorhandler(405) - def method_not_allowed(e): - # if a request has the wrong method to our API - if request.path.startswith('/api/'): - # we return a json saying so - return jsonify(message="Method Not Allowed"), 405 - else: - # otherwise we return a generic site-wide 405 page - return render_template("405.html"), 405 - - -Returning API Errors as JSON ----------------------------- - -When building APIs in Flask, some developers realise that the built-in -exceptions are not expressive enough for APIs and that the content type of -:mimetype:`text/html` they are emitting is not very useful for API consumers. - -Using the same techniques as above and :func:`~flask.json.jsonify` we can return JSON -responses to API errors. :func:`~flask.abort` is called -with a ``description`` parameter. The error handler will -use that as the JSON error message, and set the status code to 404. - -.. code-block:: python - - from flask import abort, jsonify - - @app.errorhandler(404) - def resource_not_found(e): - return jsonify(error=str(e)), 404 - - @app.route("/cheese") - def get_one_cheese(): - resource = get_resource() - - if resource is None: - abort(404, description="Resource not found") - - return jsonify(resource) - -We can also create custom exception classes. For instance, we can -introduce a new custom exception for an API that can take a proper human readable message, -a status code for the error and some optional payload to give more context -for the error. - -This is a simple example: - -.. code-block:: python - - from flask import jsonify, request - - class InvalidAPIUsage(Exception): - status_code = 400 - - def __init__(self, message, status_code=None, payload=None): - super().__init__() - self.message = message - if status_code is not None: - self.status_code = status_code - self.payload = payload - - def to_dict(self): - rv = dict(self.payload or ()) - rv['message'] = self.message - return rv - - @app.errorhandler(InvalidAPIUsage) - def invalid_api_usage(e): - return jsonify(e.to_dict()), e.status_code - - # an API app route for getting user information - # a correct request might be /api/user?user_id=420 - @app.route("/api/user") - def user_api(user_id): - user_id = request.arg.get("user_id") - if not user_id: - raise InvalidAPIUsage("No user id provided!") - - user = get_user(user_id=user_id) - if not user: - raise InvalidAPIUsage("No such user!", status_code=404) - - return jsonify(user.to_dict()) - -A view can now raise that exception with an error message. Additionally -some extra payload can be provided as a dictionary through the `payload` -parameter. - - -Logging -------- - -See :doc:`/logging` for information about how to log exceptions, such as -by emailing them to admins. - - -Debugging ---------- - -See :doc:`/debugging` for information about how to debug errors in -development and production. +with errors. + +Error Mails +----------- + +If the application runs in production mode (which it will do on your +server) you won't see any log messages by default. Why is that? Flask +tries to be a zero-configuration framework. Where should it drop the logs +for you if there is no configuration? Guessing is not a good idea because +chances are, the place it guessed is not the place where the user has +permission to create a logfile. Also, for most small applications nobody +will look at the logs anyways. + +In fact, I promise you right now that if you configure a logfile for the +application errors you will never look at it except for debugging an issue +when a user reported it for you. What you want instead is a mail the +second the exception happened. Then you get an alert and you can do +something about it. + +Flask uses the Python builtin logging system, and it can actually send +you mails for errors which is probably what you want. Here is how you can +configure the Flask logger to send you mails for exceptions:: + + ADMINS = ['yourname@example.com'] + if not app.debug: + import logging + from logging.handlers import SMTPHandler + mail_handler = SMTPHandler('127.0.0.1', + 'server-error@example.com', + ADMINS, 'YourApplication Failed') + mail_handler.setLevel(logging.ERROR) + app.logger.addHandler(mail_handler) + +So what just happened? We created a new +:class:`~logging.handlers.SMTPHandler` that will send mails with the mail +server listening on ``127.0.0.1`` to all the `ADMINS` from the address +*server-error@example.com* with the subject "YourApplication Failed". If +your mail server requires credentials, these can also be provided. For +that check out the documentation for the +:class:`~logging.handlers.SMTPHandler`. + +We also tell the handler to only send errors and more critical messages. +Because we certainly don't want to get a mail for warnings or other +useless logs that might happen during request handling. + +Before you run that in production, please also look at :ref:`logformat` to +put more information into that error mail. That will save you from a lot +of frustration. + + +Logging to a File +----------------- + +Even if you get mails, you probably also want to log warnings. It's a +good idea to keep as much information around that might be required to +debug a problem. Please note that Flask itself will not issue any +warnings in the core system, so it's your responsibility to warn in the +code if something seems odd. + +There are a couple of handlers provided by the logging system out of the +box but not all of them are useful for basic error logging. The most +interesting are probably the following: + +- :class:`~logging.FileHandler` - logs messages to a file on the + filesystem. +- :class:`~logging.handlers.RotatingFileHandler` - logs messages to a file + on the filesystem and will rotate after a certain number of messages. +- :class:`~logging.handlers.NTEventLogHandler` - will log to the system + event log of a Windows system. If you are deploying on a Windows box, + this is what you want to use. +- :class:`~logging.handlers.SysLogHandler` - sends logs to a UNIX + syslog. + +Once you picked your log handler, do like you did with the SMTP handler +above, just make sure to use a lower setting (I would recommend +`WARNING`):: + + if not app.debug: + import logging + from themodule import TheHandlerYouWant + file_handler = TheHandlerYouWant(...) + file_handler.setLevel(logging.WARNING) + app.logger.addHandler(file_handler) + +.. _logformat: + +Controlling the Log Format +-------------------------- + +By default a handler will only write the message string into a file or +send you that message as mail. A log record stores more information, +and it makes a lot of sense to configure your logger to also contain that +information so that you have a better idea of why that error happened, and +more importantly, where it did. + +A formatter can be instantiated with a format string. Note that +tracebacks are appended to the log entry automatically. You don't have to +do that in the log formatter format string. + +Here some example setups: + +Email +````` + +:: + + from logging import Formatter + mail_handler.setFormatter(Formatter(''' + Message type: %(levelname)s + Location: %(pathname)s:%(lineno)d + Module: %(module)s + Function: %(funcName)s + Time: %(asctime)s + + Message: + + %(message)s + ''')) + +File logging +```````````` + +:: + + from logging import Formatter + file_handler.setFormatter(Formatter( + '%(asctime)s %(levelname)s: %(message)s ' + '[in %(pathname)s:%(lineno)d]' + )) + + +Complex Log Formatting +`````````````````````` + +Here is a list of useful formatting variables for the format string. Note +that this list is not complete, consult the official documentation of the +:mod:`logging` package for a full list. + +.. tabularcolumns:: |p{3cm}|p{12cm}| + ++------------------+----------------------------------------------------+ +| Format | Description | ++==================+====================================================+ +| ``%(levelname)s``| Text logging level for the message | +| | (``'DEBUG'``, ``'INFO'``, ``'WARNING'``, | +| | ``'ERROR'``, ``'CRITICAL'``). | ++------------------+----------------------------------------------------+ +| ``%(pathname)s`` | Full pathname of the source file where the | +| | logging call was issued (if available). | ++------------------+----------------------------------------------------+ +| ``%(filename)s`` | Filename portion of pathname. | ++------------------+----------------------------------------------------+ +| ``%(module)s`` | Module (name portion of filename). | ++------------------+----------------------------------------------------+ +| ``%(funcName)s`` | Name of function containing the logging call. | ++------------------+----------------------------------------------------+ +| ``%(lineno)d`` | Source line number where the logging call was | +| | issued (if available). | ++------------------+----------------------------------------------------+ +| ``%(asctime)s`` | Human-readable time when the LogRecord` was | +| | created. By default this is of the form | +| | ``"2003-07-08 16:49:45,896"`` (the numbers after | +| | the comma are millisecond portion of the time). | +| | This can be changed by subclassing the formatter | +| | and overriding the | +| | :meth:`~logging.Formatter.formatTime` method. | ++------------------+----------------------------------------------------+ +| ``%(message)s`` | The logged message, computed as ``msg % args`` | ++------------------+----------------------------------------------------+ + +If you want to further customize the formatting, you can subclass the +formatter. The formatter has three interesting methods: + +:meth:`~logging.Formatter.format`: + handles the actual formatting. It is passed a + :class:`~logging.LogRecord` object and has to return the formatted + string. +:meth:`~logging.Formatter.formatTime`: + called for `asctime` formatting. If you want a different time format + you can override this method. +:meth:`~logging.Formatter.formatException` + called for exception formatting. It is passed an :attr:`~sys.exc_info` + tuple and has to return a string. The default is usually fine, you + don't have to override it. + +For more information, head over to the official documentation. + + +Other Libraries +--------------- + +So far we only configured the logger your application created itself. +Other libraries might log themselves as well. For example, SQLAlchemy uses +logging heavily in its core. While there is a method to configure all +loggers at once in the :mod:`logging` package, I would not recommend using +it. There might be a situation in which you want to have multiple +separate applications running side by side in the same Python interpreter +and then it becomes impossible to have different logging setups for those. + +Instead, I would recommend figuring out which loggers you are interested +in, getting the loggers with the :func:`~logging.getLogger` function and +iterating over them to attach handlers:: + + from logging import getLogger + loggers = [app.logger, getLogger('sqlalchemy'), + getLogger('otherlibrary')] + for logger in loggers: + logger.addHandler(mail_handler) + logger.addHandler(file_handler) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index ae5ed330..ee0d5e60 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -1,305 +1,387 @@ Flask Extension Development =========================== -.. currentmodule:: flask +Flask, being a microframework, often requires some repetitive steps to get +a third party library working. Because very often these steps could be +abstracted to support multiple projects the `Flask Extension Registry`_ +was created. -Extensions are extra packages that add functionality to a Flask -application. While `PyPI`_ contains many Flask extensions, you may not -find one that fits your need. If this is the case, you can create your -own, and publish it for others to use as well. +If you want to create your own Flask extension for something that does not +exist yet, this guide to extension development will help you get your +extension running in no time and to feel like users would expect your +extension to behave. -This guide will show how to create a Flask extension, and some of the -common patterns and requirements involved. Since extensions can do -anything, this guide won't be able to cover every possibility. +.. _Flask Extension Registry: http://flask.pocoo.org/extensions/ -The best ways to learn about extensions are to look at how other -extensions you use are written, and discuss with others. Discuss your -design ideas with others on our `Discord Chat`_ or -`GitHub Discussions`_. +Anatomy of an Extension +----------------------- -The best extensions share common patterns, so that anyone familiar with -using one extension won't feel completely lost with another. This can -only work if collaboration happens early. +Extensions are all located in a package called ``flask_something`` +where "something" is the name of the library you want to bridge. So for +example if you plan to add support for a library named `simplexml` to +Flask, you would name your extension's package ``flask_simplexml``. + +The name of the actual extension (the human readable name) however would +be something like "Flask-SimpleXML". Make sure to include the name +"Flask" somewhere in that name and that you check the capitalization. +This is how users can then register dependencies to your extension in +their `setup.py` files. + +Flask sets up a redirect package called :data:`flask.ext` where users +should import the extensions from. If you for instance have a package +called ``flask_something`` users would import it as +``flask.ext.something``. This is done to transition from the old +namespace packages. See :ref:`ext-import-transition` for more details. + +But how do extensions look like themselves? An extension has to ensure +that it works with multiple Flask application instances at once. This is +a requirement because many people will use patterns like the +:ref:`app-factories` pattern to create their application as needed to aid +unittests and to support multiple configurations. Because of that it is +crucial that your application supports that kind of behaviour. + +Most importantly the extension must be shipped with a `setup.py` file and +registered on PyPI. Also the development checkout link should work so +that people can easily install the development version into their +virtualenv without having to download the library by hand. + +Flask extensions must be licensed as BSD or MIT or a more liberal license +to be enlisted on the Flask Extension Registry. Keep in mind that the +Flask Extension Registry is a moderated place and libraries will be +reviewed upfront if they behave as required. + +"Hello Flaskext!" +----------------- + +So let's get started with creating such a Flask extension. The extension +we want to create here will provide very basic support for SQLite3. + +First we create the following folder structure:: + + flask-sqlite3/ + flask_sqlite3.py + LICENSE + README + +Here's the contents of the most important files: + +setup.py +```````` + +The next file that is absolutely required is the `setup.py` file which is +used to install your Flask extension. The following contents are +something you can work with:: + + """ + Flask-SQLite3 + ------------- + + This is the description for that library + """ + from setuptools import setup -Naming ------- + setup( + name='Flask-SQLite3', + version='1.0', + url='http://example.com/flask-sqlite3/', + license='BSD', + author='Your Name', + author_email='your-email@example.com', + description='Very short description', + long_description=__doc__, + py_modules=['flask_sqlite3'], + # if you would be using a package instead use packages instead + # of py_modules: + # packages=['flask_sqlite3'], + zip_safe=False, + include_package_data=True, + platforms='any', + install_requires=[ + 'Flask' + ], + classifiers=[ + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Libraries :: Python Modules' + ] + ) -A Flask extension typically has ``flask`` in its name as a prefix or -suffix. If it wraps another library, it should include the library name -as well. This makes it easy to search for extensions, and makes their -purpose clearer. +That's a lot of code but you can really just copy/paste that from existing +extensions and adapt. -A general Python packaging recommendation is that the install name from -the package index and the name used in ``import`` statements should be -related. The import name is lowercase, with words separated by -underscores (``_``). The install name is either lower case or title -case, with words separated by dashes (``-``). If it wraps another -library, prefer using the same case as that library's name. +flask_sqlite3.py +```````````````` -Here are some example install and import names: +Now this is where your extension code goes. But how exactly should such +an extension look like? What are the best practices? Continue reading +for some insight. -- ``Flask-Name`` imported as ``flask_name`` -- ``flask-name-lower`` imported as ``flask_name_lower`` -- ``Flask-ComboName`` imported as ``flask_comboname`` -- ``Name-Flask`` imported as ``name_flask`` +Initializing Extensions +----------------------- +Many extensions will need some kind of initialization step. For example, +consider your application is currently connecting to SQLite like the +documentation suggests (:ref:`sqlite3`) you will need to provide a few +functions and before / after request handlers. So how does the extension +know the name of the application object? -The Extension Class and Initialization --------------------------------------- +Quite simple: you pass it to it. -All extensions will need some entry point that initializes the -extension with the application. The most common pattern is to create a -class that represents the extension's configuration and behavior, with -an ``init_app`` method to apply the extension instance to the given -application instance. +There are two recommended ways for an extension to initialize: -.. code-block:: python +initialization functions: + If your extension is called `helloworld` you might have a function + called ``init_helloworld(app[, extra_args])`` that initializes the + extension for that application. It could attach before / after + handlers etc. + +classes: + Classes work mostly like initialization functions but can later be + used to further change the behaviour. For an example look at how the + `OAuth extension`_ works: there is an `OAuth` object that provides + some helper functions like `OAuth.remote_app` to create a reference to + a remote application that uses OAuth. + +What to use depends on what you have in mind. For the SQLite 3 extension +we will use the class based approach because it will provide users with a +manager object that handles opening and closing database connections. + +The Extension Code +------------------ + +Here's the contents of the `flask_sqlite3.py` for copy/paste:: + + from __future__ import absolute_import + import sqlite3 + + from flask import _request_ctx_stack + + class SQLite3(object): + + def __init__(self, app): + self.app = app + self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') + self.app.teardown_request(self.teardown_request) + self.app.before_request(self.before_request) + + def connect(self): + return sqlite3.connect(self.app.config['SQLITE3_DATABASE']) + + def before_request(self): + ctx = _request_ctx_stack.top + ctx.sqlite3_db = self.connect() + + def teardown_request(self, exception): + ctx = _request_ctx_stack.top + ctx.sqlite3_db.close() + + def get_db(self): + ctx = _request_ctx_stack.top + if ctx is not None: + return ctx.sqlite3_db + +So here's what these lines of code do: + +1. The ``__future__`` import is necessary to activate absolute imports. + Otherwise we could not call our module `sqlite3.py` and import the + top-level `sqlite3` module which actually implements the connection to + SQLite. +2. We create a class for our extension that requires a supplied `app` object, + sets a configuration for the database if it's not there + (:meth:`dict.setdefault`), and attaches `before_request` and + `teardown_request` handlers. +3. Next, we define a `connect` function that opens a database connection. +4. Then we set up the request handlers we bound to the app above. Note here + that we're attaching our database connection to the top request context via + `_request_ctx_stack.top`. Extensions should use the top context and not the + `g` object to store things like database connections. +5. Finally, we add a `get_db` function that simplifies access to the context's + database. + +So why did we decide on a class based approach here? Because using our +extension looks something like this:: + + from flask import Flask + from flask_sqlite3 import SQLite3 + + app = Flask(__name__) + app.config.from_pyfile('the-config.cfg') + manager = SQLite3(app) + db = manager.get_db() + +You can then use the database from views like this:: + + @app.route('/') + def show_all(): + cur = db.cursor() + cur.execute(...) + +Opening a database connection from outside a view function is simple. + +>>> from yourapplication import db +>>> cur = db.cursor() +>>> cur.execute(...) + +Adding an `init_app` Function +----------------------------- + +In practice, you'll almost always want to permit users to initialize your +extension and provide an app object after the fact. This can help avoid +circular import problems when a user is breaking their app into multiple files. +Our extension could add an `init_app` function as follows:: + + class SQLite3(object): - class HelloExtension: def __init__(self, app=None): if app is not None: - self.init_app(app) + self.app = app + self.init_app(self.app) + else: + self.app = None def init_app(self, app): - app.before_request(...) + self.app = app + self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') + self.app.teardown_request(self.teardown_request) + self.app.before_request(self.before_request) -It is important that the app is not stored on the extension, don't do -``self.app = app``. The only time the extension should have direct -access to an app is during ``init_app``, otherwise it should use -:data:`.current_app`. + def connect(self): + return sqlite3.connect(app.config['SQLITE3_DATABASE']) -This allows the extension to support the application factory pattern, -avoids circular import issues when importing the extension instance -elsewhere in a user's code, and makes testing with different -configurations easier. + def before_request(self): + ctx = _request_ctx_stack.top + ctx.sqlite3_db = self.connect() -.. code-block:: python + def teardown_request(self, exception): + ctx = _request_ctx_stack.top + ctx.sqlite3_db.close() - hello = HelloExtension() + def get_db(self): + ctx = _request_ctx_stack.top + if ctx is not None: + return ctx.sqlite3_db - def create_app(): - app = Flask(__name__) - hello.init_app(app) - return app +The user could then initialize the extension in one file:: -Above, the ``hello`` extension instance exists independently of the -application. This means that other modules in a user's project can do -``from project import hello`` and use the extension in blueprints before -the app exists. + manager = SQLite3() -The :attr:`Flask.extensions` dict can be used to store a reference to -the extension on the application, or some other state specific to the -application. Be aware that this is a single namespace, so use a name -unique to your extension, such as the extension's name without the -"flask" prefix. +and bind their app to the extension in another file:: + + manager.init_app(app) + +End-Of-Request Behavior +----------------------- + +Due to the change in Flask 0.7 regarding functions that are run at the end +of the request your extension will have to be extra careful there if it +wants to continue to support older versions of Flask. The following +pattern is a good way to support both:: + + def close_connection(response): + ctx = _request_ctx_stack.top + ctx.sqlite3_db.close() + return response + + if hasattr(app, 'teardown_request'): + app.teardown_request(close_connection) + else: + app.after_request(close_connection) + +Strictly speaking the above code is wrong, because teardown functions are +passed the exception and typically don't return anything. However because +the return value is discarded this will just work assuming that the code +in between does not touch the passed parameter. + +Learn from Others +----------------- + +This documentation only touches the bare minimum for extension +development. If you want to learn more, it's a very good idea to check +out existing extensions on the `Flask Extension Registry`_. If you feel +lost there is still the `mailinglist`_ and the `IRC channel`_ to get some +ideas for nice looking APIs. Especially if you do something nobody before +you did, it might be a very good idea to get some more input. This not +only to get an idea about what people might want to have from an +extension, but also to avoid having multiple developers working on pretty +much the same side by side. + +Remember: good API design is hard, so introduce your project on the +mailinglist, and let other developers give you a helping hand with +designing the API. + +The best Flask extensions are extensions that share common idioms for the +API. And this can only work if collaboration happens early. + +Approved Extensions +------------------- + +Flask also has the concept of approved extensions. Approved extensions +are tested as part of Flask itself to ensure extensions do not break on +new releases. These approved extensions are listed on the `Flask +Extension Registry`_ and marked appropriately. If you want your own +extension to be approved you have to follow these guidelines: + +1. An approved Flask extension must provide exactly one package or module + named ``flask_extensionname``. They might also reside inside a + ``flaskext`` namespace packages though this is discouraged now. +2. It must ship a testing suite that can either be invoked with ``make test`` + or ``python setup.py test``. For test suites invoked with ``make + test`` the extension has to ensure that all dependencies for the test + are installed automatically, in case of ``python setup.py test`` + dependencies for tests alone can be specified in the `setup.py` + file. The test suite also has to be part of the distribution. +3. APIs of approved extensions will be checked for the following + characteristics: + + - an approved extension has to support multiple applications + running in the same Python process. + - it must be possible to use the factory pattern for creating + applications. + +4. The license must be BSD/MIT/WTFPL licensed. +5. The naming scheme for official extensions is *Flask-ExtensionName* or + *ExtensionName-Flask*. +6. Approved extensions must define all their dependencies in the + `setup.py` file unless a dependency cannot be met because it is not + available on PyPI. +7. The extension must have documentation that uses one of the two Flask + themes for Sphinx documentation. +8. The setup.py description (and thus the PyPI description) has to + link to the documentation, website (if there is one) and there + must be a link to automatically install the development version + (``PackageName==dev``). +9. The ``zip_safe`` flag in the setup script must be set to ``False``, + even if the extension would be safe for zipping. +10. An extension currently has to support Python 2.5, 2.6 as well as + Python 2.7 -Adding Behavior ---------------- +.. _ext-import-transition: -There are many ways that an extension can add behavior. Any setup -methods that are available on the :class:`Flask` object can be used -during an extension's ``init_app`` method. +Extension Import Transition +--------------------------- -A common pattern is to use :meth:`~Flask.before_request` to initialize -some data or a connection at the beginning of each request, then -:meth:`~Flask.teardown_request` to clean it up at the end. This can be -stored on :data:`.g`, discussed more below. +For a while we recommended using namespace packages for Flask extensions. +This turned out to be problematic in practice because many different +competing namespace package systems exist and pip would automatically +switch between different systems and this caused a lot of problems for +users. -A more lazy approach is to provide a method that initializes and caches -the data or connection. For example, a ``ext.get_db`` method could -create a database connection the first time it's called, so that a view -that doesn't use the database doesn't create a connection. +Instead we now recommend naming packages ``flask_foo`` instead of the now +deprecated ``flaskext.foo``. Flask 0.8 introduces a redirect import +system that lets uses import from ``flask.ext.foo`` and it will try +``flask_foo`` first and if that fails ``flaskext.foo``. -Besides doing something before and after every view, your extension -might want to add some specific views as well. In this case, you could -define a :class:`Blueprint`, then call :meth:`~Flask.register_blueprint` -during ``init_app`` to add the blueprint to the app. +Flask extensions should urge users to import from ``flask.ext.foo`` +instead of ``flask_foo`` or ``flaskext_foo`` so that extensions can +transition to the new package name without affecting users. -Configuration Techniques ------------------------- - -There can be multiple levels and sources of configuration for an -extension. You should consider what parts of your extension fall into -each one. - -- Configuration per application instance, through ``app.config`` - values. This is configuration that could reasonably change for each - deployment of an application. A common example is a URL to an - external resource, such as a database. Configuration keys should - start with the extension's name so that they don't interfere with - other extensions. -- Configuration per extension instance, through ``__init__`` - arguments. This configuration usually affects how the extension - is used, such that it wouldn't make sense to change it per - deployment. -- Configuration per extension instance, through instance attributes - and decorator methods. It might be more ergonomic to assign to - ``ext.value``, or use a ``@ext.register`` decorator to register a - function, after the extension instance has been created. -- Global configuration through class attributes. Changing a class - attribute like ``Ext.connection_class`` can customize default - behavior without making a subclass. This could be combined - per-extension configuration to override defaults. -- Subclassing and overriding methods and attributes. Making the API of - the extension itself something that can be overridden provides a - very powerful tool for advanced customization. - -The :class:`~flask.Flask` object itself uses all of these techniques. - -It's up to you to decide what configuration is appropriate for your -extension, based on what you need and what you want to support. - -Configuration should not be changed after the application setup phase is -complete and the server begins handling requests. Configuration is -global, any changes to it are not guaranteed to be visible to other -workers. - - -Data During a Request ---------------------- - -When writing a Flask application, the :data:`~flask.g` object is used to -store information during a request. For example the -:doc:`tutorial ` stores a connection to a SQLite -database as ``g.db``. Extensions can also use this, with some care. -Since ``g`` is a single global namespace, extensions must use unique -names that won't collide with user data. For example, use the extension -name as a prefix, or as a namespace. - -.. code-block:: python - - # an internal prefix with the extension name - g._hello_user_id = 2 - - # or an internal prefix as a namespace - from types import SimpleNamespace - g._hello = SimpleNamespace() - g._hello.user_id = 2 - -The data in ``g`` lasts for an application context. An application context is -active during a request, CLI command, or ``with app.app_context()`` block. If -you're storing something that should be closed, use -:meth:`~flask.Flask.teardown_appcontext` to ensure that it gets closed when the -app context ends. If it should only be valid during a request, or would not be -used in the CLI outside a request, use :meth:`~flask.Flask.teardown_request`. - - -Views and Models ----------------- - -Your extension views might want to interact with specific models in your -database, or some other extension or data connected to your application. -For example, let's consider a ``Flask-SimpleBlog`` extension that works -with Flask-SQLAlchemy to provide a ``Post`` model and views to write -and read posts. - -The ``Post`` model needs to subclass the Flask-SQLAlchemy ``db.Model`` -object, but that's only available once you've created an instance of -that extension, not when your extension is defining its views. So how -can the view code, defined before the model exists, access the model? - -One method could be to use :doc:`views`. During ``__init__``, create -the model, then create the views by passing the model to the view -class's :meth:`~views.View.as_view` method. - -.. code-block:: python - - class PostAPI(MethodView): - def __init__(self, model): - self.model = model - - def get(self, id): - post = self.model.query.get(id) - return jsonify(post.to_json()) - - class BlogExtension: - def __init__(self, db): - class Post(db.Model): - id = db.Column(primary_key=True) - title = db.Column(db.String, nullable=False) - - self.post_model = Post - - def init_app(self, app): - api_view = PostAPI.as_view(model=self.post_model) - - db = SQLAlchemy() - blog = BlogExtension(db) - db.init_app(app) - blog.init_app(app) - -Another technique could be to use an attribute on the extension, such as -``self.post_model`` from above. Add the extension to ``app.extensions`` -in ``init_app``, then access -``current_app.extensions["simple_blog"].post_model`` from views. - -You may also want to provide base classes so that users can provide -their own ``Post`` model that conforms to the API your extension -expects. So they could implement ``class Post(blog.BasePost)``, then -set it as ``blog.post_model``. - -As you can see, this can get a bit complex. Unfortunately, there's no -perfect solution here, only different strategies and tradeoffs depending -on your needs and how much customization you want to offer. Luckily, -this sort of resource dependency is not a common need for most -extensions. Remember, if you need help with design, ask on our -`Discord Chat`_ or `GitHub Discussions`_. - - -Recommended Extension Guidelines --------------------------------- - -Flask previously had the concept of "approved extensions", where the -Flask maintainers evaluated the quality, support, and compatibility of -the extensions before listing them. While the list became too difficult -to maintain over time, the guidelines are still relevant to all -extensions maintained and developed today, as they help the Flask -ecosystem remain consistent and compatible. - -1. An extension requires a maintainer. In the event an extension author - would like to move beyond the project, the project should find a new - maintainer and transfer access to the repository, documentation, - PyPI, and any other services. The `Pallets-Eco`_ organization on - GitHub allows for community maintenance with oversight from the - Pallets maintainers. -2. The naming scheme is *Flask-ExtensionName* or *ExtensionName-Flask*. - It must provide exactly one package or module named - ``flask_extension_name``. -3. The extension must use an open source license. The Python web - ecosystem tends to prefer BSD or MIT. It must be open source and - publicly available. -4. The extension's API must have the following characteristics: - - - It must support multiple applications running in the same Python - process. Use ``current_app`` instead of ``self.app``, store - configuration and state per application instance. - - It must be possible to use the factory pattern for creating - applications. Use the ``ext.init_app()`` pattern. - -5. From a clone of the repository, an extension with its dependencies - must be installable in editable mode with ``pip install -e .``. -6. It must ship tests that can be invoked with a common tool like - ``tox -e py``, ``nox -s test`` or ``pytest``. If not using ``tox``, - the test dependencies should be specified in a requirements file. - The tests must be part of the sdist distribution. -7. A link to the documentation or project website must be in the PyPI - metadata or the readme. The documentation should use the Flask theme - from the `Official Pallets Themes`_. -8. The extension's dependencies should not use upper bounds or assume - any particular version scheme, but should use lower bounds to - indicate minimum compatibility support. For example, - ``sqlalchemy>=1.4``. -9. Indicate the versions of Python supported using ``python_requires=">=version"``. - Flask and Pallets policy is to support all Python versions that are not - within six months of end of life (EOL). See Python's `EOL calendar`_ for - timing. - -.. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask -.. _Discord Chat: https://discord.gg/pallets -.. _GitHub Discussions: https://github.com/pallets/flask/discussions -.. _Official Pallets Themes: https://pypi.org/project/Pallets-Sphinx-Themes/ -.. _Pallets-Eco: https://github.com/pallets-eco -.. _EOL calendar: https://devguide.python.org/versions/ +.. _OAuth extension: http://packages.python.org/Flask-OAuth/ +.. _mailinglist: http://flask.pocoo.org/mailinglist/ +.. _IRC channel: http://flask.pocoo.org/community/irc/ diff --git a/docs/extensions.rst b/docs/extensions.rst index 4713ec8e..53dca56e 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -1,48 +1,48 @@ -Extensions -========== - -Extensions are extra packages that add functionality to a Flask -application. For example, an extension might add support for sending -email or connecting to a database. Some extensions add entire new -frameworks to help build certain types of applications, like a REST API. +Flask Extensions +================ +Flask extensions extend the functionality of Flask in various different +ways. For instance they add support for databases and other common tasks. Finding Extensions ------------------ -Flask extensions are usually named "Flask-Foo" or "Foo-Flask". You can -search PyPI for packages tagged with `Framework :: Flask `_. - +Flask extensions are listed on the `Flask Extension Registry`_ and can be +downloaded with ``easy_install`` or ``pip``. If you add a Flask extension +as dependency to your ``requirements.rst`` or ``setup.py`` file they are +usually installed with a simple command or when your application installs. Using Extensions ---------------- -Consult each extension's documentation for installation, configuration, -and usage instructions. Generally, extensions pull their own -configuration from :attr:`app.config ` and are -passed an application instance during initialization. For example, -an extension called "Flask-Foo" might be used like this:: +Extensions typically have documentation that goes along that shows how to +use it. There are no general rules in how extensions are supposed to +behave but they are imported from common locations. If you have an +extension called ``Flask-Foo`` or ``Foo-Flask`` it will be always +importable from ``flask.ext.foo``:: - from flask_foo import Foo + from flask.ext import foo - foo = Foo() +Flask Before 0.8 +---------------- - app = Flask(__name__) - app.config.update( - FOO_BAR='baz', - FOO_SPAM='eggs', - ) +If you are using Flask 0.7 or earlier the :data:`flask.ext` package will not +exist, instead you have to import from ``flaskext.foo`` or ``flask_foo`` +depending on how the extension is distributed. If you want to develop an +application that supports Flask 0.7 or earlier you should still import +from the :data:`flask.ext` package. We provide you with a compatibility +module that provides this package for older versions of Flask. You can +download it from github: `flaskext_compat.py`_ - foo.init_app(app) +And here is how you can use it:: + import flaskext_compat + flaskext_compat.activate() -Building Extensions -------------------- + from flask.ext import foo -While `PyPI `_ contains many Flask extensions, you may not find -an extension that fits your need. If this is the case, you can create -your own, and publish it for others to use as well. Read -:doc:`extensiondev` to develop your own Flask extension. +Once the ``flaskext_compat`` module is activated the :data:`flask.ext` will +exist and you can start importing from there. - -.. _pypi: https://pypi.org/search/?c=Framework+%3A%3A+Flask +.. _Flask Extension Registry: http://flask.pocoo.org/extensions/ +.. _flaskext_compat.py: https://github.com/mitsuhiko/flask/raw/master/scripts/flaskext_compat.py diff --git a/docs/flaskdocext.py b/docs/flaskdocext.py new file mode 100644 index 00000000..db4cfd20 --- /dev/null +++ b/docs/flaskdocext.py @@ -0,0 +1,16 @@ +import re +import inspect + + +_internal_mark_re = re.compile(r'^\s*:internal:\s*$(?m)') + + +def skip_member(app, what, name, obj, skip, options): + docstring = inspect.getdoc(obj) + if skip: + return True + return _internal_mark_re.search(docstring or '') is not None + + +def setup(app): + app.connect('autodoc-skip-member', skip_member) diff --git a/docs/flaskext.py b/docs/flaskext.py new file mode 100644 index 00000000..33f47449 --- /dev/null +++ b/docs/flaskext.py @@ -0,0 +1,86 @@ +# flasky extensions. flasky pygments style based on tango style +from pygments.style import Style +from pygments.token import Keyword, Name, Comment, String, Error, \ + Number, Operator, Generic, Whitespace, Punctuation, Other, Literal + + +class FlaskyStyle(Style): + background_color = "#f8f8f8" + default_style = "" + + styles = { + # No corresponding class for the following: + #Text: "", # class: '' + Whitespace: "underline #f8f8f8", # class: 'w' + Error: "#a40000 border:#ef2929", # class: 'err' + Other: "#000000", # class 'x' + + Comment: "italic #8f5902", # class: 'c' + Comment.Preproc: "noitalic", # class: 'cp' + + Keyword: "bold #004461", # class: 'k' + Keyword.Constant: "bold #004461", # class: 'kc' + Keyword.Declaration: "bold #004461", # class: 'kd' + Keyword.Namespace: "bold #004461", # class: 'kn' + Keyword.Pseudo: "bold #004461", # class: 'kp' + Keyword.Reserved: "bold #004461", # class: 'kr' + Keyword.Type: "bold #004461", # class: 'kt' + + Operator: "#582800", # class: 'o' + Operator.Word: "bold #004461", # class: 'ow' - like keywords + + Punctuation: "bold #000000", # class: 'p' + + # because special names such as Name.Class, Name.Function, etc. + # are not recognized as such later in the parsing, we choose them + # to look the same as ordinary variables. + Name: "#000000", # class: 'n' + Name.Attribute: "#c4a000", # class: 'na' - to be revised + Name.Builtin: "#004461", # class: 'nb' + Name.Builtin.Pseudo: "#3465a4", # class: 'bp' + Name.Class: "#000000", # class: 'nc' - to be revised + Name.Constant: "#000000", # class: 'no' - to be revised + Name.Decorator: "#888", # class: 'nd' - to be revised + Name.Entity: "#ce5c00", # class: 'ni' + Name.Exception: "bold #cc0000", # class: 'ne' + Name.Function: "#000000", # class: 'nf' + Name.Property: "#000000", # class: 'py' + Name.Label: "#f57900", # class: 'nl' + Name.Namespace: "#000000", # class: 'nn' - to be revised + Name.Other: "#000000", # class: 'nx' + Name.Tag: "bold #004461", # class: 'nt' - like a keyword + Name.Variable: "#000000", # class: 'nv' - to be revised + Name.Variable.Class: "#000000", # class: 'vc' - to be revised + Name.Variable.Global: "#000000", # class: 'vg' - to be revised + Name.Variable.Instance: "#000000", # class: 'vi' - to be revised + + Number: "#990000", # class: 'm' + + Literal: "#000000", # class: 'l' + Literal.Date: "#000000", # class: 'ld' + + String: "#4e9a06", # class: 's' + String.Backtick: "#4e9a06", # class: 'sb' + String.Char: "#4e9a06", # class: 'sc' + String.Doc: "italic #8f5902", # class: 'sd' - like a comment + String.Double: "#4e9a06", # class: 's2' + String.Escape: "#4e9a06", # class: 'se' + String.Heredoc: "#4e9a06", # class: 'sh' + String.Interpol: "#4e9a06", # class: 'si' + String.Other: "#4e9a06", # class: 'sx' + String.Regex: "#4e9a06", # class: 'sr' + String.Single: "#4e9a06", # class: 's1' + String.Symbol: "#4e9a06", # class: 'ss' + + Generic: "#000000", # class: 'g' + Generic.Deleted: "#a40000", # class: 'gd' + Generic.Emph: "italic #000000", # class: 'ge' + Generic.Error: "#ef2929", # class: 'gr' + Generic.Heading: "bold #000080", # class: 'gh' + Generic.Inserted: "#00A000", # class: 'gi' + Generic.Output: "#888", # class: 'go' + Generic.Prompt: "#745334", # class: 'gp' + Generic.Strong: "bold #000000", # class: 'gs' + Generic.Subheading: "bold #800080", # class: 'gu' + Generic.Traceback: "bold #a40000", # class: 'gt' + } diff --git a/docs/flaskstyle.sty b/docs/flaskstyle.sty new file mode 100644 index 00000000..8a3f75c3 --- /dev/null +++ b/docs/flaskstyle.sty @@ -0,0 +1,118 @@ +\definecolor{TitleColor}{rgb}{0,0,0} +\definecolor{InnerLinkColor}{rgb}{0,0,0} + +\renewcommand{\maketitle}{% + \begin{titlepage}% + \let\footnotesize\small + \let\footnoterule\relax + \ifsphinxpdfoutput + \begingroup + % This \def is required to deal with multi-line authors; it + % changes \\ to ', ' (comma-space), making it pass muster for + % generating document info in the PDF file. + \def\\{, } + \pdfinfo{ + /Author (\@author) + /Title (\@title) + } + \endgroup + \fi + \begin{flushright}% + %\sphinxlogo% + {\center + \vspace*{3cm} + \includegraphics{logo.pdf} + \vspace{3cm} + \par + {\rm\Huge \@title \par}% + {\em\LARGE \py@release\releaseinfo \par} + {\large + \@date \par + \py@authoraddress \par + }}% + \end{flushright}%\par + \@thanks + \end{titlepage}% + \cleardoublepage% + \setcounter{footnote}{0}% + \let\thanks\relax\let\maketitle\relax + %\gdef\@thanks{}\gdef\@author{}\gdef\@title{} +} + +\fancypagestyle{normal}{ + \fancyhf{} + \fancyfoot[LE,RO]{{\thepage}} + \fancyfoot[LO]{{\nouppercase{\rightmark}}} + \fancyfoot[RE]{{\nouppercase{\leftmark}}} + \fancyhead[LE,RO]{{ \@title, \py@release}} + \renewcommand{\headrulewidth}{0.4pt} + \renewcommand{\footrulewidth}{0.4pt} +} + +\fancypagestyle{plain}{ + \fancyhf{} + \fancyfoot[LE,RO]{{\thepage}} + \renewcommand{\headrulewidth}{0pt} + \renewcommand{\footrulewidth}{0.4pt} +} + +\titleformat{\section}{\Large}% + {\py@TitleColor\thesection}{0.5em}{\py@TitleColor}{\py@NormalColor} +\titleformat{\subsection}{\large}% + {\py@TitleColor\thesubsection}{0.5em}{\py@TitleColor}{\py@NormalColor} +\titleformat{\subsubsection}{}% + {\py@TitleColor\thesubsubsection}{0.5em}{\py@TitleColor}{\py@NormalColor} +\titleformat{\paragraph}{\large}% + {\py@TitleColor}{0em}{\py@TitleColor}{\py@NormalColor} + +\ChNameVar{\raggedleft\normalsize} +\ChNumVar{\raggedleft \bfseries\Large} +\ChTitleVar{\raggedleft \rm\Huge} + +\renewcommand\thepart{\@Roman\c@part} +\renewcommand\part{% + \pagestyle{plain} + \if@noskipsec \leavevmode \fi + \cleardoublepage + \vspace*{6cm}% + \@afterindentfalse + \secdef\@part\@spart} + +\def\@part[#1]#2{% + \ifnum \c@secnumdepth >\m@ne + \refstepcounter{part}% + \addcontentsline{toc}{part}{\thepart\hspace{1em}#1}% + \else + \addcontentsline{toc}{part}{#1}% + \fi + {\parindent \z@ %\center + \interlinepenalty \@M + \normalfont + \ifnum \c@secnumdepth >\m@ne + \rm\Large \partname~\thepart + \par\nobreak + \fi + \MakeUppercase{\rm\Huge #2}% + \markboth{}{}\par}% + \nobreak + \vskip 8ex + \@afterheading} +\def\@spart#1{% + {\parindent \z@ %\center + \interlinepenalty \@M + \normalfont + \huge \bfseries #1\par}% + \nobreak + \vskip 3ex + \@afterheading} + +% use inconsolata font +\usepackage{inconsolata} + +% fix single quotes, for inconsolata. (does not work) +%%\usepackage{textcomp} +%%\begingroup +%% \catcode`'=\active +%% \g@addto@macro\@noligs{\let'\textsinglequote} +%% \endgroup +%%\endinput diff --git a/docs/foreword.rst b/docs/foreword.rst new file mode 100644 index 00000000..10b886bf --- /dev/null +++ b/docs/foreword.rst @@ -0,0 +1,100 @@ +Foreword +======== + +Read this before you get started with Flask. This hopefully answers some +questions about the purpose and goals of the project, and when you +should or should not be using it. + +What does "micro" mean? +----------------------- + +To me, the "micro" in microframework refers not only to the simplicity and +small size of the framework, but also the fact that it does not make much +decisions for you. While Flask does pick a templating engine for you, we +won't make such decisions for your datastore or other parts. + +For us however the term “micro” does not mean that the whole implementation +has to fit into a single Python file. + +One of the design decisions with Flask was that simple tasks should be +simple and not take up a lot of code and yet not limit yourself. Because +of that we took a few design choices that some people might find +surprising or unorthodox. For example, Flask uses thread-local objects +internally so that you don't have to pass objects around from function to +function within a request in order to stay threadsafe. While this is a +really easy approach and saves you a lot of time, it might also cause some +troubles for very large applications because changes on these thread-local +objects can happen anywhere in the same thread. In order to solve these +problems we don't hide the thread locals for you but instead embrace them +and provide you with a lot of tools to make it as pleasant as possible to +work with them. + +Flask is also based on convention over configuration, which means that +many things are preconfigured. For example, by convention, templates and +static files are in subdirectories within the Python source tree of the +application. While this can be changed you usually don't have to. + +The main reason however why Flask is called a "microframework" is the idea +to keep the core simple but extensible. There is no database abstraction +layer, no form validation or anything else where different libraries +already exist that can handle that. However Flask knows the concept of +extensions that can add this functionality into your application as if it +was implemented in Flask itself. There are currently extensions for +object relational mappers, form validation, upload handling, various open +authentication technologies and more. + +Since Flask is based on a very solid foundation there is not a lot of code +in Flask itself. As such it's easy to adapt even for lage applications +and we are making sure that you can either configure it as much as +possible by subclassing things or by forking the entire codebase. If you +are interested in that, check out the :ref:`becomingbig` chapter. + +If you are curious about the Flask design principles, head over to the +section about :ref:`design`. + +Web Development is Dangerous +---------------------------- + +I'm not joking. Well, maybe a little. If you write a web +application, you are probably allowing users to register and leave their +data on your server. The users are entrusting you with data. And even if +you are the only user that might leave data in your application, you still +want that data to be stored securely. + +Unfortunately, there are many ways the security of a web application can be +compromised. Flask protects you against one of the most common security +problems of modern web applications: cross-site scripting (XSS). Unless +you deliberately mark insecure HTML as secure, Flask and the underlying +Jinja2 template engine have you covered. But there are many more ways to +cause security problems. + +The documentation will warn you about aspects of web development that +require attention to security. Some of these security concerns +are far more complex than one might think, and we all sometimes underestimate +the likelihood that a vulnerability will be exploited, until a clever +attacker figures out a way to exploit our applications. And don't think +that your application is not important enough to attract an attacker. +Depending on the kind of attack, chances are that automated bots are +probing for ways to fill your database with spam, links to malicious +software, and the like. + +So always keep security in mind when doing web development. + +The Status of Python 3 +---------------------- + +Currently the Python community is in the process of improving libraries to +support the new iteration of the Python programming language. While the +situation is greatly improving there are still some issues that make it +hard for us to switch over to Python 3 just now. These problems are +partially caused by changes in the language that went unreviewed for too +long, partially also because we have not quite worked out how the lower +level API should change for the unicode differences in Python3. + +Werkzeug and Flask will be ported to Python 3 as soon as a solution for +the changes is found, and we will provide helpful tips how to upgrade +existing applications to Python 3. Until then, we strongly recommend +using Python 2.6 and 2.7 with activated Python 3 warnings during +development. If you plan on upgrading to Python 3 in the near future we +strongly recommend that you read `How to write forwards compatible +Python code `_. diff --git a/docs/gevent.rst b/docs/gevent.rst deleted file mode 100644 index 3ead05c5..00000000 --- a/docs/gevent.rst +++ /dev/null @@ -1,125 +0,0 @@ -Async with Gevent -================= - -`Gevent`_ patches Python's standard library to run within special async workers -called `greenlets`_. Gevent has existed since long before Python's native -asyncio was available, and Flask has always worked with it. - -.. _gevent: https://www.gevent.org -.. _greenlets: https://greenlet.readthedocs.io - -Gevent is a reliable way to handle numerous, long lived, concurrent connections, -and to achieve similar capabilities to ASGI and asyncio. This works without -needing to write ``async def`` or ``await`` anywhere, but relies on gevent and -greenlet's low level manipulation of the Python interpreter. - -Deciding whether you should use gevent with Flask, or `Quart`_, or something -else, is ultimately up to understanding the specific needs of your project. - -.. _quart: https://quart.palletsprojects.com - - -Enabling gevent ---------------- - -You need to apply gevent's patching as early as possible in your code. This -enables gevent's underlying event loop and converts many Python internals to run -inside it. Add the following at the top of your project's module or top -``__init__.py``: - -.. code-block:: python - - import gevent.monkey - gevent.monkey.patch_all() - -When deploying in production, use :doc:`/deploying/gunicorn` or -:doc:`/deploying/uwsgi` with a gevent worker, as described on those pages. - -To run concurrent tasks within your own code, such as views, use -|gevent.spawn|_: - -.. |gevent.spawn| replace:: ``gevent.spawn()`` -.. _gevent.spawn: https://www.gevent.org/api/gevent.html#gevent.spawn - -.. code-block:: python - - @app.post("/send") - def send_email(): - gevent.spawn(email.send, to="example@example.example", text="example") - return "Email is being sent." - -If you need to access :data:`request` or other Flask context globals within the -spawned function, decorate the function with :func:`.stream_with_context` or -:func:`.copy_current_request_context`. Prefer passing the exact data you need -when spawning the function, rather than using the decorators. - -.. note:: - - When using gevent, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 - is required. - - -.. _gevent-asyncio: - -Combining with ``async``/``await`` ----------------------------------- - -Gevent's patching does not interact well with Flask's built-in asyncio support. -If you want to use Gevent and asyncio in the same app, you'll need to override -:meth:`flask.Flask.async_to_sync` to run async functions inside gevent. - -.. code-block:: python - - import gevent.monkey - gevent.monkey.patch_all() - - import asyncio - from flask import Flask, request - - loop = asyncio.EventLoop() - gevent.spawn(loop.run_forever) - - class GeventFlask(Flask): - def async_to_sync(self, func): - def run(*args, **kwargs): - coro = func(*args, **kwargs) - future = asyncio.run_coroutine_threadsafe(coro, loop) - return future.result() - - return run - - app = GeventFlask(__name__) - - @app.get("/") - async def greet(): - await asyncio.sleep(1) - return f"Hello, {request.args.get("name", "World")}!" - -This starts an asyncio event loop in a gevent worker. Async functions are -scheduled on that event loop. This may still have limitations, and may need to -be modified further when using other asyncio implementations. - - -libuv -~~~~~ - -`libuv`_ is another event loop implementation that `gevent supports`_. There's -also a project called `uvloop`_ that enables libuv in asyncio. If you want to -use libuv, use gevent's support, not uvloop. It may be possible to further -modify the ``async_to_sync`` code from the previous section to work with uvloop, -but that's not currently known. - -.. _libuv: https://libuv.org/ -.. _gevent supports: https://www.gevent.org/loop_impls.html -.. _uvloop: https://uvloop.readthedocs.io/ - -To enable gevent's libuv support, add the following at the *very* top of your -code, before ``gevent.monkey.patch_all()``: - -.. code-block:: python - - import gevent - gevent.config.loop = "libuv" - - import gevent.monkey - gevent.monkey.patch_all() diff --git a/docs/htmlfaq.rst b/docs/htmlfaq.rst new file mode 100644 index 00000000..1da25f3d --- /dev/null +++ b/docs/htmlfaq.rst @@ -0,0 +1,207 @@ +HTML/XHTML FAQ +============== + +The Flask documentation and example applications are using HTML5. You +may notice that in many situations, when end tags are optional they are +not used, so that the HTML is cleaner and faster to load. Because there +is much confusion about HTML and XHTML among developers, this document tries +to answer some of the major questions. + + +History of XHTML +---------------- + +For a while, it appeared that HTML was about to be replaced by XHTML. +However, barely any websites on the Internet are actual XHTML (which is +HTML processed using XML rules). There are a couple of major reasons +why this is the case. One of them is Internet Explorer's lack of proper +XHTML support. The XHTML spec states that XHTML must be served with the MIME +type `application/xhtml+xml`, but Internet Explorer refuses to read files +with that MIME type. +While it is relatively easy to configure Web servers to serve XHTML properly, +few people do. This is likely because properly using XHTML can be quite +painful. + +One of the most important causes of pain is XML's draconian (strict and +ruthless) error handling. When an XML parsing error is encountered, +the browser is supposed to show the user an ugly error message, instead +of attempting to recover from the error and display what it can. Most of +the (X)HTML generation on the web is based on non-XML template engines +(such as Jinja, the one used in Flask) which do not protect you from +accidentally creating invalid XHTML. There are XML based template engines, +such as Kid and the popular Genshi, but they often come with a larger +runtime overhead and, are not as straightforward to use because they have +to obey XML rules. + +The majority of users, however, assumed they were properly using XHTML. +They wrote an XHTML doctype at the top of the document and self-closed all +the necessary tags (``
`` becomes ``
`` or ``

`` in XHTML). +However, even if the document properly validates as XHTML, what really +determines XHTML/HTML processing in browsers is the MIME type, which as +said before is often not set properly. So the valid XHTML was being treated +as invalid HTML. + +XHTML also changed the way JavaScript is used. To properly work with XHTML, +programmers have to use the namespaced DOM interface with the XHTML +namespace to query for HTML elements. + +History of HTML5 +---------------- + +Development of the HTML5 specification was started in 2004 under the name +"Web Applications 1.0" by the Web Hypertext Application Technology Working +Group, or WHATWG (which was formed by the major browser vendors Apple, +Mozilla, and Opera) with the goal of writing a new and improved HTML +specification, based on existing browser behaviour instead of unrealistic +and backwards-incompatible specifications. + +For example, in HTML4 ``Hello``. However, since people were using +XHTML-like tags along the lines of ````, browser vendors implemented +the XHTML syntax over the syntax defined by the specification. + +In 2007, the specification was adopted as the basis of a new HTML +specification under the umbrella of the W3C, known as HTML5. Currently, +it appears that XHTML is losing traction, as the XHTML 2 working group has +been disbanded and HTML5 is being implemented by all major browser vendors. + +HTML versus XHTML +----------------- + +The following table gives you a quick overview of features available in +HTML 4.01, XHTML 1.1 and HTML5. (XHTML 1.0 is not included, as it was +superseded by XHTML 1.1 and the barely-used XHTML5.) + +.. tabularcolumns:: |p{9cm}|p{2cm}|p{2cm}|p{2cm}| + ++-----------------------------------------+----------+----------+----------+ +| | HTML4.01 | XHTML1.1 | HTML5 | ++=========================================+==========+==========+==========+ +| ``value`` | |Y| [1]_ | |N| | |N| | ++-----------------------------------------+----------+----------+----------+ +| ``
`` supported | |N| | |Y| | |Y| [2]_ | ++-----------------------------------------+----------+----------+----------+ +| `` - -A less common pattern is to add the data to a ``data-`` attribute on an -HTML tag. In this case, you must use single quotes around the value, not -double quotes, otherwise you will produce invalid or unsafe HTML. - -.. code-block:: jinja - -

- - -Generating URLs ---------------- - -The other way to get data from the server to JavaScript is to make a -request for it. First, you need to know the URL to request. - -The simplest way to generate URLs is to continue to use -:func:`~flask.url_for` when rendering the template. For example: - -.. code-block:: javascript - - const user_url = {{ url_for("user", id=current_user.id)|tojson }} - fetch(user_url).then(...) - -However, you might need to generate a URL based on information you only -know in JavaScript. As discussed above, JavaScript runs in the user's -browser, not as part of the template rendering, so you can't use -``url_for`` at that point. - -In this case, you need to know the "root URL" under which your -application is served. In simple setups, this is ``/``, but it might -also be something else, like ``https://example.com/myapp/``. - -A simple way to tell your JavaScript code about this root is to set it -as a global variable when rendering the template. Then you can use it -when generating URLs from JavaScript. - -.. code-block:: javascript - - const SCRIPT_ROOT = {{ request.script_root|tojson }} - let user_id = ... // do something to get a user id from the page - let user_url = `${SCRIPT_ROOT}/user/${user_id}` - fetch(user_url).then(...) - - -Making a Request with ``fetch`` -------------------------------- - -|fetch|_ takes two arguments, a URL and an object with other options, -and returns a |Promise|_. We won't cover all the available options, and -will only use ``then()`` on the promise, not other callbacks or -``await`` syntax. Read the linked MDN docs for more information about -those features. - -By default, the GET method is used. If the response contains JSON, it -can be used with a ``then()`` callback chain. - -.. code-block:: javascript - - const room_url = {{ url_for("room_detail", id=room.id)|tojson }} - fetch(room_url) - .then(response => response.json()) - .then(data => { - // data is a parsed JSON object - }) - -To send data, use a data method such as POST, and pass the ``body`` -option. The most common types for data are form data or JSON data. - -To send form data, pass a populated |FormData|_ object. This uses the -same format as an HTML form, and would be accessed with ``request.form`` -in a Flask view. - -.. code-block:: javascript - - let data = new FormData() - data.append("name", "Flask Room") - data.append("description", "Talk about Flask here.") - fetch(room_url, { - "method": "POST", - "body": data, - }).then(...) - -In general, prefer sending request data as form data, as would be used -when submitting an HTML form. JSON can represent more complex data, but -unless you need that it's better to stick with the simpler format. When -sending JSON data, the ``Content-Type: application/json`` header must be -sent as well, otherwise Flask will return a 415 Unsupported Media Type -error. - -.. code-block:: javascript - - let data = { - "name": "Flask Room", - "description": "Talk about Flask here.", - } - fetch(room_url, { - "method": "POST", - "headers": {"Content-Type": "application/json"}, - "body": JSON.stringify(data), - }).then(...) - -.. |Promise| replace:: ``Promise`` -.. _Promise: https://developer.mozilla.org/Web/JavaScript/Reference/Global_Objects/Promise -.. |FormData| replace:: ``FormData`` -.. _FormData: https://developer.mozilla.org/en-US/docs/Web/API/FormData - - -Following Redirects -------------------- - -A response might be a redirect, for example if you logged in with -JavaScript instead of a traditional HTML form, and your view returned -a redirect instead of JSON. JavaScript requests do follow redirects, but -they don't change the page. If you want to make the page change you can -inspect the response and apply the redirect manually. - -.. code-block:: javascript - - fetch("/login", {"body": ...}).then( - response => { - if (response.redirected) { - window.location = response.url - } else { - showLoginError() - } - } - ) - - -Replacing Content ------------------ - -A response might be new HTML, either a new section of the page to add or -replace, or an entirely new page. In general, if you're returning the -entire page, it would be better to handle that with a redirect as shown -in the previous section. The following example shows how to replace a -``
`` with the HTML returned by a request. - -.. code-block:: html - -
- {{ include "geology_fact.html" }} -
- - - -Return JSON from Views ----------------------- - -To return a JSON object from your API view, you can directly return a -dict from the view. It will be serialized to JSON automatically. - -.. code-block:: python - - @app.route("/user/") - def user_detail(id): - user = User.query.get_or_404(id) - return { - "username": User.username, - "email": User.email, - "picture": url_for("static", filename=f"users/{id}/profile.png"), - } - -If you want to return another JSON type, use the -:func:`~flask.json.jsonify` function, which creates a response object -with the given data serialized to JSON. - -.. code-block:: python - - from flask import jsonify - - @app.route("/users") - def user_list(): - users = User.query.order_by(User.name).all() - return jsonify([u.to_json() for u in users]) - -It is usually not a good idea to return file data in a JSON response. -JSON cannot represent binary data directly, so it must be base64 -encoded, which can be slow, takes more bandwidth to send, and is not as -easy to cache. Instead, serve files using one view, and generate a URL -to the desired file to include in the JSON. Then the client can make a -separate request to get the linked resource after getting the JSON. - - -Receiving JSON in Views ------------------------ - -Use the :attr:`~flask.Request.json` property of the -:data:`~flask.request` object to decode the request's body as JSON. If -the body is not valid JSON, a 400 Bad Request error will be raised. If -the ``Content-Type`` header is not set to ``application/json``, a 415 -Unsupported Media Type error will be raised. - -.. code-block:: python - - from flask import request - - @app.post("/user/") - def user_update(id): - user = User.query.get_or_404(id) - user.update_from_json(request.json) - db.session.commit() - return user.to_json() diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index 7ac6856e..f3c46e39 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -1,6 +1,167 @@ -:orphan: - AJAX with jQuery ================ -Obsolete, see :doc:`/patterns/javascript` instead. +`jQuery`_ is a small JavaScript library commonly used to simplify working +with the DOM and JavaScript in general. It is the perfect tool to make +web applications more dynamic by exchanging JSON between server and +client. + +JSON itself is a very lightweight transport format, very similar to how +Python primitives (numbers, strings, dicts and lists) look like which is +widely supported and very easy to parse. It became popular a few years +ago and quickly replaced XML as transport format in web applications. + +If you have Python 2.6 JSON will work out of the box, in Python 2.5 you +will have to install the `simplejson`_ library from PyPI. + +.. _jQuery: http://jquery.com/ +.. _simplejson: http://pypi.python.org/pypi/simplejson + +Loading jQuery +-------------- + +In order to use jQuery, you have to download it first and place it in the +static folder of your application and then ensure it's loaded. Ideally +you have a layout template that is used for all pages where you just have +to add a script statement to the bottom of your `` to load jQuery: + +.. sourcecode:: html + + + +Another method is using Google's `AJAX Libraries API +`_ to load jQuery: + +.. sourcecode:: html + + + + +In this case you have to put jQuery into your static folder as a fallback, but it will +first try to load it directly from Google. This has the advantage that your +website will probably load faster for users if they went to at least one +other website before using the same jQuery version from Google because it +will already be in the browser cache. + +Where is My Site? +----------------- + +Do you know where your application is? If you are developing the answer +is quite simple: it's on localhost port something and directly on the root +of that server. But what if you later decide to move your application to +a different location? For example to ``http://example.com/myapp``? On +the server side this never was a problem because we were using the handy +:func:`~flask.url_for` function that could answer that question for +us, but if we are using jQuery we should not hardcode the path to +the application but make that dynamic, so how can we do that? + +A simple method would be to add a script tag to our page that sets a +global variable to the prefix to the root of the application. Something +like this: + +.. sourcecode:: html+jinja + + + +The ``|safe`` is necessary so that Jinja does not escape the JSON encoded +string with HTML rules. Usually this would be necessary, but we are +inside a `script` block here where different rules apply. + +.. admonition:: Information for Pros + + In HTML the `script` tag is declared `CDATA` which means that entities + will not be parsed. Everything until ```` is handled as script. + This also means that there must never be any ``"|tojson|safe }}`` is rendered as + ``"<\/script>"``). + + +JSON View Functions +------------------- + +Now let's create a server side function that accepts two URL arguments of +numbers which should be added together and then sent back to the +application in a JSON object. This is a really ridiculous example and is +something you usually would do on the client side alone, but a simple +example that shows how you would use jQuery and Flask nonetheless:: + + from flask import Flask, jsonify, render_template, request + app = Flask(__name__) + + @app.route('/_add_numbers') + def add_numbers(): + a = request.args.get('a', 0, type=int) + b = request.args.get('b', 0, type=int) + return jsonify(result=a + b) + + @app.route('/') + def index(): + return render_template('index.html') + +As you can see I also added an `index` method here that renders a +template. This template will load jQuery as above and have a little form +we can add two numbers and a link to trigger the function on the server +side. + +Note that we are using the :meth:`~werkzeug.datastructures.MultiDict.get` method here +which will never fail. If the key is missing a default value (here ``0``) +is returned. Furthermore it can convert values to a specific type (like +in our case `int`). This is especially handy for code that is +triggered by a script (APIs, JavaScript etc.) because you don't need +special error reporting in that case. + +The HTML +-------- + +Your index.html template either has to extend a `layout.html` template with +jQuery loaded and the `$SCRIPT_ROOT` variable set, or do that on the top. +Here's the HTML code needed for our little application (`index.html`). +Notice that we also drop the script directly into the HTML here. It is +usually a better idea to have that in a separate script file: + +.. sourcecode:: html + + +

jQuery Example

+

+ + = + ? +

calculate server side + +I won't got into detail here about how jQuery works, just a very quick +explanation of the little bit of code above: + +1. ``$(function() { ... })`` specifies code that should run once the + browser is done loading the basic parts of the page. +2. ``$('selector')`` selects an element and lets you operate on it. +3. ``element.bind('event', func)`` specifies a function that should run + when the user clicked on the element. If that function returns + `false`, the default behaviour will not kick in (in this case, navigate + to the `#` URL). +4. ``$.getJSON(url, data, func)`` sends a `GET` request to `url` and will + send the contents of the `data` object as query parameters. Once the + data arrived, it will call the given function with the return value as + argument. Note that we can use the `$SCRIPT_ROOT` variable here that + we set earlier. + +If you don't get the whole picture, download the `sourcecode +for this example +`_ +from github. diff --git a/docs/patterns/lazyloading.rst b/docs/patterns/lazyloading.rst index 658a1cd4..50ad6fa8 100644 --- a/docs/patterns/lazyloading.rst +++ b/docs/patterns/lazyloading.rst @@ -32,8 +32,8 @@ Imagine the current application looks somewhat like this:: def user(username): pass -Then, with the centralized approach you would have one file with the views -(:file:`views.py`) but without any decorator:: +Then the centralized approach you would have one file with the views +(`views.py`) but without any decorator:: def index(): pass @@ -54,11 +54,11 @@ Loading Late ------------ So far we only split up the views and the routing, but the module is still -loaded upfront. The trick is to actually load the view function as needed. +loaded upfront. The trick to actually load the view function as needed. This can be accomplished with a helper class that behaves just like a function but internally imports the real function on first use:: - from werkzeug.utils import import_string, cached_property + from werkzeug import import_string, cached_property class LazyView(object): @@ -90,19 +90,14 @@ Then you can define your central place to combine the views like this:: You can further optimize this in terms of amount of keystrokes needed to write this by having a function that calls into :meth:`~flask.Flask.add_url_rule` by prefixing a string with the project -name and a dot, and by wrapping `view_func` in a `LazyView` as needed. :: +name and a dot, and by wrapping `view_func` in a `LazyView` as needed:: - def url(import_name, url_rules=[], **options): - view = LazyView(f"yourapplication.{import_name}") - for url_rule in url_rules: - app.add_url_rule(url_rule, view_func=view, **options) + def url(url_rule, import_name, **options): + view = LazyView('yourapplication.' + import_name) + app.add_url_rule(url_rule, view_func=view, **options) - # add a single route to the index view - url('views.index', ['/']) - - # add two routes to a single function endpoint - url_rules = ['/user/','/user/'] - url('views.user', url_rules) + url('/', 'views.index') + url('/user/', 'views.user') One thing to keep in mind is that before and after request handlers have to be in a file that is imported upfront to work properly on the first diff --git a/docs/patterns/methodoverrides.rst b/docs/patterns/methodoverrides.rst deleted file mode 100644 index 45dbb87e..00000000 --- a/docs/patterns/methodoverrides.rst +++ /dev/null @@ -1,42 +0,0 @@ -Adding HTTP Method Overrides -============================ - -Some HTTP proxies do not support arbitrary HTTP methods or newer HTTP -methods (such as PATCH). In that case it's possible to "proxy" HTTP -methods through another HTTP method in total violation of the protocol. - -The way this works is by letting the client do an HTTP POST request and -set the ``X-HTTP-Method-Override`` header. Then the method is replaced -with the header value before being passed to Flask. - -This can be accomplished with an HTTP middleware:: - - class HTTPMethodOverrideMiddleware(object): - allowed_methods = frozenset([ - 'GET', - 'HEAD', - 'POST', - 'DELETE', - 'PUT', - 'PATCH', - 'OPTIONS' - ]) - bodyless_methods = frozenset(['GET', 'HEAD', 'OPTIONS', 'DELETE']) - - def __init__(self, app): - self.app = app - - def __call__(self, environ, start_response): - method = environ.get('HTTP_X_HTTP_METHOD_OVERRIDE', '').upper() - if method in self.allowed_methods: - environ['REQUEST_METHOD'] = method - if method in self.bodyless_methods: - environ['CONTENT_LENGTH'] = '0' - return self.app(environ, start_response) - -To use this with Flask, wrap the app object with the middleware:: - - from flask import Flask - - app = Flask(__name__) - app.wsgi_app = HTTPMethodOverrideMiddleware(app.wsgi_app) diff --git a/docs/patterns/mongoengine.rst b/docs/patterns/mongoengine.rst deleted file mode 100644 index 8d49de7c..00000000 --- a/docs/patterns/mongoengine.rst +++ /dev/null @@ -1,102 +0,0 @@ -MongoDB with MongoEngine -======================== - -Using a document database like MongoDB is a common alternative to -relational SQL databases. This pattern shows how to use -`MongoEngine`_, a document mapper library, to integrate with MongoDB. - -A running MongoDB server and `Flask-MongoEngine`_ are required. :: - - pip install flask-mongoengine - -.. _MongoEngine: http://mongoengine.org -.. _Flask-MongoEngine: https://docs.mongoengine.org/projects/flask-mongoengine/en/latest/ - -Configuration -------------- - -Basic setup can be done by defining ``MONGODB_SETTINGS`` on -``app.config`` and creating a ``MongoEngine`` instance. :: - - from flask import Flask - from flask_mongoengine import MongoEngine - - app = Flask(__name__) - app.config['MONGODB_SETTINGS'] = { - "db": "myapp", - } - db = MongoEngine(app) - - -Mapping Documents ------------------ - -To declare a model that represents a Mongo document, create a class that -inherits from ``Document`` and declare each of the fields. :: - - import mongoengine as me - - class Movie(me.Document): - title = me.StringField(required=True) - year = me.IntField() - rated = me.StringField() - director = me.StringField() - actors = me.ListField() - -If the document has nested fields, use ``EmbeddedDocument`` to -defined the fields of the embedded document and -``EmbeddedDocumentField`` to declare it on the parent document. :: - - class Imdb(me.EmbeddedDocument): - imdb_id = me.StringField() - rating = me.DecimalField() - votes = me.IntField() - - class Movie(me.Document): - ... - imdb = me.EmbeddedDocumentField(Imdb) - - -Creating Data -------------- - -Instantiate your document class with keyword arguments for the fields. -You can also assign values to the field attributes after instantiation. -Then call ``doc.save()``. :: - - bttf = Movie(title="Back To The Future", year=1985) - bttf.actors = [ - "Michael J. Fox", - "Christopher Lloyd" - ] - bttf.imdb = Imdb(imdb_id="tt0088763", rating=8.5) - bttf.save() - - -Queries -------- - -Use the class ``objects`` attribute to make queries. A keyword argument -looks for an equal value on the field. :: - - bttf = Movie.objects(title="Back To The Future").get_or_404() - -Query operators may be used by concatenating them with the field name -using a double-underscore. ``objects``, and queries returned by -calling it, are iterable. :: - - some_theron_movie = Movie.objects(actors__in=["Charlize Theron"]).first() - - for recents in Movie.objects(year__gte=2017): - print(recents.title) - - -Documentation -------------- - -There are many more ways to define and query documents with MongoEngine. -For more information, check out the `official documentation -`_. - -Flask-MongoEngine adds helpful utilities on top of MongoEngine. Check -out their `documentation `_ as well. diff --git a/docs/patterns/mongokit.rst b/docs/patterns/mongokit.rst new file mode 100644 index 00000000..a9c4eef5 --- /dev/null +++ b/docs/patterns/mongokit.rst @@ -0,0 +1,144 @@ +.. mongokit-pattern: + +MongoKit in Flask +================= + +Using a document database rather than a full DBMS gets more common these days. +This pattern shows how to use MongoKit, a document mapper library, to +integrate with MongoDB. + +This pattern requires a running MongoDB server and the MongoKit library +installed. + +There are two very common ways to use MongoKit. I will outline each of them +here: + + +Declarative +----------- + +The default behaviour of MongoKit is the declarative one that is based on +common ideas from Django or the SQLAlchemy declarative extension. + +Here an example `app.py` module for your application:: + + from flask import Flask + from mongokit import Connection, Document + + # configuration + MONGODB_HOST = 'localhost' + MONGODB_PORT = 27017 + + # create the little application object + app = Flask(__name__) + app.config.from_object(__name__) + + # connect to the database + connection = Connection(app.config['MONGODB_HOST'], + app.config['MONGODB_PORT']) + + +To define your models, just subclass the `Document` class that is imported +from MongoKit. If you've seen the SQLAlchemy pattern you may wonder why we do +not have a session and even do not define a `init_db` function here. On the +one hand, MongoKit does not have something like a session. This sometimes +makes it more to type but also makes it blazingly fast. On the other hand, +MongoDB is schemaless. This means you can modify the data structure from one +insert query to the next without any problem. MongoKit is just schemaless +too, but implements some validation to ensure data integrity. + +Here is an example document (put this also into `app.py`, e.g.):: + + def max_length(length): + def validate(value): + if len(value) <= length: + return True + raise Exception('%s must be at most %s characters long' % length) + return validate + + class User(Document): + structure = { + 'name': unicode, + 'email': unicode, + } + validators = { + 'name': max_length(50), + 'email': max_length(120) + } + use_dot_notation = True + def __repr__(self): + return '' % (self.name) + + # register the User document with our current connection + connection.register([User]) + + +This example shows you how to define your schema (named structure), a +validator for the maximum character length and uses a special MongoKit feature +called `use_dot_notation`. Per default MongoKit behaves like a python +dictionary but with `use_dot_notation` set to `True` you can use your +documents like you use models in nearly any other ORM by using dots to +separate between attributes. + +You can insert entries into the database like this: + +>>> from yourapplication.database import connection +>>> from yourapplication.models import User +>>> collection = connection['test'].users +>>> user = collection.User() +>>> user['name'] = u'admin' +>>> user['email'] = u'admin@localhost' +>>> user.save() + +Note that MongoKit is kinda strict with used column types, you must not use a +common `str` type for either `name` or `email` but unicode. + +Querying is simple as well: + +>>> list(collection.User.find()) +[] +>>> collection.User.find_one({'name': u'admin'}) + + +.. _MongoKit: http://bytebucket.org/namlook/mongokit/ + + +PyMongo Compatibility Layer +--------------------------- + +If you just want to use PyMongo, you can do that with MongoKit as well. You +may use this process if you need the best performance to get. Note that this +example does not show how to couple it with Flask, see the above MongoKit code +for examples:: + + from MongoKit import Connection + + connection = Connection() + +To insert data you can use the `insert` method. We have to get a +collection first, this is somewhat the same as a table in the SQL world. + +>>> collection = connection['test'].users +>>> user = {'name': u'admin', 'email': u'admin@localhost'} +>>> collection.insert(user) + +print list(collection.find()) +print collection.find_one({'name': u'admin'}) + +MongoKit will automatically commit for us. + +To query your database, you use the collection directly: + +>>> list(collection.find()) +[{u'_id': ObjectId('4c271729e13823182f000000'), u'name': u'admin', u'email': u'admin@localhost'}] +>>> collection.find_one({'name': u'admin'}) +{u'_id': ObjectId('4c271729e13823182f000000'), u'name': u'admin', u'email': u'admin@localhost'} + +These results are also dict-like objects: + +>>> r = collection.find_one({'name': u'admin'}) +>>> r['email'] +u'admin@localhost' + +For more information about MongoKit, head over to the +`website `_. diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 90fa8a8f..79fd2c58 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -1,38 +1,37 @@ -Large Applications as Packages -============================== +.. _larger-applications: -Imagine a simple flask application structure that looks like this:: +Larger Applications +=================== + +For larger applications it's a good idea to use a package instead of a +module. That is quite simple. Imagine a small application looks like +this:: /yourapplication - yourapplication.py + /yourapplication.py /static - style.css + /style.css /templates layout.html index.html login.html ... -While this is fine for small applications, for larger applications -it's a good idea to use a package instead of a module. -The :doc:`/tutorial/index` is structured to use the package pattern, -see the :gh:`example code `. - Simple Packages --------------- To convert that into a larger one, just create a new folder -:file:`yourapplication` inside the existing one and move everything below it. -Then rename :file:`yourapplication.py` to :file:`__init__.py`. (Make sure to delete -all ``.pyc`` files first, otherwise things would most likely break) +`yourapplication` inside the existing one and move everything below it. +Then rename `yourapplication.py` to `__init__.py`. (Make sure to delete +all `.pyc` files first, otherwise things would most likely break) You should then end up with something like that:: /yourapplication /yourapplication - __init__.py + /__init__.py /static - style.css + /style.css /templates layout.html index.html @@ -42,55 +41,32 @@ You should then end up with something like that:: But how do you run your application now? The naive ``python yourapplication/__init__.py`` will not work. Let's just say that Python does not want modules in packages to be the startup file. But that is not -a big problem, just add a new file called :file:`pyproject.toml` next to the inner -:file:`yourapplication` folder with the following contents: +a big problem, just add a new file called `runserver.py` next to the inner +`yourapplication` folder with the following contents:: -.. code-block:: toml - - [project] - name = "yourapplication" - dependencies = [ - "flask", - ] - - [build-system] - requires = ["flit_core<4"] - build-backend = "flit_core.buildapi" - -Install your application so it is importable: - -.. code-block:: text - - $ pip install -e . - -To use the ``flask`` command and run your application you need to set -the ``--app`` option that tells Flask where to find the application -instance: - -.. code-block:: text - - $ flask --app yourapplication run + from yourapplication import app + app.run(debug=True) What did we gain from this? Now we can restructure the application a bit into multiple modules. The only thing you have to remember is the following quick checklist: 1. the `Flask` application object creation has to be in the - :file:`__init__.py` file. That way each module can import it safely and the + `__init__.py` file. That way each module can import it safely and the `__name__` variable will resolve to the correct package. 2. all the view functions (the ones with a :meth:`~flask.Flask.route` - decorator on top) have to be imported in the :file:`__init__.py` file. + decorator on top) have to be imported when in the `__init__.py` file. Not the object itself, but the module it is in. Import the view module **after the application object is created**. -Here's an example :file:`__init__.py`:: +Here's an example `__init__.py`:: from flask import Flask app = Flask(__name__) import yourapplication.views -And this is what :file:`views.py` would look like:: +And this is what `views.py` would look like:: from yourapplication import app @@ -101,12 +77,12 @@ And this is what :file:`views.py` would look like:: You should then end up with something like that:: /yourapplication - pyproject.toml + /runserver.py /yourapplication - __init__.py - views.py + /__init__.py + /views.py /static - style.css + /style.css /templates layout.html index.html @@ -117,12 +93,18 @@ You should then end up with something like that:: Every Python programmer hates them, and yet we just added some: circular imports (That's when two modules depend on each other. In this - case :file:`views.py` depends on :file:`__init__.py`). Be advised that this is a + case `views.py` depends on `__init__.py`). Be advised that this is a bad idea in general but here it is actually fine. The reason for this is - that we are not actually using the views in :file:`__init__.py` and just + that we are not actually using the views in `__init__.py` and just ensuring the module is imported and we are doing that at the bottom of the file. + There are still some problems with that approach but if you want to use + decorators there is no way around that. Check out the + :ref:`becomingbig` section for some inspiration how to deal with that. + + +.. _working-with-modules: Working with Blueprints ----------------------- @@ -130,4 +112,4 @@ Working with Blueprints If you have larger applications it's recommended to divide them into smaller groups where each group is implemented with the help of a blueprint. For a gentle introduction into this topic refer to the -:doc:`/blueprints` chapter of the documentation. +:ref:`blueprints` chapter of the documentation. diff --git a/docs/patterns/requestchecksum.rst b/docs/patterns/requestchecksum.rst deleted file mode 100644 index 25bc38b2..00000000 --- a/docs/patterns/requestchecksum.rst +++ /dev/null @@ -1,55 +0,0 @@ -Request Content Checksums -========================= - -Various pieces of code can consume the request data and preprocess it. -For instance JSON data ends up on the request object already read and -processed, form data ends up there as well but goes through a different -code path. This seems inconvenient when you want to calculate the -checksum of the incoming request data. This is necessary sometimes for -some APIs. - -Fortunately this is however very simple to change by wrapping the input -stream. - -The following example calculates the SHA1 checksum of the incoming data as -it gets read and stores it in the WSGI environment:: - - import hashlib - - class ChecksumCalcStream(object): - - def __init__(self, stream): - self._stream = stream - self._hash = hashlib.sha1() - - def read(self, bytes): - rv = self._stream.read(bytes) - self._hash.update(rv) - return rv - - def readline(self, size_hint): - rv = self._stream.readline(size_hint) - self._hash.update(rv) - return rv - - def generate_checksum(request): - env = request.environ - stream = ChecksumCalcStream(env['wsgi.input']) - env['wsgi.input'] = stream - return stream._hash - -To use this, all you need to do is to hook the calculating stream in -before the request starts consuming data. (Eg: be careful accessing -``request.form`` or anything of that nature. ``before_request_handlers`` -for instance should be careful not to access it). - -Example usage:: - - @app.route('/special-api', methods=['POST']) - def special_api(): - hash = generate_checksum(request) - # Accessing this parses the input stream - files = request.files - # At this point the hash is fully constructed. - checksum = hash.hexdigest() - return f"Hash was: {checksum}" diff --git a/docs/patterns/singlepageapplications.rst b/docs/patterns/singlepageapplications.rst deleted file mode 100644 index 1cb779b3..00000000 --- a/docs/patterns/singlepageapplications.rst +++ /dev/null @@ -1,24 +0,0 @@ -Single-Page Applications -======================== - -Flask can be used to serve Single-Page Applications (SPA) by placing static -files produced by your frontend framework in a subfolder inside of your -project. You will also need to create a catch-all endpoint that routes all -requests to your SPA. - -The following example demonstrates how to serve an SPA along with an API:: - - from flask import Flask, jsonify - - app = Flask(__name__, static_folder='app', static_url_path="/app") - - - @app.route("/heartbeat") - def heartbeat(): - return jsonify({"status": "healthy"}) - - - @app.route('/', defaults={'path': ''}) - @app.route('/') - def catch_all(path): - return app.send_static_file("index.html") diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 9e9afe48..5a33d1f6 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -1,10 +1,12 @@ +.. _sqlalchemy-pattern: + SQLAlchemy in Flask =================== Many people prefer `SQLAlchemy`_ for database access. In this case it's encouraged to use a package instead of a module for your flask application -and drop the models into a separate module (:doc:`packages`). While that -is not necessary, it makes a lot of sense. +and drop the models into a separate module (:ref:`larger-applications`). +While that is not necessary, it makes a lot of sense. There are four very common ways to use SQLAlchemy. I will outline each of them here: @@ -18,9 +20,9 @@ there is a Flask extension that handles that for you. This is recommended if you want to get started quickly. You can download `Flask-SQLAlchemy`_ from `PyPI -`_. +`_. -.. _Flask-SQLAlchemy: https://flask-sqlalchemy.palletsprojects.com/ +.. _Flask-SQLAlchemy: http://packages.python.org/Flask-SQLAlchemy/ Declarative @@ -31,15 +33,16 @@ SQLAlchemy. It allows you to define tables and models in one go, similar to how Django works. In addition to the following text I recommend the official documentation on the `declarative`_ extension. -Here's the example :file:`database.py` module for your application:: +Here the example `database.py` module for your application:: from sqlalchemy import create_engine - from sqlalchemy.orm import scoped_session, sessionmaker, declarative_base + from sqlalchemy.orm import scoped_session, sessionmaker + from sqlalchemy.ext.declarative import declarative_base - engine = create_engine('sqlite:////tmp/test.db') + engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True) db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, - bind=engine)) + bind=engine)) Base = declarative_base() Base.query = db_session.query_property() @@ -58,16 +61,15 @@ already with the :class:`~sqlalchemy.orm.scoped_session`. To use SQLAlchemy in a declarative way with your application, you just have to put the following code into your application module. Flask will -automatically remove database sessions at the end of the request or -when the application shuts down:: +automatically remove database sessions at the end of the request for you:: from yourapplication.database import db_session - @app.teardown_appcontext + @app.teardown_request def shutdown_session(exception=None): db_session.remove() -Here is an example model (put this into :file:`models.py`, e.g.):: +Here is an example model (put this into `models.py`, e.g.):: from sqlalchemy import Column, Integer, String from yourapplication.database import Base @@ -83,7 +85,7 @@ Here is an example model (put this into :file:`models.py`, e.g.):: self.email = email def __repr__(self): - return f'' + return '' % (self.name) To create the database you can use the `init_db` function: @@ -101,12 +103,13 @@ You can insert entries into the database like this: Querying is simple as well: >>> User.query.all() -[] +[] >>> User.query.filter(User.name == 'admin').first() - + -.. _SQLAlchemy: https://www.sqlalchemy.org/ -.. _declarative: https://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/ +.. _SQLAlchemy: http://www.sqlalchemy.org/ +.. _declarative: + http://www.sqlalchemy.org/docs/orm/extensions/declarative.html Manual Object Relational Mapping -------------------------------- @@ -118,29 +121,29 @@ flexible but a little more to type. In general it works like the declarative approach, so make sure to also split up your application into multiple modules in a package. -Here is an example :file:`database.py` module for your application:: +Here is an example `database.py` module for your application:: from sqlalchemy import create_engine, MetaData from sqlalchemy.orm import scoped_session, sessionmaker - engine = create_engine('sqlite:////tmp/test.db') + engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True) metadata = MetaData() db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, - bind=engine)) + bind=engine)) def init_db(): metadata.create_all(bind=engine) -As in the declarative approach, you need to close the session after each app -context. Put this into your application module:: +As for the declarative approach you need to close the session after +each request. Put this into your application module:: from yourapplication.database import db_session - @app.teardown_appcontext + @app.teardown_request def shutdown_session(exception=None): db_session.remove() -Here is an example table and model (put this into :file:`models.py`):: +Here is an example table and model (put this into `models.py`):: from sqlalchemy import Table, Column, Integer, String from sqlalchemy.orm import mapper @@ -154,7 +157,7 @@ Here is an example table and model (put this into :file:`models.py`):: self.email = email def __repr__(self): - return f'' + return '' % (self.name, self.email) users = Table('users', metadata, Column('id', Integer, primary_key=True), @@ -172,42 +175,40 @@ SQL Abstraction Layer If you just want to use the database system (and SQL) abstraction layer you basically only need the engine:: - from sqlalchemy import create_engine, MetaData, Table + from sqlalchemy import create_engine, MetaData - engine = create_engine('sqlite:////tmp/test.db') + engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True) metadata = MetaData(bind=engine) Then you can either declare the tables in your code like in the examples above, or automatically load them:: - from sqlalchemy import Table - users = Table('users', metadata, autoload=True) To insert data you can use the `insert` method. We have to get a connection first so that we can use a transaction: >>> con = engine.connect() ->>> con.execute(users.insert(), name='admin', email='admin@localhost') +>>> con.execute(users.insert(name='admin', email='admin@localhost')) SQLAlchemy will automatically commit for us. To query your database, you use the engine directly or use a connection: >>> users.select(users.c.id == 1).execute().first() -(1, 'admin', 'admin@localhost') +(1, u'admin', u'admin@localhost') These results are also dict-like tuples: >>> r = users.select(users.c.id == 1).execute().first() >>> r['name'] -'admin' +u'admin' You can also pass strings of SQL statements to the :meth:`~sqlalchemy.engine.base.Connection.execute` method: >>> engine.execute('select * from users where id = :1', [1]).first() -(1, 'admin', 'admin@localhost') +(1, u'admin', u'admin@localhost') For more information about SQLAlchemy, head over to the -`website `_. +`website `_. diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index f42e0f8e..0d02e465 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -1,130 +1,101 @@ +.. _sqlite3: + Using SQLite 3 with Flask ========================= -You can implement a few functions to work with a SQLite connection during a -request context. The connection is created the first time it's accessed, -reused on subsequent access, until it is closed when the request context ends. +In Flask you can implement the opening of database connections at the +beginning of the request and closing at the end with the +:meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.teardown_request` +decorators in combination with the special :class:`~flask.g` object. -Here is a simple example of how you can use SQLite 3 with Flask:: +So here is a simple example of how you can use SQLite 3 with Flask:: import sqlite3 from flask import g DATABASE = '/path/to/database.db' - def get_db(): - db = getattr(g, '_database', None) - if db is None: - db = g._database = sqlite3.connect(DATABASE) - return db + def connect_db(): + return sqlite3.connect(DATABASE) - @app.teardown_appcontext - def close_connection(exception): - db = getattr(g, '_database', None) - if db is not None: - db.close() - -Now, to use the database, the application must either have an active -application context (which is always true if there is a request in flight) -or create an application context itself. At that point the ``get_db`` -function can be used to get the current database connection. Whenever the -context is destroyed the database connection will be terminated. - -Example:: - - @app.route('/') - def index(): - cur = get_db().cursor() - ... + @app.before_request + def before_request(): + g.db = connect_db() + @app.teardown_request + def teardown_request(exception): + if hasattr(g, 'db'): + g.db.close() .. note:: - Please keep in mind that the teardown request and appcontext functions - are always executed, even if a before-request handler failed or was - never executed. Because of this we have to make sure here that the - database is there before we close it. + Please keep in mind that the teardown request functions are always + executed, even if a before-request handler failed or was never + executed. Because of this we have to make sure here that the database + is there before we close it. Connect on Demand ----------------- -The upside of this approach (connecting on first use) is that this will -only open the connection if truly necessary. If you want to use this -code outside a request context you can use it in a Python shell by opening -the application context by hand:: +The downside of this approach is that this will only work if Flask +executed the before-request handlers for you. If you are attempting to +use the database from a script or the interactive Python shell you would +have to do something like this:: - with app.app_context(): - # now you can use get_db() + with app.test_request_context(): + app.preprocess_request() + # now you can use the g.db object +In order to trigger the execution of the connection code. You won't be +able to drop the dependency on the request context this way, but you could +make it so that the application connects when necessary:: + + def get_connection(): + db = getattr(g, '_db', None) + if db is None: + db = g._db = connect_db() + return db + +Downside here is that you have to use ``db = get_connection()`` instead of +just being able to use ``g.db`` directly. + +.. _easy-querying: Easy Querying ------------- -Now in each request handling function you can access `get_db()` to get the +Now in each request handling function you can access `g.db` to get the current open database connection. To simplify working with SQLite, a -row factory function is useful. It is executed for every result returned -from the database to convert the result. For instance, in order to get -dictionaries instead of tuples, this could be inserted into the ``get_db`` -function we created above:: - - def make_dicts(cursor, row): - return dict((cursor.description[idx][0], value) - for idx, value in enumerate(row)) - - db.row_factory = make_dicts - -This will make the sqlite3 module return dicts for this database connection, which are much nicer to deal with. Even more simply, we could place this in ``get_db`` instead:: - - db.row_factory = sqlite3.Row - -This would use Row objects rather than dicts to return the results of queries. These are ``namedtuple`` s, so we can access them either by index or by key. For example, assuming we have a ``sqlite3.Row`` called ``r`` for the rows ``id``, ``FirstName``, ``LastName``, and ``MiddleInitial``:: - - >>> # You can get values based on the row's name - >>> r['FirstName'] - John - >>> # Or, you can get them based on index - >>> r[1] - John - # Row objects are also iterable: - >>> for value in r: - ... print(value) - 1 - John - Doe - M - -Additionally, it is a good idea to provide a query function that combines -getting the cursor, executing and fetching the results:: +helper function can be useful:: def query_db(query, args=(), one=False): - cur = get_db().execute(query, args) - rv = cur.fetchall() - cur.close() + cur = g.db.execute(query, args) + rv = [dict((cur.description[idx][0], value) + for idx, value in enumerate(row)) for row in cur.fetchall()] return (rv[0] if rv else None) if one else rv -This handy little function, in combination with a row factory, makes -working with the database much more pleasant than it is by just using the -raw cursor and connection objects. +This handy little function makes working with the database much more +pleasant than it is by just using the raw cursor and connection objects. Here is how you can use it:: for user in query_db('select * from users'): - print(user['username'], 'has the id', user['user_id']) + print user['username'], 'has the id', user['user_id'] Or if you just want a single result:: user = query_db('select * from users where username = ?', [the_username], one=True) if user is None: - print('No such user') + print 'No such user' else: - print(the_username, 'has the id', user['user_id']) + print the_username, 'has the id', user['user_id'] To pass variable parts to the SQL statement, use a question mark in the statement and pass in the arguments as a list. Never directly add them to the SQL statement with string formatting because this makes it possible to attack the application using `SQL Injections -`_. +`_. Initial Schemas --------------- @@ -134,14 +105,15 @@ Relational databases need schemas, so applications often ship a a function that creates the database based on that schema. This function can do that for you:: + from contextlib import closing + def init_db(): - with app.app_context(): - db = get_db() - with app.open_resource('schema.sql', mode='r') as f: + with closing(connect_db()) as db: + with app.open_resource('schema.sql') as f: db.cursor().executescript(f.read()) db.commit() -You can then create such a database from the Python shell: +You can then create such a database from the python shell: >>> from yourapplication import init_db >>> init_db() diff --git a/docs/patterns/streaming.rst b/docs/patterns/streaming.rst index fc2f1739..8393b00b 100644 --- a/docs/patterns/streaming.rst +++ b/docs/patterns/streaming.rst @@ -8,21 +8,6 @@ roundtrip to the filesystem? The answer is by using generators and direct responses. -HTTP Response Behavior ----------------------- - -**Headers cannot be changed after the streaming response starts.** - -When using streaming, it's important to be aware of the order than an HTTP -response is sent. All headers must be sent first, then the body. More headers -cannot be sent after the body has begun. Therefore, you must make sure all -headers are set before starting the response, outside the generator. - -In particular, if the generator will access ``session``, be sure to do so in the -view as well so that the ``Vary: cookie`` header will be set. Do not modify the -session in the generator, as the ``Set-Cookie`` header will already be sent. - - Basic Usage ----------- @@ -30,71 +15,47 @@ This is a basic view function that generates a lot of CSV data on the fly. The trick is to have an inner function that uses a generator to generate data and to then invoke that function and pass it to a response object:: + from flask import Response + @app.route('/large.csv') def generate_large_csv(): def generate(): for row in iter_all_rows(): - yield f"{','.join(row)}\n" - return generate(), {"Content-Type": "text/csv"} + yield ','.join(row) + '\n' + return Response(generate(), mimetype='text/csv') -Each ``yield`` expression is directly sent to the browser. Note though +Each ``yield`` expression is directly sent to the browser. Now though that some WSGI middlewares might break streaming, so be careful there in debug environments with profilers and other things you might have enabled. Streaming from Templates ------------------------ -The Jinja template engine supports rendering a template piece by -piece, returning an iterator of strings. Flask provides the -:func:`~flask.stream_template` and :func:`~flask.stream_template_string` -functions to make this easier to use. +The Jinja2 template engine also supports rendering templates piece by +piece. This functionality is not directly exposed by Flask because it is +quite uncommon, but you can easily do it yourself:: -.. code-block:: python + from flask import Response - from flask import stream_template + def stream_template(template_name, **context): + app.update_template_context(context) + t = app.jinja_env.get_template(template_name) + rv = t.stream(context) + rv.enable_buffering(5) + return rv - @app.get("/timeline") - def timeline(): - return stream_template("timeline.html") + @app.route('/my-large-page.html') + def render_large_template(): + rows = iter_all_rows() + return Response(stream_template('the_template.html', rows=rows)) -The parts yielded by the render stream tend to match statement blocks in -the template. - - -Streaming with Context ----------------------- - -The :data:`.request` proxy will not be active while the generator is -running, because the app has already returned control to the WSGI server at that -point. If you try to access ``request``, you'll get a ``RuntimeError``. - -If your generator function relies on data in ``request``, use the -:func:`.stream_with_context` wrapper. This will keep the request context active -during the generator. - -.. code-block:: python - - from flask import stream_with_context, request - from markupsafe import escape - - @app.route('/stream') - def streamed_response(): - def generate(): - yield '

Hello ' - yield escape(request.args['name']) - yield '!

' - return stream_with_context(generate()) - -It can also be used as a decorator. - -.. code-block:: python - - @stream_with_context - def generate(): - ... - - return generate() - -The :func:`~flask.stream_template` and -:func:`~flask.stream_template_string` functions automatically -use :func:`~flask.stream_with_context` if a request is active. +The trick here is to get the template object from the Jinja2 environment +on the application and to call :meth:`~jinja2.Template.stream` instead of +:meth:`~jinja2.Template.render` which returns a stream object instead of a +string. Since we're bypassing the Flask template render functions and +using the template object itself we have to make sure to update the render +context ourselves by calling :meth:`~flask.Flask.update_template_context`. +The template is then evaluated as the stream is iterated over. Since each +time you do a yield the server will flush the content to the client you +might want to buffer up a few items in the template which you can do with +``rv.enable_buffering(size)``. ``5`` is a sane default. diff --git a/docs/patterns/subclassing.rst b/docs/patterns/subclassing.rst deleted file mode 100644 index d8de2335..00000000 --- a/docs/patterns/subclassing.rst +++ /dev/null @@ -1,17 +0,0 @@ -Subclassing Flask -================= - -The :class:`~flask.Flask` class is designed for subclassing. - -For example, you may want to override how request parameters are handled to preserve their order:: - - from flask import Flask, Request - from werkzeug.datastructures import ImmutableOrderedMultiDict - class MyRequest(Request): - """Request subclass to override request parameter storage""" - parameter_storage_class = ImmutableOrderedMultiDict - class MyFlask(Flask): - """Flask subclass using the custom request class""" - request_class = MyRequest - -This is the recommended approach for overriding or augmenting Flask's internal functionality. diff --git a/docs/patterns/templateinheritance.rst b/docs/patterns/templateinheritance.rst index bb5cba27..70015ecc 100644 --- a/docs/patterns/templateinheritance.rst +++ b/docs/patterns/templateinheritance.rst @@ -1,3 +1,5 @@ +.. _template-inheritance: + Template Inheritance ==================== @@ -12,7 +14,7 @@ with an example. Base Template ------------- -This template, which we'll call :file:`layout.html`, defines a simple HTML skeleton +This template, which we'll call ``layout.html``, defines a simple HTML skeleton document that you might use for a simple two-column page. It's the job of "child" templates to fill the empty blocks with content: @@ -26,15 +28,14 @@ document that you might use for a simple two-column page. It's the job of {% block title %}{% endblock %} - My Webpage {% endblock %} - -
{% block content %}{% endblock %}
- - - + +
{% block content %}{% endblock %}
+ + In this example, the ``{% block %}`` tags define four blocks that child templates can fill in. All the `block` tag does is tell the template engine that a diff --git a/docs/patterns/urlprocessors.rst b/docs/patterns/urlprocessors.rst index 0d743205..778a5a6b 100644 --- a/docs/patterns/urlprocessors.rst +++ b/docs/patterns/urlprocessors.rst @@ -39,8 +39,8 @@ generate URLs from one function to another you would have to still provide the language code explicitly which can be annoying. For the latter, this is where :func:`~flask.Flask.url_defaults` functions -come in. They can automatically inject values into a call to -:func:`~flask.url_for`. The code below checks if the +come in. They can automatically inject values into a call for +:func:`~flask.url_for` automatically. The code below checks if the language code is not yet in the dictionary of URL values and if the endpoint wants a value named ``'lang_code'``:: @@ -65,7 +65,7 @@ dictionary and put it somewhere else:: def pull_lang_code(endpoint, values): g.lang_code = values.pop('lang_code', None) -That way you no longer have to do the `lang_code` assignment to +That way you no longer have to do the `lang_code` assigment to :data:`~flask.g` in every function. You can further improve that by writing your own decorator that prefixes URLs with the language code, but the more beautiful solution is using a blueprint. Once the diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst index 0b0479ef..a0948577 100644 --- a/docs/patterns/viewdecorators.rst +++ b/docs/patterns/viewdecorators.rst @@ -2,12 +2,12 @@ View Decorators =============== Python has a really interesting feature called function decorators. This -allows some really neat things for web applications. Because each view in -Flask is a function, decorators can be used to inject additional +allow some really neat things for web applications. Because each view in +Flask is a function decorators can be used to inject additional functionality to one or more functions. The :meth:`~flask.Flask.route` decorator is the one you probably used already. But there are use cases for implementing your own decorator. For instance, imagine you have a -view that should only be used by people that are logged in. If a user +view that should only be used by people that are logged in to. If a user goes to the site and is not logged in, they should be redirected to the login page. This is a good example of a use case where a decorator is an excellent solution. @@ -16,13 +16,15 @@ Login Required Decorator ------------------------ So let's implement such a decorator. A decorator is a function that -wraps and replaces another function. Since the original function is -replaced, you need to remember to copy the original function's information -to the new function. Use :func:`functools.wraps` to handle this for you. +returns a function. Pretty simple actually. The only thing you have to +keep in mind when implementing something like this is to update the +`__name__`, `__module__` and some other attributes of a function. This is +often forgotten, but you don't have to do that by hand, there is a +function for that that is used like a decorator (:func:`functools.wraps`). This example assumes that the login page is called ``'login'`` and that -the current user is stored in ``g.user`` and is ``None`` if there is no-one -logged in. :: +the current user is stored as `g.user` and `None` if there is no-one +logged in:: from functools import wraps from flask import g, request, redirect, url_for @@ -35,33 +37,24 @@ logged in. :: return f(*args, **kwargs) return decorated_function -To use the decorator, apply it as innermost decorator to a view function. -When applying further decorators, always remember -that the :meth:`~flask.Flask.route` decorator is the outermost. :: +So how would you use that decorator now? Apply it as innermost decorator +to a view function. When applying further decorators, always remember +that the :meth:`~flask.Flask.route` decorator is the outermost:: @app.route('/secret_page') @login_required def secret_page(): pass -.. note:: - The ``next`` value will exist in ``request.args`` after a ``GET`` request for - the login page. You'll have to pass it along when sending the ``POST`` request - from the login form. You can do this with a hidden input tag, then retrieve it - from ``request.form`` when logging the user in. :: - - - - Caching Decorator ----------------- Imagine you have a view function that does an expensive calculation and because of that you would like to cache the generated results for a certain amount of time. A decorator would be nice for that. We're -assuming you have set up a cache like mentioned in :doc:`caching`. +assuming you have set up a cache like mentioned in :ref:`caching-pattern`. -Here is an example cache function. It generates the cache key from a +Here an example cache function. It generates the cache key from a specific prefix (actually a format string) and the current path of the request. Notice that we are using a function that first creates the decorator that then decorates the function. Sounds awful? Unfortunately @@ -70,7 +63,7 @@ straightforward to read. The decorated function will then work as follows -1. get the unique cache key for the current request based on the current +1. get the unique cache key for the current request base on the current path. 2. get the value for that key from the cache. If the cache returned something we will return that value. @@ -82,11 +75,11 @@ Here the code:: from functools import wraps from flask import request - def cached(timeout=5 * 60, key='view/{}'): + def cached(timeout=5 * 60, key='view/%s'): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): - cache_key = key.format(request.path) + cache_key = key % request.path rv = cache.get(cache_key) if rv is not None: return rv @@ -96,8 +89,8 @@ Here the code:: return decorated_function return decorator -Notice that this assumes an instantiated ``cache`` object is available, see -:doc:`caching`. +Notice that this assumes an instantiated `cache` object is available, see +:ref:`caching-pattern` for more information. Templating Decorator @@ -127,14 +120,14 @@ As you can see, if no template name is provided it will use the endpoint of the URL map with dots converted to slashes + ``'.html'``. Otherwise the provided template name is used. When the decorated function returns, the dictionary returned is passed to the template rendering function. If -``None`` is returned, an empty dictionary is assumed, if something else than +`None` is returned, an empty dictionary is assumed, if something else than a dictionary is returned we return it from the function unchanged. That way you can still use the redirect function or return simple strings. -Here is the code for that decorator:: +Here the code for that decorator:: from functools import wraps - from flask import request, render_template + from flask import request def templated(template=None): def decorator(f): @@ -142,7 +135,8 @@ Here is the code for that decorator:: def decorated_function(*args, **kwargs): template_name = template if template_name is None: - template_name = f"{request.endpoint.replace('.', '/')}.html" + template_name = request.endpoint \ + .replace('.', '/') + '.html' ctx = f(*args, **kwargs) if ctx is None: ctx = {} @@ -157,15 +151,18 @@ Endpoint Decorator ------------------ When you want to use the werkzeug routing system for more flexibility you -need to map the endpoint as defined in the :class:`~werkzeug.routing.Rule` -to a view function. This is possible with this decorator. For example:: +need to map the endpoint as defined in the :class:`~werkzeug.routing.Rule` +to a view function. This is possible with this decorator. For example:: from flask import Flask from werkzeug.routing import Rule - app = Flask(__name__) - app.url_map.add(Rule('/', endpoint='index')) + app = Flask(__name__) + app.url_map.add(Rule('/', endpoint='index')) + + @app.endpoint('index') + def my_index(): + return "Hello world" + + - @app.endpoint('index') - def my_index(): - return "Hello world" diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index cb1208fe..93824df7 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -1,42 +1,42 @@ Form Validation with WTForms ============================ -When you have to work with form data submitted by a browser view, code +When you have to work with form data submitted by a browser view code quickly becomes very hard to read. There are libraries out there designed to make this process easier to manage. One of them is `WTForms`_ which we will handle here. If you find yourself in the situation of having many forms, you might want to give it a try. When you are working with WTForms you have to define your forms as classes -first. I recommend breaking up the application into multiple modules -(:doc:`packages`) for that and adding a separate module for the +first. I recommend breaking up the application into multiple modules +(:ref:`larger-applications`) for that and adding a separate module for the forms. -.. admonition:: Getting the most out of WTForms with an Extension +.. admonition:: Getting most of WTForms with an Extension - The `Flask-WTF`_ extension expands on this pattern and adds a - few little helpers that make working with forms and Flask more + The `Flask-WTF`_ extension expands on this pattern and adds a few + handful little helpers that make working with forms and Flask more fun. You can get it from `PyPI - `_. + `_. -.. _Flask-WTF: https://flask-wtf.readthedocs.io/ +.. _Flask-WTF: http://packages.python.org/Flask-WTF/ The Forms --------- This is an example form for a typical registration page:: - from wtforms import Form, BooleanField, StringField, PasswordField, validators + from wtforms import Form, BooleanField, TextField, validators class RegistrationForm(Form): - username = StringField('Username', [validators.Length(min=4, max=25)]) - email = StringField('Email Address', [validators.Length(min=6, max=35)]) + username = TextField('Username', [validators.Length(min=4, max=25)]) + email = TextField('Email Address', [validators.Length(min=6, max=35)]) password = PasswordField('New Password', [ - validators.DataRequired(), + validators.Required(), validators.EqualTo('confirm', message='Passwords must match') ]) confirm = PasswordField('Repeat Password') - accept_tos = BooleanField('I accept the TOS', [validators.DataRequired()]) + accept_tos = BooleanField('I accept the TOS', [validators.Required()]) In the View ----------- @@ -54,30 +54,30 @@ In the view function, the usage of this form looks like this:: return redirect(url_for('login')) return render_template('register.html', form=form) -Notice we're implying that the view is using SQLAlchemy here -(:doc:`sqlalchemy`), but that's not a requirement, of course. Adapt +Notice that we are implying that the view is using SQLAlchemy here +(:ref:`sqlalchemy-pattern`) but this is no requirement of course. Adapt the code as necessary. Things to remember: 1. create the form from the request :attr:`~flask.request.form` value if - the data is submitted via the HTTP ``POST`` method and - :attr:`~flask.request.args` if the data is submitted as ``GET``. + the data is submitted via the HTTP `POST` method and + :attr:`~flask.request.args` if the data is submitted as `GET`. 2. to validate the data, call the :func:`~wtforms.form.Form.validate` - method, which will return ``True`` if the data validates, ``False`` + method which will return `True` if the data validates, `False` otherwise. 3. to access individual values from the form, access `form..data`. Forms in Templates ------------------ -Now to the template side. When you pass the form to the templates, you can +Now to the template side. When you pass the form to the templates you can easily render them there. Look at the following example template to see how easy this is. WTForms does half the form generation for us already. To make it even nicer, we can write a macro that renders a field with label and a list of errors if there are any. -Here's an example :file:`_formhelpers.html` template with such a macro: +Here's an example `_formhelpers.html` template with such a macro: .. sourcecode:: html+jinja @@ -85,30 +85,28 @@ Here's an example :file:`_formhelpers.html` template with such a macro:
{{ field.label }}
{{ field(**kwargs)|safe }} {% if field.errors %} -
    - {% for error in field.errors %} -
  • {{ error }}
  • - {% endfor %} +
      + {% for error in field.errors %}
    • {{ error }}{% endfor %}
    {% endif %}
{% endmacro %} This macro accepts a couple of keyword arguments that are forwarded to -WTForm's field function, which renders the field for us. The keyword -arguments will be inserted as HTML attributes. So, for example, you can +WTForm's field function that renders the field for us. The keyword +arguments will be inserted as HTML attributes. So for example you can call ``render_field(form.username, class='username')`` to add a class to -the input element. Note that WTForms returns standard Python strings, -so we have to tell Jinja that this data is already HTML-escaped with -the ``|safe`` filter. +the input element. Note that WTForms returns standard Python unicode +strings, so we have to tell Jinja2 that this data is already HTML escaped +with the `|safe` filter. -Here is the :file:`register.html` template for the function we used above, which -takes advantage of the :file:`_formhelpers.html` template: +Here the `register.html` template for the function we used above which +takes advantage of the `_formhelpers.html` template: .. sourcecode:: html+jinja {% from "_formhelpers.html" import render_field %} -
+
{{ render_field(form.username) }} {{ render_field(form.email) }} @@ -122,5 +120,5 @@ takes advantage of the :file:`_formhelpers.html` template: For more information about WTForms, head over to the `WTForms website`_. -.. _WTForms: https://wtforms.readthedocs.io/ -.. _WTForms website: https://wtforms.readthedocs.io/ +.. _WTForms: http://wtforms.simplecodes.com/ +.. _WTForms website: http://wtforms.simplecodes.com/ diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 712ba977..34aa3be4 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,164 +1,135 @@ +.. _quickstart: + Quickstart ========== -Eager to get started? This page gives a good introduction to Flask. -Follow :doc:`installation` to set up a project and install Flask first. +Eager to get started? This page gives a good introduction in how to get +started with Flask. This assumes you already have Flask installed. If +you do not, head over to the :ref:`installation` section. A Minimal Application --------------------- -A minimal Flask application looks something like this: - -.. code-block:: python +A minimal Flask application looks something like this:: from flask import Flask - app = Flask(__name__) - @app.route("/") + @app.route('/') def hello_world(): - return "

Hello, World!

" + return 'Hello World!' + + if __name__ == '__main__': + app.run() + +Just save it as `hello.py` or something similar and run it with your +Python interpreter. Make sure to not call your application `flask.py` +because this would conflict with Flask itself. + +:: + + $ python hello.py + * Running on http://127.0.0.1:5000/ + +Head over to `http://127.0.0.1:5000/ `_, you should +see your hello world greeting. So what did that code do? -1. First we imported the :class:`~flask.Flask` class. An instance of - this class will be our WSGI application. -2. Next we create an instance of this class. The first argument is the - name of the application's module or package. ``__name__`` is a - convenient shortcut for this that is appropriate for most cases. - This is needed so that Flask knows where to look for resources such - as templates and static files. -3. We then use the :meth:`~flask.Flask.route` decorator to tell Flask - what URL should trigger our function. -4. The function returns the message we want to display in the user's - browser. The default content type is HTML, so HTML in the string - will be rendered by the browser. +1. First we imported the :class:`~flask.Flask` class. An instance of this + class will be our WSGI application. The first argument is the name of + the application's module. If you are using a single module (like here) + you should use `__name__` because depending on if it's started as + application or imported as module the name will be different + (``'__main__'`` versus the actual import name). For more information + on that, have a look at the :class:`~flask.Flask` documentation. +2. Next we create an instance of it. We pass it the name of the module / + package. This is needed so that Flask knows where it should look for + templates, static files and so on. +3. Then we use the :meth:`~flask.Flask.route` decorator to tell Flask + what URL should trigger our function. +4. The function then has a name which is also used to generate URLs to + that particular function, and returns the message we want to display in + the user's browser. +5. Finally we use the :meth:`~flask.Flask.run` function to run the + local server with our application. The ``if __name__ == '__main__':`` + makes sure the server only runs if the script is executed directly from + the Python interpreter and not used as imported module. -Save it as :file:`hello.py` or something similar. Make sure to not call -your application :file:`flask.py` because this would conflict with Flask -itself. - -To run the application, use the ``flask`` command or -``python -m flask``. You need to tell the Flask where your application -is with the ``--app`` option. - -.. code-block:: text - - $ flask --app hello run - * Serving Flask app 'hello' - * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) - -.. admonition:: Application Discovery Behavior - - As a shortcut, if the file is named ``app.py`` or ``wsgi.py``, you - don't have to use ``--app``. See :doc:`/cli` for more details. - -This launches a very simple builtin server, which is good enough for -testing but probably not what you want to use in production. For -deployment options see :doc:`deploying/index`. - -Now head over to http://127.0.0.1:5000/, and you should see your hello -world greeting. - -If another program is already using port 5000, you'll see -``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the -server tries to start. See :ref:`address-already-in-use` for how to -handle that. +To stop the server, hit control-C. .. _public-server: .. admonition:: Externally Visible Server - If you run the server you will notice that the server is only accessible + If you run the server you will notice that the server is only available from your own computer, not from any other in the network. This is the default because in debugging mode a user of the application can execute - arbitrary Python code on your computer. + arbitrary Python code on your computer. If you have `debug` disabled + or trust the users on your network, you can make the server publicly + available. - If you have the debugger disabled or trust the users on your network, - you can make the server publicly available simply by adding - ``--host=0.0.0.0`` to the command line:: + Just change the call of the :meth:`~flask.Flask.run` method to look + like this:: - $ flask run --host=0.0.0.0 + app.run(host='0.0.0.0') - This tells your operating system to listen on all public IPs. + This tells your operating system to listen on a public IP. Debug Mode ---------- -The ``flask run`` command can do more than just start the development -server. By enabling debug mode, the server will automatically reload if -code changes, and will show an interactive debugger in the browser if an -error occurs during a request. +The :meth:`~flask.Flask.run` method is nice to start a local +development server, but you would have to restart it manually after each +change you do to code. That is not very nice and Flask can do better. If +you enable the debug support the server will reload itself on code changes +and also provide you with a helpful debugger if things go wrong. + +There are two ways to enable debugging. Either set that flag on the +application object:: + + app.debug = True + app.run() + +Or pass it to run:: + + app.run(debug=True) + +Both will have exactly the same effect. + +.. admonition:: Attention + + Even though the interactive debugger does not work in forking environments + (which makes it nearly impossible to use on production servers), it still + allows the execution of arbitrary code. That makes it a major security + risk and therefore it **must never be used on production machines**. + +Screenshot of the debugger in action: .. image:: _static/debugger.png - :align: center - :class: screenshot - :alt: The interactive debugger in action. + :align: center + :class: screenshot + :alt: screenshot of debugger in action -.. warning:: +.. admonition:: Working With Other Debuggers - The debugger allows executing arbitrary Python code from the - browser. It is protected by a pin, but still represents a major - security risk. Do not run the development server or debugger in a - production environment. - -To enable debug mode, use the ``--debug`` option. - -.. code-block:: text - - $ flask --app hello run --debug - * Serving Flask app 'hello' - * Debug mode: on - * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) - * Restarting with stat - * Debugger is active! - * Debugger PIN: nnn-nnn-nnn - -See also: - -- :doc:`/server` and :doc:`/cli` for information about running in debug mode. -- :doc:`/debugging` for information about using the built-in debugger - and other debuggers. -- :doc:`/logging` and :doc:`/errorhandling` to log errors and display - nice error pages. - - -HTML Escaping -------------- - -When returning HTML (the default response type in Flask), any -user-provided values rendered in the output must be escaped to protect -from injection attacks. HTML templates rendered with Jinja, introduced -later, will do this automatically. - -:func:`~markupsafe.escape`, shown here, can be used manually. It is -omitted in most examples for brevity, but you should always be aware of -how you're using untrusted data. - -.. code-block:: python - - from flask import request - from markupsafe import escape - - @app.route("/hello") - def hello(): - name = request.args.get("name", "Flask") - return f"Hello, {escape(name)}!" - -If a user submits ``/hello?name=``, escaping causes -it to be rendered as text, rather than running the script in the user's browser. + Debuggers interfere with each other. If you are using another debugger + (e.g. PyDev or IntelliJ), you may need to set ``app.debug = False``. Routing ------- -Modern web applications use meaningful URLs to help users. Users are more -likely to like a page and come back if the page uses a meaningful URL they can -remember and use to directly visit a page. +Modern web applications have beautiful URLs. This helps people remember +the URLs which is especially handy for applications that are used from +mobile devices with slower network connections. If the user can directly +go to the desired page without having to hit the index page it is more +likely they will like the page and come back next time. -Use the :meth:`~flask.Flask.route` decorator to bind a function to a URL. :: +As you have seen above, the :meth:`~flask.Flask.route` decorator is used +to bind a function to a URL. Here are some basic examples:: @app.route('/') def index(): @@ -166,70 +137,68 @@ Use the :meth:`~flask.Flask.route` decorator to bind a function to a URL. :: @app.route('/hello') def hello(): - return 'Hello, World' + return 'Hello World' -You can do more! You can make parts of the URL dynamic and attach multiple -rules to a function. +But there is more to it! You can make certain parts of the URL dynamic +and attach multiple rules to a function. Variable Rules `````````````` -You can add variable sections to a URL by marking sections with -````. Your function then receives the ```` -as a keyword argument. Optionally, you can use a converter to specify the type -of the argument like ````. :: - - from markupsafe import escape +To add variable parts to a URL you can mark these special sections as +````. Such a part is then passed as keyword argument to +your function. Optionally a converter can be specified by specifying a +rule with ````. Here are some nice examples:: @app.route('/user/') def show_user_profile(username): # show the user profile for that user - return f'User {escape(username)}' + pass @app.route('/post/') def show_post(post_id): # show the post with the given id, the id is an integer - return f'Post {post_id}' + pass - @app.route('/path/') - def show_subpath(subpath): - # show the subpath after /path/ - return f'Subpath {escape(subpath)}' +The following converters exist: -Converter types: +=========== =========================================== +`int` accepts integers +`float` like `int` but for floating point values +`path` like the default but also accepts slashes +=========== =========================================== -========== ========================================== -``string`` (default) accepts any text without a slash -``int`` accepts positive integers -``float`` accepts positive floating point values -``path`` like ``string`` but also accepts slashes -``uuid`` accepts UUID strings -========== ========================================== +.. admonition:: Unique URLs / Redirection Behaviour + Flask's URL rules are based on Werkzeug's routing module. The idea + behind that module is to ensure nice looking and also unique URLs based + on behaviour Apache and earlier servers coined. -Unique URLs / Redirection Behavior -`````````````````````````````````` + Take these two rules:: -The following two rules differ in their use of a trailing slash. :: + @app.route('/projects/') + def projects(): + pass - @app.route('/projects/') - def projects(): - return 'The project page' + @app.route('/about') + def about(): + pass - @app.route('/about') - def about(): - return 'The about page' + They look rather similar, the difference is the trailing slash in the + URL *definition*. In the first case, the canonical URL for the + `projects` endpoint has a trailing slash. It's similar to a folder in + that sense. Accessing it without a trailing slash will cause Flask to + redirect to the canonical URL with the trailing slash. -The canonical URL for the ``projects`` endpoint has a trailing slash. -It's similar to a folder in a file system. If you access the URL without -a trailing slash (``/projects``), Flask redirects you to the canonical URL -with the trailing slash (``/projects/``). + However in the second case the URL is defined without a slash so it + behaves similar to a file and accessing the URL with a trailing slash + will be a 404 error. -The canonical URL for the ``about`` endpoint does not have a trailing -slash. It's similar to the pathname of a file. Accessing the URL with a -trailing slash (``/about/``) produces a 404 "Not Found" error. This helps -keep URLs unique for these resources, which helps search engines avoid -indexing the same page twice. + Why is this? This allows relative URLs to continue working if users + access the page when they forget a trailing slash. This behaviour is + also consistent with how Apache and other servers work. Also, the URLs + will stay unique which helps search engines not indexing the same page + twice. .. _url-building: @@ -237,134 +206,153 @@ indexing the same page twice. URL Building ```````````` -To build a URL to a specific function, use the :func:`~flask.url_for` function. -It accepts the name of the function as its first argument and any number of -keyword arguments, each corresponding to a variable part of the URL rule. -Unknown variable parts are appended to the URL as query parameters. +If it can match URLs, can it also generate them? Of course it can. To +build a URL to a specific function you can use the :func:`~flask.url_for` +function. It accepts the name of the function as first argument and a +number of keyword arguments, each corresponding to the variable part of +the URL rule. Unknown variable parts are appended to the URL as query +parameter. Here are some examples: -Why would you want to build URLs using the URL reversing function -:func:`~flask.url_for` instead of hard-coding them into your templates? +>>> from flask import Flask, url_for +>>> app = Flask(__name__) +>>> @app.route('/') +... def index(): pass +... +>>> @app.route('/login') +... def login(): pass +... +>>> @app.route('/user/') +... def profile(username): pass +... +>>> with app.test_request_context(): +... print url_for('index') +... print url_for('login') +... print url_for('login', next='/') +... print url_for('profile', username='John Doe') +... +/ +/login +/login?next=/ +/user/John%20Doe -1. Reversing is often more descriptive than hard-coding the URLs. -2. You can change your URLs in one go instead of needing to remember to - manually change hard-coded URLs. -3. URL building handles escaping of special characters transparently. -4. The generated paths are always absolute, avoiding unexpected behavior - of relative paths in browsers. -5. If your application is placed outside the URL root, for example, in - ``/myapplication`` instead of ``/``, :func:`~flask.url_for` properly - handles that for you. +(This also uses the :meth:`~flask.Flask.test_request_context` method +explained below. It basically tells Flask to think we are handling a +request even though we are not, we are in an interactive Python shell. +Have a look at the explanation below. :ref:`context-locals`). -For example, here we use the :meth:`~flask.Flask.test_request_context` method -to try out :func:`~flask.url_for`. :meth:`~flask.Flask.test_request_context` -tells Flask to behave as though it's handling a request even while we use a -Python shell. See :doc:`/appcontext`. +Why would you want to build URLs instead of hardcoding them in your +templates? There are three good reasons for this: -.. code-block:: python - - from flask import url_for - - @app.route('/') - def index(): - return 'index' - - @app.route('/login') - def login(): - return 'login' - - @app.route('/user/') - def profile(username): - return f'{username}\'s profile' - - with app.test_request_context(): - print(url_for('index')) - print(url_for('login')) - print(url_for('login', next='/')) - print(url_for('profile', username='John Doe')) - -.. code-block:: text - - / - /login - /login?next=/ - /user/John%20Doe +1. reversing is often more descriptive than hardcoding the URLs. Also and + more importantly you can change URLs in one go without having to change + the URLs all over the place. +2. URL building will handle escaping of special characters and Unicode + data transparently for you, you don't have to deal with that. +3. If your application is placed outside the URL root (so say in + ``/myapplication`` instead of ``/``), :func:`~flask.url_for` will + handle that properly for you. HTTP Methods ```````````` -Web applications use different HTTP methods when accessing URLs. You should -familiarize yourself with the HTTP methods as you work with Flask. By default, -a route only answers to ``GET`` requests. You can use the ``methods`` argument -of the :meth:`~flask.Flask.route` decorator to handle different HTTP methods. -:: - - from flask import request +HTTP (the protocol web applications are speaking) knows different methods +to access URLs. By default a route only answers to `GET` requests, but +that can be changed by providing the `methods` argument to the +:meth:`~flask.Flask.route` decorator. Here are some examples:: @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': - return do_the_login() + do_the_login() else: - return show_the_login_form() + show_the_login_form() -The example above keeps all methods for the route within one function, -which can be useful if each part uses some common data. +If `GET` is present, `HEAD` will be added automatically for you. You +don't have to deal with that. It will also make sure that `HEAD` requests +are handled like the `HTTP RFC`_ (the document describing the HTTP +protocol) demands, so you can completely ignore that part of the HTTP +specification. Likewise as of Flask 0.6, `OPTIONS` is implemented for you +as well automatically. -You can also separate views for different methods into different -functions. Flask provides a shortcut for decorating such routes with -:meth:`~flask.Flask.get`, :meth:`~flask.Flask.post`, etc. for each -common HTTP method. +You have no idea what an HTTP method is? Worry not, here is a quick +introduction to HTTP methods and why they matter: -.. code-block:: python +The HTTP method (also often called "the verb") tells the server what the +clients wants to *do* with the requested page. The following methods are +very common: - @app.get('/login') - def login_get(): - return show_the_login_form() +`GET` + The browser tells the server to just *get* the information stored on + that page and send it. This is probably the most common method. - @app.post('/login') - def login_post(): - return do_the_login() +`HEAD` + The browser tells the server to get the information, but it is only + interested in the *headers*, not the content of the page. An + application is supposed to handle that as if a `GET` request was + received but to not deliver the actual content. In Flask you don't + have to deal with that at all, the underlying Werkzeug library handles + that for you. -If ``GET`` is present, Flask automatically adds support for the ``HEAD`` method -and handles ``HEAD`` requests according to the `HTTP RFC`_. Likewise, -``OPTIONS`` is automatically implemented for you. +`POST` + The browser tells the server that it wants to *post* some new + information to that URL and that the server must ensure the data is + stored and only stored once. This is how HTML forms are usually + transmitting data to the server. -.. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt +`PUT` + Similar to `POST` but the server might trigger the store procedure + multiple times by overwriting the old values more than once. Now you + might be asking why is this useful, but there are some good reasons + to do it this way. Consider that the connection gets lost during + transmission: in this situation a system between the browser and the + server might receive the request safely a second time without breaking + things. With `POST` that would not be possible because it must only + be triggered once. + +`DELETE` + Remove the information at the given location. + +`OPTIONS` + Provides a quick way for a client to figure out which methods are + supported by this URL. Starting with Flask 0.6, this is implemented + for you automatically. + +Now the interesting part is that in HTML4 and XHTML1, the only methods a +form can submit to the server are `GET` and `POST`. But with JavaScript +and future HTML standards you can use the other methods as well. Furthermore +HTTP has become quite popular lately and browsers are no longer the only +clients that are using HTTP. For instance, many revision control system +use it. + +.. _HTTP RFC: http://www.ietf.org/rfc/rfc2068.txt Static Files ------------ -Dynamic web applications also need static files. That's usually where +Dynamic web applications need static files as well. That's usually where the CSS and JavaScript files are coming from. Ideally your web server is configured to serve them for you, but during development Flask can do that -as well. Just create a folder called :file:`static` in your package or next to -your module and it will be available at ``/static`` on the application. +as well. Just create a folder called `static` in your package or next to +your module and it will be available at `/static` on the application. -To generate URLs for static files, use the special ``'static'`` endpoint name:: +To generate URLs to that part of the URL, use the special ``'static'`` URL +name:: url_for('static', filename='style.css') -The file has to be stored on the filesystem as :file:`static/style.css`. +The file has to be stored on the filesystem as ``static/style.css``. Rendering Templates ------------------- Generating HTML from within Python is not fun, and actually pretty cumbersome because you have to do the HTML escaping on your own to keep -the application secure. Because of that Flask configures the `Jinja -`_ template engine for you automatically. - -Templates can be used to generate any type of text file. For web applications, you'll -primarily be generating HTML pages, but you can also generate markdown, plain text for -emails, and anything else. - -For a reference to HTML, CSS, and other web APIs, use the `MDN Web Docs`_. - -.. _MDN Web Docs: https://developer.mozilla.org/ +the application secure. Because of that Flask configures the `Jinja2 +`_ template engine for you automatically. To render a template you can use the :func:`~flask.render_template` -method. All you have to do is provide the name of the template and the +method. All you have to do is to provide the name of the template and the variables you want to pass to the template engine as keyword arguments. Here's a simple example of how to render a template:: @@ -373,10 +361,10 @@ Here's a simple example of how to render a template:: @app.route('/hello/') @app.route('/hello/') def hello(name=None): - return render_template('hello.html', person=name) + return render_template('hello.html', name=name) -Flask will look for templates in the :file:`templates` folder. So if your -application is a module, this folder is next to that module, if it's a +Flask will look for templates in the `templates` folder. So if your +application is a module, that folder is next to that module, if it's a package it's actually inside your package: **Case 1**: a module:: @@ -392,9 +380,9 @@ package it's actually inside your package: /templates /hello.html -For templates you can use the full power of Jinja templates. Head over -to the official `Jinja Template Documentation -`_ for more information. +For templates you can use the full power of Jinja2 templates. Head over +to the the official `Jinja2 Template Documentation +`_ for more information. Here is an example template: @@ -402,37 +390,37 @@ Here is an example template: Hello from Flask - {% if person %} -

Hello {{ person }}!

+ {% if name %} +

Hello {{ name }}!

{% else %} -

Hello, World!

+

Hello World!

{% endif %} -Inside templates you also have access to the :data:`~flask.Flask.config`, -:class:`~flask.request`, :class:`~flask.session` and :class:`~flask.g` [#]_ objects -as well as the :func:`~flask.url_for` and :func:`~flask.get_flashed_messages` functions. +Inside templates you also have access to the :class:`~flask.request`, +:class:`~flask.session` and :class:`~flask.g` [#]_ objects +as well as the :func:`~flask.get_flashed_messages` function. Templates are especially useful if inheritance is used. If you want to -know how that works, see :doc:`patterns/templateinheritance`. Basically -template inheritance makes it possible to keep certain elements on each -page (like header, navigation and footer). +know how that works, head over to the :ref:`template-inheritance` pattern +documentation. Basically template inheritance makes it possible to keep +certain elements on each page (like header, navigation and footer). -Automatic escaping is enabled, so if ``person`` contains HTML it will be escaped +Automatic escaping is enabled, so if name contains HTML it will be escaped automatically. If you can trust a variable and you know that it will be -safe HTML (for example because it came from a module that converts wiki +safe HTML (because for example it came from a module that converts wiki markup to HTML) you can mark it as safe by using the -:class:`~markupsafe.Markup` class or by using the ``|safe`` filter in the +:class:`~jinja2.Markup` class or by using the ``|safe`` filter in the template. Head over to the Jinja 2 documentation for more examples. -Here is a basic introduction to how the :class:`~markupsafe.Markup` class works:: +Here is a basic introduction to how the :class:`~jinja2.Markup` class works: - >>> from markupsafe import Markup - >>> Markup('Hello %s!') % 'hacker' - Markup('Hello <blink>hacker</blink>!') - >>> Markup.escape('hacker') - Markup('<blink>hacker</blink>') - >>> Markup('Marked up » HTML').striptags() - 'Marked up » HTML' +>>> from flask import Markup +>>> Markup('Hello %s!') % 'hacker' +Markup(u'Hello <blink>hacker</blink>!') +>>> Markup.escape('hacker') +Markup(u'<blink>hacker</blink>') +>>> Markup('Marked up » HTML').striptags() +u'Marked up \xbb HTML' .. versionchanged:: 0.5 @@ -442,65 +430,114 @@ Here is a basic introduction to how the :class:`~markupsafe.Markup` class works: autoescaping disabled. .. [#] Unsure what that :class:`~flask.g` object is? It's something in which - you can store information for your own needs. See the documentation - for :class:`flask.g` and :doc:`patterns/sqlite3`. + you can store information for your own needs, check the documentation of + that object (:class:`~flask.g`) and the :ref:`sqlite3` for more + information. Accessing Request Data ---------------------- -For web applications it's crucial to react to the data a client sends to the -server. In Flask this information is provided by the global :data:`.request` -object, which is an instance of :class:`.Request`. This object has many -attributes and methods to work with the incoming request data, but here is a -broad overview. First it needs to be imported. +For web applications it's crucial to react to the data a client sent to +the server. In Flask this information is provided by the global +:class:`~flask.request` object. If you have some experience with Python +you might be wondering how that object can be global and how Flask +manages to still be threadsafe. The answer are context locals: -.. code-block:: python + +.. _context-locals: + +Context Locals +`````````````` + +.. admonition:: Insider Information + + If you want to understand how that works and how you can implement + tests with context locals, read this section, otherwise just skip it. + +Certain objects in Flask are global objects, but not of the usual kind. +These objects are actually proxies to objects that are local to a specific +context. What a mouthful. But that is actually quite easy to understand. + +Imagine the context being the handling thread. A request comes in and the +webserver decides to spawn a new thread (or something else, the +underlying object is capable of dealing with other concurrency systems +than threads as well). When Flask starts its internal request handling it +figures out that the current thread is the active context and binds the +current application and the WSGI environments to that context (thread). +It does that in an intelligent way that one application can invoke another +application without breaking. + +So what does this mean to you? Basically you can completely ignore that +this is the case unless you are doing something like unittesting. You +will notice that code that depends on a request object will suddenly break +because there is no request object. The solution is creating a request +object yourself and binding it to the context. The easiest solution for +unittesting is by using the :meth:`~flask.Flask.test_request_context` +context manager. In combination with the `with` statement it will bind a +test request so that you can interact with it. Here is an example:: from flask import request -If you have some experience with Python you might be wondering how that object -can be global when Flask handles multiple requests at a time. The answer is -that :data:`.request` is actually a proxy, pointing at whatever request is -currently being handled by a given worker, which is managed internally by Flask -and Python. See :doc:`/appcontext` for much more information. + with app.test_request_context('/hello', method='POST'): + # now you can do something with the request until the + # end of the with block, such as basic assertions: + assert request.path == '/hello' + assert request.method == 'POST' -The current request method is available in the :attr:`~.Request.method` -attribute. To access form data (data transmitted in a ``POST`` or ``PUT`` -request), use the :attr:`~flask.Request.form` attribute, which behaves like a -dict. +The other possibility is passing a whole WSGI environment to the +:meth:`~flask.Flask.request_context` method:: -.. code-block:: python + from flask import request - @app.route("/login", methods=["GET", "POST"]) + with app.request_context(environ): + assert request.method == 'POST' + +The Request Object +`````````````````` + +The request object is documented in the API section and we will not cover +it here in detail (see :class:`~flask.request`). Here is a broad overview of +some of the most common operations. First of all you have to import it from +the `flask` module:: + + from flask import request + +The current request method is available by using the +:attr:`~flask.request.method` attribute. To access form data (data +transmitted in a `POST` or `PUT` request) you can use the +:attr:`~flask.request.form` attribute. Here is a full example of the two +attributes mentioned above:: + + @app.route('/login', methods=['POST', 'GET']) def login(): error = None - - if request.method == "POST": - if valid_login(request.form["username"], request.form["password"]): - return store_login(request.form["username"]) + if request.method == 'POST': + if valid_login(request.form['username'], + request.form['password']): + return log_the_user_in(request.form['username']) else: - error = "Invalid username or password" + error = 'Invalid username/password' + # this is executed if the request method was GET or the + # credentials were invalid - # Executed if the request method was GET or the credentials were invalid. - return render_template("login.html", error=error) +What happens if the key does not exist in the `form` attribute? In that +case a special :exc:`KeyError` is raised. You can catch it like a +standard :exc:`KeyError` but if you don't do that, a HTTP 400 Bad Request +error page is shown instead. So for many situations you don't have to +deal with that problem. -If the key does not exist in ``form``, a special :exc:`KeyError` is raised. You -can catch it like a normal ``KeyError``, otherwise it will return a HTTP 400 -Bad Request error page. You can also use the -:meth:`~werkzeug.datastructures.MultiDict.get` method to get a default -instead of an error. +To access parameters submitted in the URL (``?key=value``) you can use the +:attr:`~flask.request.args` attribute:: -To access parameters submitted in the URL (``?key=value``), use the -:attr:`~.Request.args` attribute. Key errors behave the same as ``form``, -returning a 400 response if not caught. + searchword = request.args.get('q', '') -.. code-block:: python +We recommend accessing URL parameters with `get` or by catching the +`KeyError` because users might change the URL and presenting them a 400 +bad request page in that case is not user friendly. - searchword = request.args.get('key', '') - -For a full list of methods and attributes of the request object, see the -:class:`~.Request` documentation. +For a full list of methods and attributes of the request object, head over +to the :class:`~flask.request` documentation. File Uploads @@ -515,9 +552,9 @@ filesystem. You can access those files by looking at the :attr:`~flask.request.files` attribute on the request object. Each uploaded file is stored in that dictionary. It behaves just like a standard Python :class:`file` object, but it also has a -:meth:`~werkzeug.datastructures.FileStorage.save` method that -allows you to store that file on the filesystem of the server. -Here is a simple example showing how that works:: +:meth:`~werkzeug.datastructures.FileStorage.save` method that allows you to store that +file on the filesystem of the server. Here is a simple example showing how +that works:: from flask import request @@ -530,23 +567,23 @@ Here is a simple example showing how that works:: If you want to know how the file was named on the client before it was uploaded to your application, you can access the -:attr:`~werkzeug.datastructures.FileStorage.filename` attribute. -However please keep in mind that this value can be forged -so never ever trust that value. If you want to use the filename -of the client to store the file on the server, pass it through the -:func:`~werkzeug.utils.secure_filename` function that +:attr:`~werkzeug.datastructures.FileStorage.filename` attribute. However please keep in +mind that this value can be forged so never ever trust that value. If you +want to use the filename of the client to store the file on the server, +pass it through the :func:`~werkzeug.utils.secure_filename` function that Werkzeug provides for you:: - from werkzeug.utils import secure_filename + from flask import request + from werkzeug import secure_filename @app.route('/upload', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': - file = request.files['the_file'] - file.save(f"/var/www/uploads/{secure_filename(file.filename)}") + f = request.files['the_file'] + f.save('/var/www/uploads/' + secure_filename(f.filename)) ... -For some better examples, see :doc:`patterns/fileuploads`. +For some better examples, checkout the :ref:`uploading-files` pattern. Cookies ``````` @@ -579,23 +616,23 @@ Storing cookies:: resp.set_cookie('username', 'the username') return resp -Note that cookies are set on response objects. Since you normally +Note that cookies are set on response objects. Since you normally you just return strings from the view functions Flask will convert them into response objects for you. If you explicitly want to do that you can use the :meth:`~flask.make_response` function and then modify it. Sometimes you might want to set a cookie at a point where the response object does not exist yet. This is possible by utilizing the -:doc:`patterns/deferredcallbacks` pattern. +:ref:`deferred-callbacks` pattern. For this also see :ref:`about-responses`. Redirects and Errors -------------------- -To redirect a user to another endpoint, use the :func:`~flask.redirect` -function; to abort a request early with an error code, use the -:func:`~flask.abort` function:: +To redirect a user to somewhere else you can use the +:func:`~flask.redirect` function. To abort a request early with an error +code use the :func:`~flask.abort` function. Here an example how this works:: from flask import abort, redirect, url_for @@ -626,54 +663,42 @@ Note the ``404`` after the :func:`~flask.render_template` call. This tells Flask that the status code of that page should be 404 which means not found. By default 200 is assumed which translates to: all went well. -See :doc:`errorhandling` for more details. - .. _about-responses: About Responses --------------- -The return value from a view function is automatically converted into -a response object for you. If the return value is a string it's -converted into a response object with the string as response body, a -``200 OK`` status code and a :mimetype:`text/html` mimetype. If the -return value is a dict or list, :func:`jsonify` is called to produce a -response. The logic that Flask applies to converting return values into -response objects is as follows: +The return value from a view function is automatically converted into a +response object for you. If the return value is a string it's converted +into a response object with the string as response body, an ``200 OK`` +error code and a ``text/html`` mimetype. The logic that Flask applies to +converting return values into response objects is as follows: 1. If a response object of the correct type is returned it's directly returned from the view. -2. If it's a string, a response object is created with that data and - the default parameters. -3. If it's an iterator or generator returning strings or bytes, it is - treated as a streaming response. -4. If it's a dict or list, a response object is created using - :func:`~flask.json.jsonify`. -5. If a tuple is returned the items in the tuple can provide extra - information. Such tuples have to be in the form - ``(response, status)``, ``(response, headers)``, or - ``(response, status, headers)``. The ``status`` value will override - the status code and ``headers`` can be a list or dictionary of - additional header values. -6. If none of that works, Flask will assume the return value is a - valid WSGI application and convert that into a response object. +2. If it's a string, a response object is created with that data and the + default parameters. +3. If a tuple is returned the response object is created by passing the + tuple as arguments to the response object's constructor. +4. If neither of that works, Flask will assume the return value is a + valid WSGI application and converts that into a response object. If you want to get hold of the resulting response object inside the view you can use the :func:`~flask.make_response` function. -Imagine you have a view like this:: +Imagine you have a view like this: - from flask import render_template +.. sourcecode:: python @app.errorhandler(404) def not_found(error): return render_template('error.html'), 404 You just need to wrap the return expression with -:func:`~flask.make_response` and get the response object to modify it, then -return it:: +:func:`~flask.make_response` and get the result object to modify it, then +return it: - from flask import make_response +.. sourcecode:: python @app.errorhandler(404) def not_found(error): @@ -681,48 +706,13 @@ return it:: resp.headers['X-Something'] = 'A value' return resp - -APIs with JSON -`````````````` - -A common response format when writing an API is JSON. It's easy to get -started writing such an API with Flask. If you return a ``dict`` or -``list`` from a view, it will be converted to a JSON response. - -.. code-block:: python - - @app.route("/me") - def me_api(): - user = get_current_user() - return { - "username": user.username, - "theme": user.theme, - "image": url_for("user_image", filename=user.image), - } - - @app.route("/users") - def users_api(): - users = get_all_users() - return [user.to_json() for user in users] - -This is a shortcut to passing the data to the -:func:`~flask.json.jsonify` function, which will serialize any supported -JSON data type. That means that all the data in the dict or list must be -JSON serializable. - -For complex types such as database models, you'll want to use a -serialization library to convert the data to valid JSON types first. -There are many serialization libraries and Flask API extensions -maintained by the community that support more complex applications. - - .. _sessions: Sessions -------- -In addition to the request object there is also a second object called -:class:`~flask.session` which allows you to store information specific to a +Besides the request object there is also a second object called +:class:`~flask.session` that allows you to store information specific to a user from one request to the next. This is implemented on top of cookies for you and signs the cookies cryptographically. What this means is that the user could look at the contents of your cookie but not modify it, @@ -731,15 +721,14 @@ unless they know the secret key used for signing. In order to use sessions you have to set a secret key. Here is how sessions work:: - from flask import session + from flask import Flask, session, redirect, url_for, escape, request - # Set the secret key to some random bytes. Keep this really secret! - app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' + app = Flask(__name__) @app.route('/') def index(): if 'username' in session: - return f'Logged in as {session["username"]}' + return 'Logged in as %s' % escape(session['username']) return 'You are not logged in' @app.route('/login', methods=['GET', 'POST']) @@ -748,7 +737,7 @@ sessions work:: session['username'] = request.form['username'] return redirect(url_for('index')) return ''' - +

@@ -756,29 +745,28 @@ sessions work:: @app.route('/logout') def logout(): - # remove the username from the session if it's there + # remove the username from the session if its there session.pop('username', None) return redirect(url_for('index')) + # set the secret key. keep this really secret: + app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' + +The here mentioned :func:`~flask.escape` does escaping for you if you are +not using the template engine (like in this example). + .. admonition:: How to generate good secret keys - A secret key should be as random as possible. Your operating system has - ways to generate pretty random data based on a cryptographic random - generator. Use the following command to quickly generate a value for - :attr:`Flask.secret_key` (or :data:`SECRET_KEY`):: + The problem with random is that it's hard to judge what random is. And + a secret key should be as random as possible. Your operating system + has ways to generate pretty random stuff based on a cryptographic + random generator which can be used to get such a key: - $ python -c 'import secrets; print(secrets.token_hex())' - '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' + >>> import os + >>> os.urandom(24) + '\xfd{H\xe5<\x95\xf9\xe3\x96.5\xd1\x01O`_ for more +information. -See :doc:`errorhandling`. +Hooking in WSGI Middlewares +--------------------------- +If you want to add a WSGI middleware to your application you can wrap the +internal WSGI application. For example if you want to use one of the +middlewares from the Werkzeug package to work around bugs in lighttpd, you +can do it like this:: -Hooking in WSGI Middleware --------------------------- - -To add WSGI middleware to your Flask application, wrap the application's -``wsgi_app`` attribute. For example, to apply Werkzeug's -:class:`~werkzeug.middleware.proxy_fix.ProxyFix` middleware for running -behind Nginx: - -.. code-block:: python - - from werkzeug.middleware.proxy_fix import ProxyFix - app.wsgi_app = ProxyFix(app.wsgi_app) - -Wrapping ``app.wsgi_app`` instead of ``app`` means that ``app`` still -points at your Flask application, not at the middleware, so you can -continue to use and configure ``app`` directly. - -Using Flask Extensions ----------------------- - -Extensions are packages that help you accomplish common tasks. For -example, Flask-SQLAlchemy provides SQLAlchemy support that makes it simple -and easy to use with Flask. - -For more on Flask extensions, see :doc:`extensions`. - -Deploying to a Web Server -------------------------- - -Ready to deploy your new Flask app? See :doc:`deploying/index`. + from werkzeug.contrib.fixers import LighttpdCGIRootFix + app.wsgi_app = LighttpdCGIRootFix(app.wsgi_app) diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 6660671e..0249b88e 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -1,6 +1,239 @@ -:orphan: +.. _request-context: The Request Context =================== -Obsolete, see :doc:`/appcontext` instead. +This document describes the behavior in Flask 0.7 which is mostly in line +with the old behavior but has some small, subtle differences. + +One of the design ideas behind Flask is that there are two different +“states” in which code is executed. The application setup state in which +the application implicitly is on the module level. It starts when the +:class:`Flask` object is instantiated, and it implicitly ends when the +first request comes in. While the application is in this state a few +assumptions are true: + +- the programmer can modify the application object safely. +- no request handling happened so far +- you have to have a reference to the application object in order to + modify it, there is no magic proxy that can give you a reference to + the application object you're currently creating or modifying. + +On the contrast, during request handling, a couple of other rules exist: + +- while a request is active, the context local objects + (:data:`flask.request` and others) point to the current request. +- any code can get hold of these objects at any time. + +The magic that makes this works is internally referred in Flask as the +“request context”. + +Diving into Context Locals +-------------------------- + +Say you have a utility function that returns the URL the user should be +redirected to. Imagine it would always redirect to the URL's ``next`` +parameter or the HTTP referrer or the index page:: + + from flask import request, url_for + + def redirect_url(): + return request.args.get('next') or \ + request.referrer or \ + url_for('index') + +As you can see, it accesses the request object. If you try to run this +from a plain Python shell, this is the exception you will see: + +>>> redirect_url() +Traceback (most recent call last): + File "", line 1, in +AttributeError: 'NoneType' object has no attribute 'request' + +That makes a lot of sense because we currently do not have a request we +could access. So we have to make a request and bind it to the current +context. The :attr:`~flask.Flask.test_request_context` method can create +us a :class:`~flask.ctx.RequestContext`: + +>>> ctx = app.test_request_context('/?next=http://example.com/') + +This context can be used in two ways. Either with the `with` statement +or by calling the :meth:`~flask.ctx.RequestContext.push` and +:meth:`~flask.ctx.RequestContext.pop` methods: + +>>> ctx.push() + +From that point onwards you can work with the request object: + +>>> redirect_url() +u'http://example.com/' + +Until you call `pop`: + +>>> ctx.pop() + +Because the request context is internally maintained as a stack you can +push and pop multiple times. This is very handy to implement things like +internal redirects. + +For more information of how to utilize the request context from the +interactive Python shell, head over to the :ref:`shell` chapter. + +How the Context Works +--------------------- + +If you look into how the Flask WSGI application internally works, you will +find a piece of code that looks very much like this:: + + def wsgi_app(self, environ): + with self.request_context(environ): + try: + response = self.full_dispatch_request() + except Exception, e: + response = self.make_response(self.handle_exception(e)) + return response(environ, start_response) + +The method :meth:`~Flask.request_context` returns a new +:class:`~flask.ctx.RequestContext` object and uses it in combination with +the `with` statement to bind the context. Everything that is called from +the same thread from this point onwards until the end of the `with` +statement will have access to the request globals (:data:`flask.request` +and others). + +The request context internally works like a stack: The topmost level on +the stack is the current active request. +:meth:`~flask.ctx.RequestContext.push` adds the context to the stack on +the very top, :meth:`~flask.ctx.RequestContext.pop` removes it from the +stack again. On popping the application's +:func:`~flask.Flask.teardown_request` functions are also executed. + +.. _callbacks-and-errors: + +Callbacks and Errors +-------------------- + +What happens if an error occurs in Flask during request processing? This +particular behavior changed in 0.7 because we wanted to make it easier to +understand what is actually happening. The new behavior is quite simple: + +1. Before each request, :meth:`~flask.Flask.before_request` functions are + executed. If one of these functions return a response, the other + functions are no longer called. In any case however the return value + is treated as a replacement for the view's return value. + +2. If the :meth:`~flask.Flask.before_request` functions did not return a + response, the regular request handling kicks in and the view function + that was matched has the chance to return a response. + +3. The return value of the view is then converted into an actual response + object and handed over to the :meth:`~flask.Flask.after_request` + functions which have the chance to replace it or modify it in place. + +4. At the end of the request the :meth:`~flask.Flask.teardown_request` + functions are executed. This always happens, even in case of an + unhandled exception down the road or if a before-request handler was + not executed yet or at all (for example in test environments sometimes + you might want to not execute before-request callbacks). + +Now what happens on errors? In production mode if an exception is not +caught, the 500 internal server handler is called. In development mode +however the exception is not further processed and bubbles up to the WSGI +server. That way things like the interactive debugger can provide helpful +debug information. + +An important change in 0.7 is that the internal server error is now no +longer post processed by the after request callbacks and after request +callbacks are no longer guaranteed to be executed. This way the internal +dispatching code looks cleaner and is easier to customize and understand. + +The new teardown functions are supposed to be used as a replacement for +things that absolutely need to happen at the end of request. + +Teardown Callbacks +------------------ + +The teardown callbacks are special callbacks in that they are executed at +at different point. Strictly speaking they are independent of the actual +request handling as they are bound to the lifecycle of the +:class:`~flask.ctx.RequestContext` object. When the request context is +popped, the :meth:`~flask.Flask.teardown_request` functions are called. + +This is important to know if the life of the request context is prolonged +by using the test client in a with statement or when using the request +context from the command line:: + + with app.test_client() as client: + resp = client.get('/foo') + # the teardown functions are still not called at that point + # even though the response ended and you have the response + # object in your hand + + # only when the code reaches this point the teardown functions + # are called. Alternatively the same thing happens if another + # request was triggered from the test client + +It's easy to see the behavior from the command line: + +>>> app = Flask(__name__) +>>> @app.teardown_request +... def teardown_request(exception=None): +... print 'this runs after request' +... +>>> ctx = app.test_request_context() +>>> ctx.push() +>>> ctx.pop() +this runs after request +>>> + +Keep in mind that teardown callbacks are always executed, even if +before-request callbacks were not executed yet but an exception happened. +Certain parts of the test system might also temporarily create a request +context without calling the before-request handlers. Make sure to write +your teardown-request handlers in a way that they will never fail. + +.. _notes-on-proxies: + +Notes On Proxies +---------------- + +Some of the objects provided by Flask are proxies to other objects. The +reason behind this is that these proxies are shared between threads and +they have to dispatch to the actual object bound to a thread behind the +scenes as necessary. + +Most of the time you don't have to care about that, but there are some +exceptions where it is good to know that this object is an actual proxy: + +- The proxy objects do not fake their inherited types, so if you want to + perform actual instance checks, you have to do that on the instance + that is being proxied (see `_get_current_object` below). +- if the object reference is important (so for example for sending + :ref:`signals`) + +If you need to get access to the underlying object that is proxied, you +can use the :meth:`~werkzeug.local.LocalProxy._get_current_object` method:: + + app = current_app._get_current_object() + my_signal.send(app) + +Context Preservation on Error +----------------------------- + +If an error occurs or not, at the end of the request the request context +is popped and all data associated with it is destroyed. During +development however that can be problematic as you might want to have the +information around for a longer time in case an exception occurred. In +Flask 0.6 and earlier in debug mode, if an exception occurred, the +request context was not popped so that the interactive debugger can still +provide you with important information. + +Starting with Flask 0.7 you have finer control over that behavior by +setting the ``PRESERVE_CONTEXT_ON_EXCEPTION`` configuration variable. By +default it's linked to the setting of ``DEBUG``. If the application is in +debug mode the context is preserved, in production mode it's not. + +Do not force activate ``PRESERVE_CONTEXT_ON_EXCEPTION`` in production mode +as it will cause your application to leak memory on exceptions. However +it can be useful during development to get the same error preserving +behavior as in development mode when attempting to debug an error that +only occurs under production settings. diff --git a/docs/security.rst b/docs/security.rst new file mode 100644 index 00000000..909ef537 --- /dev/null +++ b/docs/security.rst @@ -0,0 +1,175 @@ +Security Considerations +======================= + +Web applications usually face all kinds of security problems and it's very +hard to get everything right. Flask tries to solve a few of these things +for you, but there are a couple more you have to take care of yourself. + +.. _xss: + +Cross-Site Scripting (XSS) +-------------------------- + +Cross site scripting is the concept of injecting arbitrary HTML (and with +it JavaScript) into the context of a website. To remedy this, developers +have to properly escape text so that it cannot include arbitrary HTML +tags. For more information on that have a look at the Wikipedia article +on `Cross-Site Scripting +`_. + +Flask configures Jinja2 to automatically escape all values unless +explicitly told otherwise. This should rule out all XSS problems caused +in templates, but there are still other places where you have to be +careful: + +- generating HTML without the help of Jinja2 +- calling :class:`~flask.Markup` on data submitted by users +- sending out HTML from uploaded files, never do that, use the + `Content-Disposition: attachment` header to prevent that problem. +- sending out textfiles from uploaded files. Some browsers are using + content-type guessing based on the first few bytes so users could + trick a browser to execute HTML. + +Another thing that is very important are unquoted attributes. While +Jinja2 can protect you from XSS issues by escaping HTML, there is one +thing it cannot protect you from: XSS by attribute injection. To counter +this possible attack vector, be sure to always quote your attributes with +either double or single quotes when using Jinja expressions in them: + +.. sourcecode:: html+jinja + + the text + +Why is this necessary? Because if you would not be doing that, an +attacker could easily inject custom JavaScript handlers. For example an +attacker could inject this piece of HTML+JavaScript: + +.. sourcecode:: html + + onmouseover=alert(document.cookie) + +When the user would then move with the mouse over the link, the cookie +would be presented to the user in an alert window. But instead of showing +the cookie to the user, a good attacker might also execute any other +JavaScript code. In combination with CSS injections the attacker might +even make the element fill out the entire page so that the user would +just have to have the mouse anywhere on the page to trigger the attack. + +Cross-Site Request Forgery (CSRF) +--------------------------------- + +Another big problem is CSRF. This is a very complex topic and I won't +outline it here in detail just mention what it is and how to theoretically +prevent it. + +If your authentication information is stored in cookies, you have implicit +state management. The state of "being logged in" is controlled by a +cookie, and that cookie is sent with each request to a page. +Unfortunately that includes requests triggered by 3rd party sites. If you +don't keep that in mind, some people might be able to trick your +application's users with social engineering to do stupid things without +them knowing. + +Say you have a specific URL that, when you sent `POST` requests to will +delete a user's profile (say `http://example.com/user/delete`). If an +attacker now creates a page that sends a post request to that page with +some JavaScript they just has to trick some users to load that page and +their profiles will end up being deleted. + +Imagine you were to run Facebook with millions of concurrent users and +someone would send out links to images of little kittens. When users +would go to that page, their profiles would get deleted while they are +looking at images of fluffy cats. + +How can you prevent that? Basically for each request that modifies +content on the server you would have to either use a one-time token and +store that in the cookie **and** also transmit it with the form data. +After receiving the data on the server again, you would then have to +compare the two tokens and ensure they are equal. + +Why does Flask not do that for you? The ideal place for this to happen is +the form validation framework, which does not exist in Flask. + +.. _json-security: + +JSON Security +------------- + +.. admonition:: ECMAScript 5 Changes + + Starting with ECMAScript 5 the behavior of literals changed. Now they + are not constructed with the constructor of ``Array`` and others, but + with the builtin constructor of ``Array`` which closes this particular + attack vector. + +JSON itself is a high-level serialization format, so there is barely +anything that could cause security problems, right? You can't declare +recursive structures that could cause problems and the only thing that +could possibly break are very large responses that can cause some kind of +denial of service at the receiver's side. + +However there is a catch. Due to how browsers work the CSRF issue comes +up with JSON unfortunately. Fortunately there is also a weird part of the +JavaScript specification that can be used to solve that problem easily and +Flask is kinda doing that for you by preventing you from doing dangerous +stuff. Unfortunately that protection is only there for +:func:`~flask.jsonify` so you are still at risk when using other ways to +generate JSON. + +So what is the issue and how to avoid it? The problem are arrays at +top-level in JSON. Imagine you send the following data out in a JSON +request. Say that's exporting the names and email addresses of all your +friends for a part of the user interface that is written in JavaScript. +Not very uncommon: + +.. sourcecode:: javascript + + [ + {"username": "admin", + "email": "admin@localhost"} + ] + +And it is doing that of course only as long as you are logged in and only +for you. And it is doing that for all `GET` requests to a certain URL, +say the URL for that request is +``http://example.com/api/get_friends.json``. + +So now what happens if a clever hacker is embedding this to his website +and social engineers a victim to visiting his site: + +.. sourcecode:: html + + + + + +If you know a bit of JavaScript internals you might know that it's +possible to patch constructors and register callbacks for setters. An +attacker can use this (like above) to get all the data you exported in +your JSON file. The browser will totally ignore the ``application/json`` +mimetype if ``text/javascript`` is defined as content type in the script +tag and evaluate that as JavaScript. Because top-level array elements are +allowed (albeit useless) and we hooked in our own constructor, after that +page loaded the data from the JSON response is in the `captured` array. + +Because it is a syntax error in JavaScript to have an object literal +(``{...}``) toplevel an attacker could not just do a request to an +external URL with the script tag to load up the data. So what Flask does +is to only allow objects as toplevel elements when using +:func:`~flask.jsonify`. Make sure to do the same when using an ordinary +JSON generate function. diff --git a/docs/server.rst b/docs/server.rst deleted file mode 100644 index d6beb1d8..00000000 --- a/docs/server.rst +++ /dev/null @@ -1,115 +0,0 @@ -.. currentmodule:: flask - -Development Server -================== - -Flask provides a ``run`` command to run the application with a development server. In -debug mode, this server provides an interactive debugger and will reload when code is -changed. - -.. warning:: - - Do not use the development server when deploying to production. It - is intended for use only during local development. It is not - designed to be particularly efficient, stable, or secure. - - See :doc:`/deploying/index` for deployment options. - -Command Line ------------- - -The ``flask run`` CLI command is the recommended way to run the development server. Use -the ``--app`` option to point to your application, and the ``--debug`` option to enable -debug mode. - -.. code-block:: text - - $ flask --app hello run --debug - -This enables debug mode, including the interactive debugger and reloader, and then -starts the server on http://localhost:5000/. Use ``flask run --help`` to see the -available options, and :doc:`/cli` for detailed instructions about configuring and using -the CLI. - - -.. _address-already-in-use: - -Address already in use -~~~~~~~~~~~~~~~~~~~~~~ - -If another program is already using port 5000, you'll see an ``OSError`` -when the server tries to start. It may have one of the following -messages: - -- ``OSError: [Errno 98] Address already in use`` -- ``OSError: [WinError 10013] An attempt was made to access a socket - in a way forbidden by its access permissions`` - -Either identify and stop the other program, or use -``flask run --port 5001`` to pick a different port. - -You can use ``netstat`` or ``lsof`` to identify what process id is using -a port, then use other operating system tools stop that process. The -following example shows that process id 6847 is using port 5000. - -.. tabs:: - - .. tab:: ``netstat`` (Linux) - - .. code-block:: text - - $ netstat -nlp | grep 5000 - tcp 0 0 127.0.0.1:5000 0.0.0.0:* LISTEN 6847/python - - .. tab:: ``lsof`` (macOS / Linux) - - .. code-block:: text - - $ lsof -P -i :5000 - Python 6847 IPv4 TCP localhost:5000 (LISTEN) - - .. tab:: ``netstat`` (Windows) - - .. code-block:: text - - > netstat -ano | findstr 5000 - TCP 127.0.0.1:5000 0.0.0.0:0 LISTENING 6847 - -macOS Monterey and later automatically starts a service that uses port -5000. You can choose to disable this service instead of using a different port by -searching for "AirPlay Receiver" in System Settings and toggling it off. - - -Deferred Errors on Reload -~~~~~~~~~~~~~~~~~~~~~~~~~ - -When using the ``flask run`` command with the reloader, the server will -continue to run even if you introduce syntax errors or other -initialization errors into the code. Accessing the site will show the -interactive debugger for the error, rather than crashing the server. - -If a syntax error is already present when calling ``flask run``, it will -fail immediately and show the traceback rather than waiting until the -site is accessed. This is intended to make errors more visible initially -while still allowing the server to handle errors on reload. - - -In Code -------- - -The development server can also be started from Python with the :meth:`Flask.run` -method. This method takes arguments similar to the CLI options to control the server. -The main difference from the CLI command is that the server will crash if there are -errors when reloading. ``debug=True`` can be passed to enable debug mode. - -Place the call in a main block, otherwise it will interfere when trying to import and -run the application with a production server later. - -.. code-block:: python - - if __name__ == "__main__": - app.run(debug=True) - -.. code-block:: text - - $ python hello.py diff --git a/docs/shell.rst b/docs/shell.rst index d8821e23..61b9dc05 100644 --- a/docs/shell.rst +++ b/docs/shell.rst @@ -1,37 +1,49 @@ +.. _shell: + Working with the Shell ====================== -One of the reasons everybody loves Python is the interactive shell. It allows -you to play around with code in real time and immediately get results back. -Flask provides the ``flask shell`` CLI command to start an interactive Python -shell with some setup done to make working with the Flask app easier. +.. versionadded:: 0.3 -.. code-block:: text +One of the reasons everybody loves Python is the interactive shell. It +basically allows you to execute Python commands in real time and +immediately get results back. Flask itself does not come with an +interactive shell, because it does not require any specific setup upfront, +just import your application and start playing around. - $ flask shell +There are however some handy helpers to make playing around in the shell a +more pleasant experience. The main issue with interactive console +sessions is that you're not triggering a request like a browser does which +means that :data:`~flask.g`, :data:`~flask.request` and others are not +available. But the code you want to test might depend on them, so what +can you do? + +This is where some helper functions come in handy. Keep in mind however +that these functions are not only there for interactive shell usage, but +also for unittesting and other situations that require a faked request +context. + +Generally it's recommended that you read the :ref:`request-context` +chapter of the documentation first. Creating a Request Context -------------------------- -``flask shell`` pushes an app context automatically, so :data:`.current_app` and -:data:`.g` are already available. However, there is no HTTP request being -handled in the shell, so :data:`.request` and :data:`.session` are not yet -available. - The easiest way to create a proper request context from the shell is by using the :attr:`~flask.Flask.test_request_context` method which creates us a :class:`~flask.ctx.RequestContext`: >>> ctx = app.test_request_context() -Normally you would use the ``with`` statement to make this context active, but -in the shell it's easier to call :meth:`~.RequestContext.push` and -:meth:`~.RequestContext.pop` manually: +Normally you would use the `with` statement to make this request object +active, but in the shell it's easier to use the +:meth:`~flask.ctx.RequestContext.push` and +:meth:`~flask.ctx.RequestContext.pop` methods by hand: >>> ctx.push() -From that point onwards you can work with the request object until you call -``pop``: +From that point onwards you can work with the request object until you +call `pop`: >>> ctx.pop() @@ -76,6 +88,6 @@ with stuff you want to star import into your interactive session. There you could also define some more helper methods for common things such as initializing the database, dropping tables etc. -Just put them into a module (like `shelltools`) and import from there: +Just put them into a module (like `shelltools` and import from there): >>> from shelltools import * diff --git a/docs/signals.rst b/docs/signals.rst index 7ca81a9d..0d1d9eea 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -1,28 +1,35 @@ +.. _signals: + Signals ======= -Signals are a lightweight way to notify subscribers of certain events during the -lifecycle of the application and each request. When an event occurs, it emits the -signal, which calls each subscriber. +.. versionadded:: 0.6 -Signals are implemented by the `Blinker`_ library. See its documentation for detailed -information. Flask provides some built-in signals. Extensions may provide their own. +Starting with Flask 0.6, there is integrated support for signalling in +Flask. This support is provided by the excellent `blinker`_ library and +will gracefully fall back if it is not available. -Many signals mirror Flask's decorator-based callbacks with similar names. For example, -the :data:`.request_started` signal is similar to the :meth:`~.Flask.before_request` -decorator. The advantage of signals over handlers is that they can be subscribed to -temporarily, and can't directly affect the application. This is useful for testing, -metrics, auditing, and more. For example, if you want to know what templates were -rendered at what parts of what requests, there is a signal that will notify you of that -information. +What are signals? Signals help you decouple applications by sending +notifications when actions occur elsewhere in the core framework or +another Flask extensions. In short, signals allow certain senders to +notify subscribers that something happened. +Flask comes with a couple of signals and other extensions might provide +more. Also keep in mind that signals are intended to notify subscribers +and should not encourage subscribers to modify data. You will notice that +there are signals that appear to do the same thing like some of the +builtin decorators do (eg: :data:`~flask.request_started` is very similar +to :meth:`~flask.Flask.before_request`). There are however difference in +how they work. The core :meth:`~flask.Flask.before_request` handler for +example is executed in a specific order and is able to abort the request +early by returning a response. In contrast all signal handlers are +executed in undefined order and do not modify any data. -Core Signals ------------- - -See :ref:`core-signals-list` for a list of all built-in signals. The :doc:`lifecycle` -page also describes the order that signals and decorators execute. - +The big advantage of signals over handlers is that you can safely +subscribe to them for the split of a second. These temporary +subscriptions are helpful for unittesting for example. Say you want to +know what templates were rendered as part of a request: signals allow you +to do exactly that. Subscribing to Signals ---------------------- @@ -35,11 +42,11 @@ signal, you can use the :meth:`~blinker.base.Signal.disconnect` method. For all core Flask signals, the sender is the application that issued the signal. When you subscribe to a signal, be sure to also provide a sender -unless you really want to listen for signals from all applications. This is +unless you really want to listen for signals of all applications. This is especially true if you are developing an extension. -For example, here is a helper context manager that can be used in a unit test -to determine which templates were rendered and what variables were passed +Here for example a helper context manager that can be used to figure out +in a unittest which templates were rendered and what variables were passed to the template:: from flask import template_rendered @@ -48,7 +55,7 @@ to the template:: @contextmanager def captured_templates(app): recorded = [] - def record(sender, template, context, **extra): + def record(sender, template, context): recorded.append((template, context)) template_rendered.connect(record, app) try: @@ -66,23 +73,20 @@ This can now easily be paired with a test client:: assert template.name == 'index.html' assert len(context['items']) == 10 -Make sure to subscribe with an extra ``**extra`` argument so that your -calls don't fail if Flask introduces new arguments to the signals. - All the template rendering in the code issued by the application `app` -in the body of the ``with`` block will now be recorded in the `templates` +in the body of the `with` block will now be recorded in the `templates` variable. Whenever a template is rendered, the template object as well as context are appended to it. Additionally there is a convenient helper method -(:meth:`~blinker.base.Signal.connected_to`) that allows you to -temporarily subscribe a function to a signal with a context manager on +(:meth:`~blinker.base.Signal.connected_to`). that allows you to +temporarily subscribe a function to a signal with is a context manager on its own. Because the return value of the context manager cannot be -specified that way, you have to pass the list in as an argument:: +specified that way one has to pass the list in as argument:: from flask import template_rendered - def captured_templates(app, recorded, **extra): + def captured_templates(app, recorded): def record(sender, template, context): recorded.append((template, context)) return template_rendered.connected_to(record, app) @@ -90,16 +94,21 @@ specified that way, you have to pass the list in as an argument:: The example above would then look like this:: templates = [] - with captured_templates(app, templates, **extra): + with captured_templates(app, templates): ... template, context = templates[0] +.. admonition:: Blinker API Changes + + The :meth:`~blinker.base.Signal.connected_to` method arrived in Blinker + with version 1.1. + Creating Signals ---------------- If you want to use signals in your own application, you can use the blinker library directly. The most common use case are named signals in a -custom :class:`~blinker.base.Namespace`. This is what is recommended +custom :class:`~blinker.base.Namespace`.. This is what is recommended most of the time:: from blinker import Namespace @@ -113,7 +122,11 @@ The name for the signal here makes it unique and also simplifies debugging. You can access the name of the signal with the :attr:`~blinker.base.NamedSignal.name` attribute. -.. _signals-sending: +.. admonition:: For Extension Developers + + If you are writing a Flask extension and you want to gracefully degrade for + missing blinker installations, you can do so by using the + :class:`flask.signals.Namespace` class. Sending Signals --------------- @@ -130,7 +143,7 @@ signal subscribers:: model_saved.send(self) Try to always pick a good sender. If you have a class that is emitting a -signal, pass ``self`` as sender. If you are emitting a signal from a random +signal, pass `self` as sender. If you emitting a signal from a random function, you can pass ``current_app._get_current_object()`` as sender. .. admonition:: Passing Proxies as Senders @@ -140,27 +153,103 @@ function, you can pass ``current_app._get_current_object()`` as sender. that :data:`~flask.current_app` is a proxy and not the real application object. - -Signals and Flask's Request Context ------------------------------------ - -Context-local proxies are available between :data:`~flask.request_started` and -:data:`~flask.request_finished`, so you can rely on :class:`flask.g` and others -as needed. Note the limitations described in :ref:`signals-sending` and the -:data:`~flask.request_tearing_down` signal. - - Decorator Based Signal Subscriptions ------------------------------------ -You can also easily subscribe to signals by using the +With Blinker 1.1 you can also easily subscribe to signals by using the new :meth:`~blinker.base.NamedSignal.connect_via` decorator:: from flask import template_rendered @template_rendered.connect_via(app) - def when_template_rendered(sender, template, context, **extra): - print(f'Template {template.name} is rendered with {context}') + def when_template_rendered(sender, template, context): + print 'Template %s is rendered with %s' % (template.name, context) +Core Signals +------------ -.. _blinker: https://pypi.org/project/blinker/ +.. when modifying this list, also update the one in api.rst + +The following signals exist in Flask: + +.. data:: flask.template_rendered + :noindex: + + This signal is sent when a template was successfully rendered. The + signal is invoked with the instance of the template as `template` + and the context as dictionary (named `context`). + + Example subscriber:: + + def log_template_renders(sender, template, context): + sender.logger.debug('Rendering template "%s" with context %s', + template.name or 'string template', + context) + + from flask import template_rendered + template_rendered.connect(log_template_renders, app) + +.. data:: flask.request_started + :noindex: + + This signal is sent before any request processing started but when the + request context was set up. Because the request context is already + bound, the subscriber can access the request with the standard global + proxies such as :class:`~flask.request`. + + Example subscriber:: + + def log_request(sender): + sender.logger.debug('Request context is set up') + + from flask import request_started + request_started.connect(log_request, app) + +.. data:: flask.request_finished + :noindex: + + This signal is sent right before the response is sent to the client. + It is passed the response to be sent named `response`. + + Example subscriber:: + + def log_response(sender, response): + sender.logger.debug('Request context is about to close down. ' + 'Response: %s', response) + + from flask import request_finished + request_finished.connect(log_response, app) + +.. data:: flask.got_request_exception + :noindex: + + This signal is sent when an exception happens during request processing. + It is sent *before* the standard exception handling kicks in and even + in debug mode, where no exception handling happens. The exception + itself is passed to the subscriber as `exception`. + + Example subscriber:: + + def log_exception(sender, exception): + sender.logger.debug('Got exception during processing: %s', exception) + + from flask import got_request_exception + got_request_exception.connect(log_exception, app) + +.. data:: flask.request_tearing_down + :noindex: + + This signal is sent when the request is tearing down. This is always + called, even if an exception is caused. Currently functions listening + to this signal are called after the regular teardown handlers, but this + is not something you can rely on. + + Example subscriber:: + + def close_db_connection(sender): + session.close() + + from flask import request_tearing_down + request_tearing_down.connect(close_db_connection, app) + +.. _blinker: http://pypi.python.org/pypi/blinker diff --git a/docs/styleguide.rst b/docs/styleguide.rst new file mode 100644 index 00000000..d46ecd04 --- /dev/null +++ b/docs/styleguide.rst @@ -0,0 +1,200 @@ +Pocoo Styleguide +================ + +The Pocoo styleguide is the styleguide for all Pocoo Projects, including +Flask. This styleguide is a requirement for Patches to Flask and a +recommendation for Flask extensions. + +In general the Pocoo Styleguide closely follows :pep:`8` with some small +differences and extensions. + +General Layout +-------------- + +Indentation: + 4 real spaces. No tabs, no exceptions. + +Maximum line length: + 79 characters with a soft limit for 84 if absolutely necessary. Try + to avoid too nested code by cleverly placing `break`, `continue` and + `return` statements. + +Continuing long statements: + To continue a statement you can use backslashes in which case you should + align the next line with the last dot or equal sign, or indent four + spaces:: + + this_is_a_very_long(function_call, 'with many parameters') \ + .that_returns_an_object_with_an_attribute + + MyModel.query.filter(MyModel.scalar > 120) \ + .order_by(MyModel.name.desc()) \ + .limit(10) + + If you break in a statement with parentheses or braces, align to the + braces:: + + this_is_a_very_long(function_call, 'with many parameters', + 23, 42, 'and even more') + + For lists or tuples with many items, break immediately after the + opening brace:: + + items = [ + 'this is the first', 'set of items', 'with more items', + 'to come in this line', 'like this' + ] + +Blank lines: + Top level functions and classes are separated by two lines, everything + else by one. Do not use too many blank lines to separate logical + segments in code. Example:: + + def hello(name): + print 'Hello %s!' % name + + + def goodbye(name): + print 'See you %s.' % name + + + class MyClass(object): + """This is a simple docstring""" + + def __init__(self, name): + self.name = name + + def get_annoying_name(self): + return self.name.upper() + '!!!!111' + +Expressions and Statements +-------------------------- + +General whitespace rules: + - No whitespace for unary operators that are not words + (e.g.: ``-``, ``~`` etc.) as well on the inner side of parentheses. + - Whitespace is placed between binary operators. + + Good:: + + exp = -1.05 + value = (item_value / item_count) * offset / exp + value = my_list[index] + value = my_dict['key'] + + Bad:: + + exp = - 1.05 + value = ( item_value / item_count ) * offset / exp + value = (item_value/item_count)*offset/exp + value=( item_value/item_count ) * offset/exp + value = my_list[ index ] + value = my_dict ['key'] + +Yoda statements are a no-go: + Never compare constant with variable, always variable with constant: + + Good:: + + if method == 'md5': + pass + + Bad:: + + if 'md5' == method: + pass + +Comparisons: + - against arbitrary types: ``==`` and ``!=`` + - against singletons with ``is`` and ``is not`` (eg: ``foo is not + None``) + - never compare something with `True` or `False` (for example never + do ``foo == False``, do ``not foo`` instead) + +Negated containment checks: + use ``foo not in bar`` instead of ``not foo in bar`` + +Instance checks: + ``isinstance(a, C)`` instead of ``type(A) is C``, but try to avoid + instance checks in general. Check for features. + + +Naming Conventions +------------------ + +- Class names: ``CamelCase``, with acronyms kept uppercase (``HTTPWriter`` + and not ``HttpWriter``) +- Variable names: ``lowercase_with_underscores`` +- Method and function names: ``lowercase_with_underscores`` +- Constants: ``UPPERCASE_WITH_UNDERSCORES`` +- precompiled regular expressions: ``name_re`` + +Protected members are prefixed with a single underscore. Double +underscores are reserved for mixin classes. + +On classes with keywords, trailing underscores are appended. Clashes with +builtins are allowed and **must not** be resolved by appending an +underline to the variable name. If the function needs to access a +shadowed builtin, rebind the builtin to a different name instead. + +Function and method arguments: + - class methods: ``cls`` as first parameter + - instance methods: ``self`` as first parameter + - lambdas for properties might have the first parameter replaced + with ``x`` like in ``display_name = property(lambda x: x.real_name + or x.username)`` + + +Docstrings +---------- + +Docstring conventions: + All docstrings are formatted with reStructuredText as understood by + Sphinx. Depending on the number of lines in the docstring, they are + laid out differently. If it's just one line, the closing triple + quote is on the same line as the opening, otherwise the text is on + the same line as the opening quote and the triple quote that closes + the string on its own line:: + + def foo(): + """This is a simple docstring""" + + + def bar(): + """This is a longer docstring with so much information in there + that it spans three lines. In this case the closing triple quote + is on its own line. + """ + +Module header: + The module header consists of an utf-8 encoding declaration (if non + ASCII letters are used, but it is recommended all the time) and a + standard docstring:: + + # -*- coding: utf-8 -*- + """ + package.module + ~~~~~~~~~~~~~~ + + A brief description goes here. + + :copyright: (c) YEAR by AUTHOR. + :license: LICENSE_NAME, see LICENSE_FILE for more details. + """ + + Please keep in mind that proper copyrights and license files are a + requirement for approved Flask extensions. + + +Comments +-------- + +Rules for comments are similar to docstrings. Both are formatted with +reStructuredText. If a comment is used to document an attribute, put a +colon after the opening pound sign (``#``):: + + class User(object): + #: the name of the user as unicode string + name = Column(String) + #: the sha1 hash of the password + inline salt + pw_hash = Column(String) diff --git a/docs/templating.rst b/docs/templating.rst index ed4a52ee..bd940b0e 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -1,69 +1,57 @@ Templates ========= -Flask leverages Jinja as its template engine. You are obviously free to use -a different template engine, but you still have to install Jinja to run +Flask leverages Jinja2 as template engine. You are obviously free to use +a different template engine, but you still have to install Jinja2 to run Flask itself. This requirement is necessary to enable rich extensions. -An extension can depend on Jinja being present. +An extension can depend on Jinja2 being present. -This section only gives a very quick introduction into how Jinja +This section only gives a very quick introduction into how Jinja2 is integrated into Flask. If you want information on the template -engine's syntax itself, head over to the official `Jinja Template -Documentation `_ for +engine's syntax itself, head over to the official `Jinja2 Template +Documentation `_ for more information. Jinja Setup ----------- -Unless customized, Jinja is configured by Flask as follows: +Unless customized, Jinja2 is configured by Flask as follows: - autoescaping is enabled for all templates ending in ``.html``, - ``.htm``, ``.xml``, ``.xhtml``, as well as ``.svg`` when using - :func:`~flask.templating.render_template`. -- autoescaping is enabled for all strings when using - :func:`~flask.templating.render_template_string`. + ``.htm``, ``.xml`` as well as ``.xhtml`` - a template has the ability to opt in/out autoescaping with the ``{% autoescape %}`` tag. - Flask inserts a couple of global functions and helpers into the - Jinja context, additionally to the values that are present by + Jinja2 context, additionally to the values that are present by default. Standard Context ---------------- -The following global variables are available within Jinja templates +The following global variables are available within Jinja2 templates by default: .. data:: config :noindex: - The current configuration object (:data:`flask.Flask.config`) + The current configuration object (:data:`flask.config`) .. versionadded:: 0.6 - .. versionchanged:: 0.10 - This is now always available, even in imported templates. - .. data:: request :noindex: - The current request object (:class:`flask.request`). This variable is - unavailable if the template was rendered without an active request - context. + The current request object (:class:`flask.request`) .. data:: session :noindex: - The current session object (:class:`flask.session`). This variable - is unavailable if the template was rendered without an active request - context. + The current session object (:class:`flask.session`) .. data:: g :noindex: - The request-bound object for global variables (:data:`flask.g`). This - variable is unavailable if the template was rendered without an active - request context. + The request-bound object for global variables (:data:`flask.g`) .. function:: url_for :noindex: @@ -75,7 +63,7 @@ by default: The :func:`flask.get_flashed_messages` function. -.. admonition:: The Jinja Context Behavior +.. admonition:: The Jinja Context Behaviour These variables are added to the context of variables, they are not global variables. The difference is that by default these will not @@ -95,27 +83,51 @@ by default: {% from '_helpers.html' import my_macro with context %} +Standard Filters +---------------- + +These filters are available in Jinja2 additionally to the filters provided +by Jinja2 itself: + +.. function:: tojson + :noindex: + + This function converts the given object into JSON representation. This + is for example very helpful if you try to generate JavaScript on the + fly. + + Note that inside `script` tags no escaping must take place, so make + sure to disable escaping with ``|safe`` if you intend to use it inside + `script` tags: + + .. sourcecode:: html+jinja + + + + That the ``|tojson`` filter escapes forward slashes properly for you. Controlling Autoescaping ------------------------ Autoescaping is the concept of automatically escaping special characters -for you. Special characters in the sense of HTML (or XML, and thus XHTML) +of you. Special characters in the sense of HTML (or XML, and thus XHTML) are ``&``, ``>``, ``<``, ``"`` as well as ``'``. Because these characters carry specific meanings in documents on their own you have to replace them by so called "entities" if you want to use them for text. Not doing so would not only cause user frustration by the inability to use these characters in text, but can also lead to security problems. (see -:ref:`security-xss`) +:ref:`xss`) Sometimes however you will need to disable autoescaping in templates. This can be the case if you want to explicitly inject HTML into pages, for -example if they come from a system that generates secure HTML like a +example if they come from a system that generate secure HTML like a markdown to HTML converter. There are three ways to accomplish that: -- In the Python code, wrap the HTML string in a :class:`~markupsafe.Markup` +- In the Python code, wrap the HTML string in a :class:`~flask.Markup` object before passing it to the template. This is in general the recommended way. - Inside the template, use the ``|safe`` filter to explicitly mark a @@ -135,70 +147,36 @@ autoescape %}`` block: Whenever you do this, please be very cautious about the variables you are using in this block. -.. _registering-filters: +Registering Filters +------------------- -Registering Filters, Tests, and Globals ---------------------------------------- +If you want to register your own filters in Jinja2 you have two ways to do +that. You can either put them by hand into the +:attr:`~flask.Flask.jinja_env` of the application or use the +:meth:`~flask.Flask.template_filter` decorator. -The Flask app and blueprints provide decorators and methods to register your own -filters, tests, and global functions for use in Jinja templates. They all follow -the same pattern, so the following examples only discuss filters. +The two following examples work the same and both reverse an object:: -Decorate a function with :meth:`~.Flask.template_filter` to register it as a -template filter. - -.. code-block:: python - - @app.template_filter - def reverse(s): - return reversed(s) - -.. code-block:: jinja - - {% for item in data | reverse %} - {% endfor %} - -By default it will use the name of the function as the name of the filter, but -that can be changed by passing a name to the decorator. - -.. code-block:: python - - @app.template_filter("reverse") + @app.template_filter('reverse') def reverse_filter(s): - return reversed(s) - -A filter can be registered separately using :meth:`~.Flask.add_template_filter`. -The name is optional and will use the function name if not given. - -.. code-block:: python + return s[::-1] def reverse_filter(s): - return reversed(s) - - app.add_template_filter(reverse_filter, "reverse") - -For template tests, use the :meth:`~.Flask.template_test` decorator or -:meth:`~.Flask.add_template_test` method. For template global functions, use the -:meth:`~.Flask.template_global` decorator or :meth:`~.Flask.add_template_global` -method. - -The same methods also exist on :class:`.Blueprint`, prefixed with ``app_`` to -indicate that the registered functions will be available to all templates, not -only when rendering from within the blueprint. - -The Jinja environment is also available as :attr:`~.Flask.jinja_env`. It may be -modified directly, as you would when using Jinja outside Flask. + return s[::-1] + app.jinja_env.filters['reverse'] = reverse_filter +In case of the decorator the argument is optional if you want to use the +function name as name of the filter. Context Processors ------------------ -To inject new variables automatically into the context of a template, +To inject new variables automatically into the context of a template context processors exist in Flask. Context processors run before the template is rendered and have the ability to inject new values into the template context. A context processor is a function that returns a dictionary. The keys and values of this dictionary are then merged with -the template context, for all templates in the app:: +the template context:: @app.context_processor def inject_user(): @@ -208,54 +186,3 @@ The context processor above makes a variable called `user` available in the template with the value of `g.user`. This example is not very interesting because `g` is available in templates anyways, but it gives an idea how this works. - -Variables are not limited to values; a context processor can also make -functions available to templates (since Python allows passing around -functions):: - - @app.context_processor - def utility_processor(): - def format_price(amount, currency="€"): - return f"{amount:.2f}{currency}" - return dict(format_price=format_price) - -The context processor above makes the `format_price` function available to all -templates:: - - {{ format_price(0.33) }} - -You could also build `format_price` as a template filter (see -:ref:`registering-filters`), but this demonstrates how to pass functions in a -context processor. - -Streaming ---------- - -It can be useful to not render the whole template as one complete -string, instead render it as a stream, yielding smaller incremental -strings. This can be used for streaming HTML in chunks to speed up -initial page load, or to save memory when rendering a very large -template. - -The Jinja template engine supports rendering a template piece -by piece, returning an iterator of strings. Flask provides the -:func:`~flask.stream_template` and :func:`~flask.stream_template_string` -functions to make this easier to use. - -.. code-block:: python - - from flask import stream_template - - @app.get("/timeline") - def timeline(): - return stream_template("timeline.html") - -These functions automatically apply the -:func:`~flask.stream_with_context` wrapper if a request is active, so that -:data:`.request`, :data:`.session`, and :data:`.g` remain available in the -template. - -More headers cannot be sent after the body has begun. Therefore, you must -make sure all headers are set before starting the response. In particular, -if the template will access ``session``, be sure to do so in the view as -well so that the ``Vary: cookie`` header will be set. diff --git a/docs/testing.rst b/docs/testing.rst index c171abd6..1e00fe80 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -1,318 +1,307 @@ +.. _testing: + Testing Flask Applications ========================== -Flask provides utilities for testing an application. This documentation -goes over techniques for working with different parts of the application -in tests. + **Something that is untested is broken.** -We will use the `pytest`_ framework to set up and run our tests. +The origin of this quote is unknown and while it is not entirely correct, it is also +not far from the truth. Untested applications make it hard to +improve existing code and developers of untested applications tend to +become pretty paranoid. If an application has automated tests, you can +safely make changes and instantly know if anything breaks. -.. code-block:: text +Flask provides a way to test your application by exposing the Werkzeug +test :class:`~werkzeug.test.Client` and handling the context locals for you. +You can then use that with your favourite testing solution. In this documentation +we will use the :mod:`unittest` package that comes pre-installed with Python. - $ pip install pytest +The Application +--------------- -.. _pytest: https://docs.pytest.org/ +First, we need an application to test; we will use the application from +the :ref:`tutorial`. If you don't have that application yet, get the +sources from `the examples`_. -The :doc:`tutorial ` goes over how to write tests for -100% coverage of the sample Flaskr blog application. See -:doc:`the tutorial on tests ` for a detailed -explanation of specific tests for an application. +.. _the examples: + http://github.com/mitsuhiko/flask/tree/master/examples/flaskr/ +The Testing Skeleton +-------------------- -Identifying Tests ------------------ +In order to test the application, we add a second module +(`flaskr_tests.py`) and create a unittest skeleton there:: -Tests are typically located in the ``tests`` folder. Tests are functions -that start with ``test_``, in Python modules that start with ``test_``. -Tests can also be further grouped in classes that start with ``Test``. + import os + import flaskr + import unittest + import tempfile -It can be difficult to know what to test. Generally, try to test the -code that you write, not the code of libraries that you use, since they -are already tested. Try to extract complex behaviors as separate -functions to test individually. + class FlaskrTestCase(unittest.TestCase): + def setUp(self): + self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() + flaskr.app.config['TESTING'] = True + self.app = flaskr.app.test_client() + flaskr.init_db() -Fixtures --------- + def tearDown(self): + os.close(self.db_fd) + os.unlink(flaskr.app.config['DATABASE']) -Pytest *fixtures* allow writing pieces of code that are reusable across -tests. A simple fixture returns a value, but a fixture can also do -setup, yield a value, then do teardown. Fixtures for the application, -test client, and CLI runner are shown below, they can be placed in -``tests/conftest.py``. + if __name__ == '__main__': + unittest.main() -If you're using an -:doc:`application factory `, define an ``app`` -fixture to create and configure an app instance. You can add code before -and after the ``yield`` to set up and tear down other resources, such as -creating and clearing a database. +The code in the :meth:`~unittest.TestCase.setUp` method creates a new test +client and initializes a new database. This function is called before +each individual test function is run. To delete the database after the +test, we close the file and remove it from the filesystem in the +:meth:`~unittest.TestCase.tearDown` method. Additionally during setup the +``TESTING`` config flag is activated. What it does is disabling the error +catching during request handling so that you get better error reports when +performing test requests against the application. -If you're not using a factory, you already have an app object you can -import and configure directly. You can still use an ``app`` fixture to -set up and tear down resources. +This test client will give us a simple interface to the application. We can +trigger test requests to the application, and the client will also keep track +of cookies for us. -.. code-block:: python +Because SQLite3 is filesystem-based we can easily use the tempfile module +to create a temporary database and initialize it. The +:func:`~tempfile.mkstemp` function does two things for us: it returns a +low-level file handle and a random file name, the latter we use as +database name. We just have to keep the `db_fd` around so that we can use +the :func:`os.close` function to close the file. + +If we now run the test suite, we should see the following output:: + + $ python flaskr_tests.py + + ---------------------------------------------------------------------- + Ran 0 tests in 0.000s + + OK - import pytest - from my_project import create_app +Even though it did not run any actual tests, we already know that our flaskr +application is syntactically valid, otherwise the import would have died +with an exception. - @pytest.fixture() - def app(): - app = create_app() - app.config.update({ - "TESTING": True, - }) +The First Test +-------------- - # other setup can go here +Now it's time to start testing the functionality of the application. +Let's check that the application shows "No entries here so far" if we +access the root of the application (``/``). To do this, we add a new +test method to our class, like this:: - yield app + class FlaskrTestCase(unittest.TestCase): - # clean up / reset resources here + def setUp(self): + self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() + self.app = flaskr.app.test_client() + flaskr.init_db() + def tearDown(self): + os.close(self.db_fd) + os.unlink(flaskr.DATABASE) - @pytest.fixture() - def client(app): - return app.test_client() + def test_empty_db(self): + rv = self.app.get('/') + assert 'No entries here so far' in rv.data +Notice that our test functions begin with the word `test`; this allows +:mod:`unittest` to automatically identify the method as a test to run. - @pytest.fixture() - def runner(app): - return app.test_cli_runner() +By using `self.app.get` we can send an HTTP `GET` request to the application with +the given path. The return value will be a :class:`~flask.Flask.response_class` object. +We can now use the :attr:`~werkzeug.wrappers.BaseResponse.data` attribute to inspect +the return value (as string) from the application. In this case, we ensure that +``'No entries here so far'`` is part of the output. +Run it again and you should see one passing test:: -Sending Requests with the Test Client -------------------------------------- + $ python flaskr_tests.py + . + ---------------------------------------------------------------------- + Ran 1 test in 0.034s -The test client makes requests to the application without running a live -server. Flask's client extends -:doc:`Werkzeug's client `, see those docs for additional -information. + OK -The ``client`` has methods that match the common HTTP request methods, -such as ``client.get()`` and ``client.post()``. They take many arguments -for building the request; you can find the full documentation in -:class:`~werkzeug.test.EnvironBuilder`. Typically you'll use ``path``, -``query_string``, ``headers``, and ``data`` or ``json``. +Logging In and Out +------------------ -To make a request, call the method the request should use with the path -to the route to test. A :class:`~werkzeug.test.TestResponse` is returned -to examine the response data. It has all the usual properties of a -response object. You'll usually look at ``response.data``, which is the -bytes returned by the view. If you want to use text, Werkzeug 2.1 -provides ``response.text``, or use ``response.get_data(as_text=True)``. +The majority of the functionality of our application is only available for +the administrative user, so we need a way to log our test client in and out +of the application. To do this, we fire some requests to the login and logout +pages with the required form data (username and password). And because the +login and logout pages redirect, we tell the client to `follow_redirects`. -.. code-block:: python +Add the following two methods to your `FlaskrTestCase` class:: - def test_request_example(client): - response = client.get("/posts") - assert b"

Hello, World!

" in response.data + def login(self, username, password): + return self.app.post('/login', data=dict( + username=username, + password=password + ), follow_redirects=True) + def logout(self): + return self.app.get('/logout', follow_redirects=True) -Pass a dict ``query_string={"key": "value", ...}`` to set arguments in -the query string (after the ``?`` in the URL). Pass a dict -``headers={}`` to set request headers. +Now we can easily test that logging in and out works and that it fails with +invalid credentials. Add this new test to the class:: -To send a request body in a POST or PUT request, pass a value to -``data``. If raw bytes are passed, that exact body is used. Usually, -you'll pass a dict to set form data. + def test_login_logout(self): + rv = self.login('admin', 'default') + assert 'You were logged in' in rv.data + rv = self.logout() + assert 'You were logged out' in rv.data + rv = self.login('adminx', 'default') + assert 'Invalid username' in rv.data + rv = self.login('admin', 'defaultx') + assert 'Invalid password' in rv.data +Test Adding Messages +-------------------- -Form Data -~~~~~~~~~ +We should also test that adding messages works. Add a new test method +like this:: -To send form data, pass a dict to ``data``. The ``Content-Type`` header -will be set to ``multipart/form-data`` or -``application/x-www-form-urlencoded`` automatically. + def test_messages(self): + self.login('admin', 'default') + rv = self.app.post('/add', data=dict( + title='', + text='HTML allowed here' + ), follow_redirects=True) + assert 'No entries here so far' not in rv.data + assert '<Hello>' in rv.data + assert 'HTML allowed here' in rv.data -If a value is a file object opened for reading bytes (``"rb"`` mode), it -will be treated as an uploaded file. To change the detected filename and -content type, pass a ``(file, filename, content_type)`` tuple. File -objects will be closed after making the request, so they do not need to -use the usual ``with open() as f:`` pattern. +Here we check that HTML is allowed in the text but not in the title, +which is the intended behavior. -It can be useful to store files in a ``tests/resources`` folder, then -use ``pathlib.Path`` to get files relative to the current test file. +Running that should now give us three passing tests:: -.. code-block:: python + $ python flaskr_tests.py + ... + ---------------------------------------------------------------------- + Ran 3 tests in 0.332s - from pathlib import Path + OK - # get the resources folder in the tests folder - resources = Path(__file__).parent / "resources" +For more complex tests with headers and status codes, check out the +`MiniTwit Example`_ from the sources which contains a larger test +suite. - def test_edit_user(client): - response = client.post("/user/2/edit", data={ - "name": "Flask", - "theme": "dark", - "picture": (resources / "picture.png").open("rb"), - }) - assert response.status_code == 200 +.. _MiniTwit Example: + http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/ -JSON Data -~~~~~~~~~ -To send JSON data, pass an object to ``json``. The ``Content-Type`` -header will be set to ``application/json`` automatically. +Other Testing Tricks +-------------------- -Similarly, if the response contains JSON data, the ``response.json`` -attribute will contain the deserialized object. +Besides using the test client as shown above, there is also the +:meth:`~flask.Flask.test_request_context` method that can be used +in combination with the `with` statement to activate a request context +temporarily. With this you can access the :class:`~flask.request`, +:class:`~flask.g` and :class:`~flask.session` objects like in view +functions. Here is a full example that demonstrates this approach:: -.. code-block:: python + app = flask.Flask(__name__) - def test_json_data(client): - response = client.post("/graphql", json={ - "query": """ - query User($id: String!) { - user(id: $id) { - name - theme - picture_url - } - } - """, - variables={"id": 2}, - }) - assert response.json["data"]["user"]["name"] == "Flask" + with app.test_request_context('/?name=Peter'): + assert flask.request.path == '/' + assert flask.request.args['name'] == 'Peter' +All the other objects that are context bound can be used in the same +way. -Following Redirects -------------------- +If you want to test your application with different configurations and +there does not seem to be a good way to do that, consider switching to +application factories (see :ref:`app-factories`). -By default, the client does not make additional requests if the response -is a redirect. By passing ``follow_redirects=True`` to a request method, -the client will continue to make requests until a non-redirect response -is returned. +Note however that if you are using a test request context, the +:meth:`~flask.Flask.before_request` functions are not automatically called +same for :meth:`~flask.Flask.after_request` functions. However +:meth:`~flask.Flask.teardown_request` functions are indeed executed when +the test request context leaves the `with` block. If you do want the +:meth:`~flask.Flask.before_request` functions to be called as well, you +need to call :meth:`~flask.Flask.preprocess_request` yourself:: -:attr:`TestResponse.history ` is -a tuple of the responses that led up to the final response. Each -response has a :attr:`~werkzeug.test.TestResponse.request` attribute -which records the request that produced that response. + app = flask.Flask(__name__) -.. code-block:: python + with app.test_request_context('/?name=Peter'): + app.preprocess_request() + ... - def test_logout_redirect(client): - response = client.get("/logout", follow_redirects=True) - # Check that there was one redirect response. - assert len(response.history) == 1 - # Check that the second request was to the index page. - assert response.request.path == "/index" +This can be necessary to open database connections or something similar +depending on how your application was designed. +If you want to call the :meth:`~flask.Flask.after_request` functions you +need to call into :meth:`~flask.Flask.process_response` which however +requires that you pass it a response object:: -Accessing and Modifying the Session ------------------------------------ + app = flask.Flask(__name__) -To access Flask's context variables, mainly -:data:`~flask.session`, use the client in a ``with`` statement. -The app and request context will remain active *after* making a request, -until the ``with`` block ends. + with app.test_request_context('/?name=Peter'): + resp = Response('...') + resp = app.process_response(resp) + ... -.. code-block:: python +This in general is less useful because at that point you can directly +start using the test client. - from flask import session - def test_access_session(client): - with client: - client.post("/auth/login", data={"username": "flask"}) - # session is still accessible - assert session["user_id"] == 1 +Keeping the Context Around +-------------------------- - # session is no longer accessible +.. versionadded:: 0.4 -If you want to access or set a value in the session *before* making a -request, use the client's -:meth:`~flask.testing.FlaskClient.session_transaction` method in a -``with`` statement. It returns a session object, and will save the -session once the block ends. +Sometimes it is helpful to trigger a regular request but still keep the +context around for a little longer so that additional introspection can +happen. With Flask 0.4 this is possible by using the +:meth:`~flask.Flask.test_client` with a `with` block:: -.. code-block:: python + app = flask.Flask(__name__) - from flask import session + with app.test_client() as c: + rv = c.get('/?tequila=42') + assert request.args['tequila'] == '42' - def test_modify_session(client): - with client.session_transaction() as session: - # set a user id without going through the login route - session["user_id"] = 1 +If you were to use just the :meth:`~flask.Flask.test_client` without +the `with` block, the `assert` would fail with an error because `request` +is no longer available (because you are trying to use it outside of the actual request). +However, keep in mind that any :meth:`~flask.Flask.after_request` functions +are already called at this point so your database connection and +everything involved is probably already closed down. - # session is saved now - response = client.get("/users/me") - assert response.json["username"] == "flask" +Accessing and Modifying Sessions +-------------------------------- +.. versionadded:: 0.8 -.. _testing-cli: +Sometimes it can be very helpful to access or modify the sessions from the +test client. Generally there are two ways for this. If you just want to +ensure that a session has certain keys set to certain values you can just +keep the context around and access :data:`flask.session`:: -Running Commands with the CLI Runner ------------------------------------- + with app.test_client() as c: + rv = c.get('/') + assert flask.session['foo'] == 42 -Flask provides :meth:`~flask.Flask.test_cli_runner` to create a -:class:`~flask.testing.FlaskCliRunner`, which runs CLI commands in -isolation and captures the output in a :class:`~click.testing.Result` -object. Flask's runner extends :doc:`Click's runner `, -see those docs for additional information. +This however does not make it possible to also modify the session or to +access the session before a request was fired. Starting with Flask 0.8 we +provide a so called “session transaction” which simulates the appropriate +calls to open a session in the context of the test client and to modify +it. At the end of the transaction the session is stored. This works +independently of the session backend used:: -Use the runner's :meth:`~flask.testing.FlaskCliRunner.invoke` method to -call commands in the same way they would be called with the ``flask`` -command from the command line. + with app.test_client() as c: + with c.session_transaction() as sess: + sess['a_key'] = 'a value' -.. code-block:: python + # once this is reached the session was stored - import click - - @app.cli.command("hello") - @click.option("--name", default="World") - def hello_command(name): - click.echo(f"Hello, {name}!") - - def test_hello_command(runner): - result = runner.invoke(args="hello") - assert "World" in result.output - - result = runner.invoke(args=["hello", "--name", "Flask"]) - assert "Flask" in result.output - - -Tests that depend on an Active Context --------------------------------------- - -You may have functions that are called from views or commands, that expect an -active :doc:`app context ` because they access :data:`.request`, -:data:`.session`, :data:`.g`, or :data:`.current_app`. Rather than testing them by -making a request or invoking the command, you can create and activate a context -directly. - -Use ``with app.app_context()`` to push an application context. For -example, database extensions usually require an active app context to -make queries. - -.. code-block:: python - - def test_db_post_model(app): - with app.app_context(): - post = db.session.query(Post).get(1) - -Use ``with app.test_request_context()`` to push a request context. It -takes the same arguments as the test client's request methods. - -.. code-block:: python - - def test_validate_user_edit(app): - with app.test_request_context( - "/user/2/edit", method="POST", data={"name": ""} - ): - # call a function that accesses `request` - messages = validate_edit_user() - - assert messages["name"][0] == "Name cannot be empty." - -Creating a test request context doesn't run any of the Flask dispatching -code, so ``before_request`` functions are not called. If you need to -call these, usually it's better to make a full request instead. However, -it's possible to call them manually. - -.. code-block:: python - - def test_auth_token(app): - with app.test_request_context("/user/2/edit", headers={"X-Auth-Token": "1"}): - app.preprocess_request() - assert g.user.name == "Flask" +Note that in this case you have to use the ``sess`` object instead of the +:data:`flask.session` proxy. The object however itself will provide the +same interface. diff --git a/docs/tutorial/blog.rst b/docs/tutorial/blog.rst deleted file mode 100644 index 6418f5ff..00000000 --- a/docs/tutorial/blog.rst +++ /dev/null @@ -1,336 +0,0 @@ -.. currentmodule:: flask - -Blog Blueprint -============== - -You'll use the same techniques you learned about when writing the -authentication blueprint to write the blog blueprint. The blog should -list all posts, allow logged in users to create posts, and allow the -author of a post to edit or delete it. - -As you implement each view, keep the development server running. As you -save your changes, try going to the URL in your browser and testing them -out. - -The Blueprint -------------- - -Define the blueprint and register it in the application factory. - -.. code-block:: python - :caption: ``flaskr/blog.py`` - - from flask import ( - Blueprint, flash, g, redirect, render_template, request, url_for - ) - from werkzeug.exceptions import abort - - from flaskr.auth import login_required - from flaskr.db import get_db - - bp = Blueprint('blog', __name__) - -Import and register the blueprint from the factory using -:meth:`app.register_blueprint() `. Place the -new code at the end of the factory function before returning the app. - -.. code-block:: python - :caption: ``flaskr/__init__.py`` - - def create_app(): - app = ... - # existing code omitted - - from . import blog - app.register_blueprint(blog.bp) - app.add_url_rule('/', endpoint='index') - - return app - - -Unlike the auth blueprint, the blog blueprint does not have a -``url_prefix``. So the ``index`` view will be at ``/``, the ``create`` -view at ``/create``, and so on. The blog is the main feature of Flaskr, -so it makes sense that the blog index will be the main index. - -However, the endpoint for the ``index`` view defined below will be -``blog.index``. Some of the authentication views referred to a plain -``index`` endpoint. :meth:`app.add_url_rule() ` -associates the endpoint name ``'index'`` with the ``/`` url so that -``url_for('index')`` or ``url_for('blog.index')`` will both work, -generating the same ``/`` URL either way. - -In another application you might give the blog blueprint a -``url_prefix`` and define a separate ``index`` view in the application -factory, similar to the ``hello`` view. Then the ``index`` and -``blog.index`` endpoints and URLs would be different. - - -Index ------ - -The index will show all of the posts, most recent first. A ``JOIN`` is -used so that the author information from the ``user`` table is -available in the result. - -.. code-block:: python - :caption: ``flaskr/blog.py`` - - @bp.route('/') - def index(): - db = get_db() - posts = db.execute( - 'SELECT p.id, title, body, created, author_id, username' - ' FROM post p JOIN user u ON p.author_id = u.id' - ' ORDER BY created DESC' - ).fetchall() - return render_template('blog/index.html', posts=posts) - -.. code-block:: html+jinja - :caption: ``flaskr/templates/blog/index.html`` - - {% extends 'base.html' %} - - {% block header %} -

{% block title %}Posts{% endblock %}

- {% if g.user %} - New - {% endif %} - {% endblock %} - - {% block content %} - {% for post in posts %} -
-
-
-

{{ post['title'] }}

-
by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
-
- {% if g.user['id'] == post['author_id'] %} - Edit - {% endif %} -
-

{{ post['body'] }}

-
- {% if not loop.last %} -
- {% endif %} - {% endfor %} - {% endblock %} - -When a user is logged in, the ``header`` block adds a link to the -``create`` view. When the user is the author of a post, they'll see an -"Edit" link to the ``update`` view for that post. ``loop.last`` is a -special variable available inside `Jinja for loops`_. It's used to -display a line after each post except the last one, to visually separate -them. - -.. _Jinja for loops: https://jinja.palletsprojects.com/templates/#for - - -Create ------- - -The ``create`` view works the same as the auth ``register`` view. Either -the form is displayed, or the posted data is validated and the post is -added to the database or an error is shown. - -The ``login_required`` decorator you wrote earlier is used on the blog -views. A user must be logged in to visit these views, otherwise they -will be redirected to the login page. - -.. code-block:: python - :caption: ``flaskr/blog.py`` - - @bp.route('/create', methods=('GET', 'POST')) - @login_required - def create(): - if request.method == 'POST': - title = request.form['title'] - body = request.form['body'] - error = None - - if not title: - error = 'Title is required.' - - if error is not None: - flash(error) - else: - db = get_db() - db.execute( - 'INSERT INTO post (title, body, author_id)' - ' VALUES (?, ?, ?)', - (title, body, g.user['id']) - ) - db.commit() - return redirect(url_for('blog.index')) - - return render_template('blog/create.html') - -.. code-block:: html+jinja - :caption: ``flaskr/templates/blog/create.html`` - - {% extends 'base.html' %} - - {% block header %} -

{% block title %}New Post{% endblock %}

- {% endblock %} - - {% block content %} -
- - - - - -
- {% endblock %} - - -Update ------- - -Both the ``update`` and ``delete`` views will need to fetch a ``post`` -by ``id`` and check if the author matches the logged in user. To avoid -duplicating code, you can write a function to get the ``post`` and call -it from each view. - -.. code-block:: python - :caption: ``flaskr/blog.py`` - - def get_post(id, check_author=True): - post = get_db().execute( - 'SELECT p.id, title, body, created, author_id, username' - ' FROM post p JOIN user u ON p.author_id = u.id' - ' WHERE p.id = ?', - (id,) - ).fetchone() - - if post is None: - abort(404, f"Post id {id} doesn't exist.") - - if check_author and post['author_id'] != g.user['id']: - abort(403) - - return post - -:func:`abort` will raise a special exception that returns an HTTP status -code. It takes an optional message to show with the error, otherwise a -default message is used. ``404`` means "Not Found", and ``403`` means -"Forbidden". (``401`` means "Unauthorized", but you redirect to the -login page instead of returning that status.) - -The ``check_author`` argument is defined so that the function can be -used to get a ``post`` without checking the author. This would be useful -if you wrote a view to show an individual post on a page, where the user -doesn't matter because they're not modifying the post. - -.. code-block:: python - :caption: ``flaskr/blog.py`` - - @bp.route('//update', methods=('GET', 'POST')) - @login_required - def update(id): - post = get_post(id) - - if request.method == 'POST': - title = request.form['title'] - body = request.form['body'] - error = None - - if not title: - error = 'Title is required.' - - if error is not None: - flash(error) - else: - db = get_db() - db.execute( - 'UPDATE post SET title = ?, body = ?' - ' WHERE id = ?', - (title, body, id) - ) - db.commit() - return redirect(url_for('blog.index')) - - return render_template('blog/update.html', post=post) - -Unlike the views you've written so far, the ``update`` function takes -an argument, ``id``. That corresponds to the ```` in the route. -A real URL will look like ``/1/update``. Flask will capture the ``1``, -ensure it's an :class:`int`, and pass it as the ``id`` argument. If you -don't specify ``int:`` and instead do ````, it will be a string. -To generate a URL to the update page, :func:`url_for` needs to be passed -the ``id`` so it knows what to fill in: -``url_for('blog.update', id=post['id'])``. This is also in the -``index.html`` file above. - -The ``create`` and ``update`` views look very similar. The main -difference is that the ``update`` view uses a ``post`` object and an -``UPDATE`` query instead of an ``INSERT``. With some clever refactoring, -you could use one view and template for both actions, but for the -tutorial it's clearer to keep them separate. - -.. code-block:: html+jinja - :caption: ``flaskr/templates/blog/update.html`` - - {% extends 'base.html' %} - - {% block header %} -

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

- {% endblock %} - - {% block content %} -
- - - - - -
-
-
- -
- {% endblock %} - -This template has two forms. The first posts the edited data to the -current page (``//update``). The other form contains only a button -and specifies an ``action`` attribute that posts to the delete view -instead. The button uses some JavaScript to show a confirmation dialog -before submitting. - -The pattern ``{{ request.form['title'] or post['title'] }}`` is used to -choose what data appears in the form. When the form hasn't been -submitted, the original ``post`` data appears, but if invalid form data -was posted you want to display that so the user can fix the error, so -``request.form`` is used instead. :data:`.request` is another variable -that's automatically available in templates. - - -Delete ------- - -The delete view doesn't have its own template, the delete button is part -of ``update.html`` and posts to the ``//delete`` URL. Since there -is no template, it will only handle the ``POST`` method and then redirect -to the ``index`` view. - -.. code-block:: python - :caption: ``flaskr/blog.py`` - - @bp.route('//delete', methods=('POST',)) - @login_required - def delete(id): - get_post(id) - db = get_db() - db.execute('DELETE FROM post WHERE id = ?', (id,)) - db.commit() - return redirect(url_for('blog.index')) - -Congratulations, you've now finished writing your application! Take some -time to try out everything in the browser. However, there's still more -to do before the project is complete. - -Continue to :doc:`install`. diff --git a/docs/tutorial/css.rst b/docs/tutorial/css.rst new file mode 100644 index 00000000..03f62ed1 --- /dev/null +++ b/docs/tutorial/css.rst @@ -0,0 +1,31 @@ +.. _tutorial-css: + +Step 7: Adding Style +==================== + +Now that everything else works, it's time to add some style to the +application. Just create a stylesheet called `style.css` in the `static` +folder we created before: + +.. sourcecode:: css + + body { font-family: sans-serif; background: #eee; } + a, h1, h2 { color: #377BA8; } + h1, h2 { font-family: 'Georgia', serif; margin: 0; } + h1 { border-bottom: 2px solid #eee; } + h2 { font-size: 1.2em; } + + .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; + padding: 0.8em; background: white; } + .entries { list-style: none; margin: 0; padding: 0; } + .entries li { margin: 0.8em 1.2em; } + .entries li h2 { margin-left: -1em; } + .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } + .add-entry dl { font-weight: bold; } + .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; + margin-bottom: 1em; background: #fafafa; } + .flash { background: #CEE5F5; padding: 0.5em; + border: 1px solid #AACBE2; } + .error { background: #F0D6D6; padding: 0.5em; } + +Continue with :ref:`tutorial-testing`. diff --git a/docs/tutorial/database.rst b/docs/tutorial/database.rst deleted file mode 100644 index cf132603..00000000 --- a/docs/tutorial/database.rst +++ /dev/null @@ -1,219 +0,0 @@ -.. currentmodule:: flask - -Define and Access the Database -============================== - -The application will use a `SQLite`_ database to store users and posts. -Python comes with built-in support for SQLite in the :mod:`sqlite3` -module. - -SQLite is convenient because it doesn't require setting up a separate -database server and is built-in to Python. However, if concurrent -requests try to write to the database at the same time, they will slow -down as each write happens sequentially. Small applications won't notice -this. Once you become big, you may want to switch to a different -database. - -The tutorial doesn't go into detail about SQL. If you are not familiar -with it, the SQLite docs describe the `language`_. - -.. _SQLite: https://sqlite.org/about.html -.. _language: https://sqlite.org/lang.html - - -Connect to the Database ------------------------ - -The first thing to do when working with a SQLite database (and most -other Python database libraries) is to create a connection to it. Any -queries and operations are performed using the connection, which is -closed after the work is finished. - -In web applications this connection is typically tied to the request. It -is created at some point when handling a request, and closed before the -response is sent. - -.. code-block:: python - :caption: ``flaskr/db.py`` - - import sqlite3 - from datetime import datetime - - import click - from flask import current_app, g - - - def get_db(): - if 'db' not in g: - g.db = sqlite3.connect( - current_app.config['DATABASE'], - detect_types=sqlite3.PARSE_DECLTYPES - ) - g.db.row_factory = sqlite3.Row - - return g.db - - - def close_db(e=None): - db = g.pop('db', None) - - if db is not None: - db.close() - -:data:`.g` is a special object that is unique for each request. It is -used to store data that might be accessed by multiple functions during -the request. The connection is stored and reused instead of creating a -new connection if ``get_db`` is called a second time in the same -request. - -:data:`.current_app` is another special object that points to the Flask -application handling the request. Since you used an application factory, -there is no application object when writing the rest of your code. -``get_db`` will be called when the application has been created and is -handling a request, so :data:`.current_app` can be used. - -:func:`sqlite3.connect` establishes a connection to the file pointed at -by the ``DATABASE`` configuration key. This file doesn't have to exist -yet, and won't until you initialize the database later. - -:class:`sqlite3.Row` tells the connection to return rows that behave -like dicts. This allows accessing the columns by name. - -``close_db`` checks if a connection was created by checking if ``g.db`` -was set. If the connection exists, it is closed. Further down you will -tell your application about the ``close_db`` function in the application -factory so that it is called after each request. - - -Create the Tables ------------------ - -In SQLite, data is stored in *tables* and *columns*. These need to be -created before you can store and retrieve data. Flaskr will store users -in the ``user`` table, and posts in the ``post`` table. Create a file -with the SQL commands needed to create empty tables: - -.. code-block:: sql - :caption: ``flaskr/schema.sql`` - - DROP TABLE IF EXISTS user; - DROP TABLE IF EXISTS post; - - CREATE TABLE user ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password TEXT NOT NULL - ); - - CREATE TABLE post ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - author_id INTEGER NOT NULL, - created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - title TEXT NOT NULL, - body TEXT NOT NULL, - FOREIGN KEY (author_id) REFERENCES user (id) - ); - -Add the Python functions that will run these SQL commands to the -``db.py`` file: - -.. code-block:: python - :caption: ``flaskr/db.py`` - - def init_db(): - db = get_db() - - with current_app.open_resource('schema.sql') as f: - db.executescript(f.read().decode('utf8')) - - - @click.command('init-db') - def init_db_command(): - """Clear the existing data and create new tables.""" - init_db() - click.echo('Initialized the database.') - - - sqlite3.register_converter( - "timestamp", lambda v: datetime.fromisoformat(v.decode()) - ) - -:meth:`open_resource() ` opens a file relative to -the ``flaskr`` package, which is useful since you won't necessarily know -where that location is when deploying the application later. ``get_db`` -returns a database connection, which is used to execute the commands -read from the file. - -:func:`click.command` defines a command line command called ``init-db`` -that calls the ``init_db`` function and shows a success message to the -user. You can read :doc:`/cli` to learn more about writing commands. - -The call to :func:`sqlite3.register_converter` tells Python how to -interpret timestamp values in the database. We convert the value to a -:class:`datetime.datetime`. - - -Register with the Application ------------------------------ - -The ``close_db`` and ``init_db_command`` functions need to be registered -with the application instance; otherwise, they won't be used by the -application. However, since you're using a factory function, that -instance isn't available when writing the functions. Instead, write a -function that takes an application and does the registration. - -.. code-block:: python - :caption: ``flaskr/db.py`` - - def init_app(app): - app.teardown_appcontext(close_db) - app.cli.add_command(init_db_command) - -:meth:`app.teardown_appcontext() ` tells -Flask to call that function when cleaning up after returning the -response. - -:meth:`app.cli.add_command() ` adds a new -command that can be called with the ``flask`` command. - -Import and call this function from the factory. Place the new code at -the end of the factory function before returning the app. - -.. code-block:: python - :caption: ``flaskr/__init__.py`` - - def create_app(): - app = ... - # existing code omitted - - from . import db - db.init_app(app) - - return app - - -Initialize the Database File ----------------------------- - -Now that ``init-db`` has been registered with the app, it can be called -using the ``flask`` command, similar to the ``run`` command from the -previous page. - -.. note:: - - If you're still running the server from the previous page, you can - either stop the server, or run this command in a new terminal. If - you use a new terminal, remember to change to your project directory - and activate the env as described in :doc:`/installation`. - -Run the ``init-db`` command: - -.. code-block:: none - - $ flask --app flaskr init-db - Initialized the database. - -There will now be a ``flaskr.sqlite`` file in the ``instance`` folder in -your project. - -Continue to :doc:`views`. diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst new file mode 100644 index 00000000..99391a27 --- /dev/null +++ b/docs/tutorial/dbcon.rst @@ -0,0 +1,57 @@ +.. _tutorial-dbcon: + +Step 4: Request Database Connections +------------------------------------ + +Now we know how we can open database connections and use them for scripts, +but how can we elegantly do that for requests? We will need the database +connection in all our functions so it makes sense to initialize them +before each request and shut them down afterwards. + +Flask allows us to do that with the :meth:`~flask.Flask.before_request`, +:meth:`~flask.Flask.after_request` and :meth:`~flask.Flask.teardown_request` +decorators:: + + @app.before_request + def before_request(): + g.db = connect_db() + + @app.teardown_request + def teardown_request(exception): + g.db.close() + +Functions marked with :meth:`~flask.Flask.before_request` are called before +a request and passed no arguments. Functions marked with +:meth:`~flask.Flask.after_request` are called after a request and +passed the response that will be sent to the client. They have to return +that response object or a different one. They are however not guaranteed +to be executed if an exception is raised, this is where functions marked with +:meth:`~flask.Flask.teardown_request` come in. They get called after the +response has been constructed. They are not allowed to modify the request, and +their return values are ignored. If an exception occurred while the request was +being processed, it is passed to each function; otherwise, `None` is passed in. + +We store our current database connection on the special :data:`~flask.g` +object that Flask provides for us. This object stores information for one +request only and is available from within each function. Never store such +things on other objects because this would not work with threaded +environments. That special :data:`~flask.g` object does some magic behind +the scenes to ensure it does the right thing. + +Continue to :ref:`tutorial-views`. + +.. hint:: Where do I put this code? + + If you've been following along in this tutorial, you might be wondering + where to put the code from this step and the next. A logical place is to + group these module-level functions together, and put your new + ``before_request`` and ``teardown_request`` functions below your existing + ``init_db`` function (following the tutorial line-by-line). + + If you need a moment to find your bearings, take a look at how the `example + source`_ is organized. In Flask, you can put all of your application code + into a single Python module. You don't have to, and if your app :ref:`grows + larger `, it's a good idea not to. + +.. _example source: + http://github.com/mitsuhiko/flask/tree/master/examples/flaskr/ diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst new file mode 100644 index 00000000..b546a1a8 --- /dev/null +++ b/docs/tutorial/dbinit.rst @@ -0,0 +1,67 @@ +.. _tutorial-dbinit: + +Step 3: Creating The Database +============================= + +Flaskr is a database powered application as outlined earlier, and more +precisely, an application powered by a relational database system. Such +systems need a schema that tells them how to store that information. So +before starting the server for the first time it's important to create +that schema. + +Such a schema can be created by piping the `schema.sql` file into the +`sqlite3` command as follows:: + + sqlite3 /tmp/flaskr.db < schema.sql + +The downside of this is that it requires the sqlite3 command to be +installed which is not necessarily the case on every system. Also one has +to provide the path to the database there which leaves some place for +errors. It's a good idea to add a function that initializes the database +for you to the application. + +If you want to do that, you first have to import the +:func:`contextlib.closing` function from the contextlib package. If you +want to use Python 2.5 it's also necessary to enable the `with` statement +first (`__future__` imports must be the very first import):: + + from __future__ import with_statement + from contextlib import closing + +Next we can create a function called `init_db` that initializes the +database. For this we can use the `connect_db` function we defined +earlier. Just add that function below the `connect_db` function:: + + def init_db(): + with closing(connect_db()) as db: + with app.open_resource('schema.sql') as f: + db.cursor().executescript(f.read()) + db.commit() + +The :func:`~contextlib.closing` helper function allows us to keep a +connection open for the duration of the `with` block. The +:func:`~flask.Flask.open_resource` method of the application object +supports that functionality out of the box, so it can be used in the +`with` block directly. This function opens a file from the resource +location (your `flaskr` folder) and allows you to read from it. We are +using this here to execute a script on the database connection. + +When we connect to a database we get a connection object (here called +`db`) that can give us a cursor. On that cursor there is a method to +execute a complete script. Finally we only have to commit the changes. +SQLite 3 and other transactional databases will not commit unless you +explicitly tell it to. + +Now it is possible to create a database by starting up a Python shell and +importing and calling that function:: + +>>> from flaskr import init_db +>>> init_db() + +.. admonition:: Troubleshooting + + If you get an exception later that a table cannot be found check that + you did call the `init_db` function and that your table names are + correct (singular vs. plural for example). + +Continue with :ref:`tutorial-dbcon` diff --git a/docs/tutorial/deploy.rst b/docs/tutorial/deploy.rst deleted file mode 100644 index eb3a53ac..00000000 --- a/docs/tutorial/deploy.rst +++ /dev/null @@ -1,111 +0,0 @@ -Deploy to Production -==================== - -This part of the tutorial assumes you have a server that you want to -deploy your application to. It gives an overview of how to create the -distribution file and install it, but won't go into specifics about -what server or software to use. You can set up a new environment on your -development computer to try out the instructions below, but probably -shouldn't use it for hosting a real public application. See -:doc:`/deploying/index` for a list of many different ways to host your -application. - - -Build and Install ------------------ - -When you want to deploy your application elsewhere, you build a *wheel* -(``.whl``) file. Install and use the ``build`` tool to do this. - -.. code-block:: none - - $ pip install build - $ python -m build --wheel - -You can find the file in ``dist/flaskr-1.0.0-py3-none-any.whl``. The -file name is in the format of {project name}-{version}-{python tag} --{abi tag}-{platform tag}. - -Copy this file to another machine, -:ref:`set up a new virtualenv `, then install the -file with ``pip``. - -.. code-block:: none - - $ pip install flaskr-1.0.0-py3-none-any.whl - -Pip will install your project along with its dependencies. - -Since this is a different machine, you need to run ``init-db`` again to -create the database in the instance folder. - - .. code-block:: text - - $ flask --app flaskr init-db - -When Flask detects that it's installed (not in editable mode), it uses -a different directory for the instance folder. You can find it at -``.venv/var/flaskr-instance`` instead. - - -Configure the Secret Key ------------------------- - -In the beginning of the tutorial that you gave a default value for -:data:`SECRET_KEY`. This should be changed to some random bytes in -production. Otherwise, attackers could use the public ``'dev'`` key to -modify the session cookie, or anything else that uses the secret key. - -You can use the following command to output a random secret key: - -.. code-block:: none - - $ python -c 'import secrets; print(secrets.token_hex())' - - '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' - -Create the ``config.py`` file in the instance folder, which the factory -will read from if it exists. Copy the generated value into it. - -.. code-block:: python - :caption: ``.venv/var/flaskr-instance/config.py`` - - SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' - -You can also set any other necessary configuration here, although -``SECRET_KEY`` is the only one needed for Flaskr. - - -Run with a Production Server ----------------------------- - -When running publicly rather than in development, you should not use the -built-in development server (``flask run``). The development server is -provided by Werkzeug for convenience, but is not designed to be -particularly efficient, stable, or secure. - -Instead, use a production WSGI server. For example, to use `Waitress`_, -first install it in the virtual environment: - -.. code-block:: none - - $ pip install waitress - -You need to tell Waitress about your application, but it doesn't use -``--app`` like ``flask run`` does. You need to tell it to import and -call the application factory to get an application object. - -.. code-block:: none - - $ waitress-serve --call 'flaskr:create_app' - - Serving on http://0.0.0.0:8080 - -See :doc:`/deploying/index` for a list of many different ways to host -your application. Waitress is just an example, chosen for the tutorial -because it supports both Windows and Linux. There are many more WSGI -servers and deployment options that you may choose for your project. - -.. _Waitress: https://docs.pylonsproject.org/projects/waitress/en/stable/ - -Continue to :doc:`next`. diff --git a/docs/tutorial/factory.rst b/docs/tutorial/factory.rst deleted file mode 100644 index 381477f9..00000000 --- a/docs/tutorial/factory.rst +++ /dev/null @@ -1,159 +0,0 @@ -.. currentmodule:: flask - -Application Setup -================= - -A Flask application is an instance of the :class:`Flask` class. -Everything about the application, such as configuration and URLs, will -be registered with this class. - -The most straightforward way to create a Flask application is to create -a global :class:`Flask` instance directly at the top of your code, like -how the "Hello, World!" example did on the previous page. While this is -simple and useful in some cases, it can cause some tricky issues as the -project grows. - -Instead of creating a :class:`Flask` instance globally, you will create -it inside a function. This function is known as the *application -factory*. Any configuration, registration, and other setup the -application needs will happen inside the function, then the application -will be returned. - - -The Application Factory ------------------------ - -It's time to start coding! Create the ``flaskr`` directory and add the -``__init__.py`` file. The ``__init__.py`` serves double duty: it will -contain the application factory, and it tells Python that the ``flaskr`` -directory should be treated as a package. - -.. code-block:: none - - $ mkdir flaskr - -.. code-block:: python - :caption: ``flaskr/__init__.py`` - - import os - - from flask import Flask - - - def create_app(test_config=None): - # create and configure the app - app = Flask(__name__, instance_relative_config=True) - app.config.from_mapping( - SECRET_KEY='dev', - DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'), - ) - - if test_config is None: - # load the instance config, if it exists, when not testing - app.config.from_pyfile('config.py', silent=True) - else: - # load the test config if passed in - app.config.from_mapping(test_config) - - # ensure the instance folder exists - os.makedirs(app.instance_path, exist_ok=True) - - # a simple page that says hello - @app.route('/hello') - def hello(): - return 'Hello, World!' - - return app - -``create_app`` is the application factory function. You'll add to it -later in the tutorial, but it already does a lot. - -#. ``app = Flask(__name__, instance_relative_config=True)`` creates the - :class:`Flask` instance. - - * ``__name__`` is the name of the current Python module. The app - needs to know where it's located to set up some paths, and - ``__name__`` is a convenient way to tell it that. - - * ``instance_relative_config=True`` tells the app that - configuration files are relative to the - :ref:`instance folder `. The instance folder - is located outside the ``flaskr`` package and can hold local - data that shouldn't be committed to version control, such as - configuration secrets and the database file. - -#. :meth:`app.config.from_mapping() ` sets - some default configuration that the app will use: - - * :data:`SECRET_KEY` is used by Flask and extensions to keep data - safe. It's set to ``'dev'`` to provide a convenient value - during development, but it should be overridden with a random - value when deploying. - - * ``DATABASE`` is the path where the SQLite database file will be - saved. It's under - :attr:`app.instance_path `, which is the - path that Flask has chosen for the instance folder. You'll learn - more about the database in the next section. - -#. :meth:`app.config.from_pyfile() ` overrides - the default configuration with values taken from the ``config.py`` - file in the instance folder if it exists. For example, when - deploying, this can be used to set a real ``SECRET_KEY``. - - * ``test_config`` can also be passed to the factory, and will be - used instead of the instance configuration. This is so the tests - you'll write later in the tutorial can be configured - independently of any development values you have configured. - -#. :func:`os.makedirs` ensures that - :attr:`app.instance_path ` exists. Flask - doesn't create the instance folder automatically, but it needs to be - created because your project will create the SQLite database file - there. - -#. :meth:`@app.route() ` creates a simple route so you can - see the application working before getting into the rest of the - tutorial. It creates a connection between the URL ``/hello`` and a - function that returns a response, the string ``'Hello, World!'`` in - this case. - - -Run The Application -------------------- - -Now you can run your application using the ``flask`` command. From the -terminal, tell Flask where to find your application, then run it in -debug mode. Remember, you should still be in the top-level -``flask-tutorial`` directory, not the ``flaskr`` package. - -Debug mode shows an interactive debugger whenever a page raises an -exception, and restarts the server whenever you make changes to the -code. You can leave it running and just reload the browser page as you -follow the tutorial. - -.. code-block:: text - - $ flask --app flaskr run --debug - -You'll see output similar to this: - -.. code-block:: text - - * Serving Flask app "flaskr" - * Debug mode: on - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) - * Restarting with stat - * Debugger is active! - * Debugger PIN: nnn-nnn-nnn - -Visit http://127.0.0.1:5000/hello in a browser and you should see the -"Hello, World!" message. Congratulations, you're now running your Flask -web application! - -If another program is already using port 5000, you'll see -``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the -server tries to start. See :ref:`address-already-in-use` for how to -handle that. - -Continue to :doc:`database`. diff --git a/docs/tutorial/flaskr_edit.png b/docs/tutorial/flaskr_edit.png deleted file mode 100644 index 6cd6e398..00000000 Binary files a/docs/tutorial/flaskr_edit.png and /dev/null differ diff --git a/docs/tutorial/flaskr_index.png b/docs/tutorial/flaskr_index.png deleted file mode 100644 index aa2b50f5..00000000 Binary files a/docs/tutorial/flaskr_index.png and /dev/null differ diff --git a/docs/tutorial/flaskr_login.png b/docs/tutorial/flaskr_login.png deleted file mode 100644 index d482c645..00000000 Binary files a/docs/tutorial/flaskr_login.png and /dev/null differ diff --git a/docs/tutorial/folders.rst b/docs/tutorial/folders.rst new file mode 100644 index 00000000..61080932 --- /dev/null +++ b/docs/tutorial/folders.rst @@ -0,0 +1,23 @@ +.. _tutorial-folders: + +Step 0: Creating The Folders +============================ + +Before we get started, let's create the folders needed for this +application:: + + /flaskr + /static + /templates + +The `flaskr` folder is not a python package, but just something where we +drop our files. Directly into this folder we will then put our database +schema as well as main module in the following steps. The files inside +the `static` folder are available to users of the application via `HTTP`. +This is the place where css and javascript files go. Inside the +`templates` folder Flask will look for `Jinja2`_ templates. The +templates you create later in the tutorial will go in this directory. + +Continue with :ref:`tutorial-schema`. + +.. _Jinja2: http://jinja.pocoo.org/2/ diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index d5dc5b3c..3f2d659e 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -1,64 +1,32 @@ +.. _tutorial: + Tutorial ======== +You want to develop an application with Python and Flask? Here you have +the chance to learn that by example. In this tutorial we will create a +simple microblog application. It only supports one user that can create +text-only entries and there are no feeds or comments, but it still +features everything you need to get started. We will use Flask and SQLite +as database which comes out of the box with Python, so there is nothing +else you need. + +If you want the full sourcecode in advance or for comparison, check out +the `example source`_. + +.. _example source: + http://github.com/mitsuhiko/flask/tree/master/examples/flaskr/ + .. toctree:: - :caption: Contents: - :maxdepth: 1 + :maxdepth: 2 - layout - factory - database - views - templates - static - blog - install - tests - deploy - next - -This tutorial will walk you through creating a basic blog application -called Flaskr. Users will be able to register, log in, create posts, -and edit or delete their own posts. You will be able to package and -install the application on other computers. - -.. image:: flaskr_index.png - :align: center - :class: screenshot - :alt: screenshot of index page - -It's assumed that you're already familiar with Python. The `official -tutorial`_ in the Python docs is a great way to learn or review first. - -.. _official tutorial: https://docs.python.org/3/tutorial/ - -While it's designed to give a good starting point, the tutorial doesn't -cover all of Flask's features. Check out the :doc:`/quickstart` for an -overview of what Flask can do, then dive into the docs to find out more. -The tutorial only uses what's provided by Flask and Python. In another -project, you might decide to use :doc:`/extensions` or other libraries -to make some tasks simpler. - -.. image:: flaskr_login.png - :align: center - :class: screenshot - :alt: screenshot of login page - -Flask is flexible. It doesn't require you to use any particular project -or code layout. However, when first starting, it's helpful to use a more -structured approach. This means that the tutorial will require a bit of -boilerplate up front, but it's done to avoid many common pitfalls that -new developers encounter, and it creates a project that's easy to expand -on. Once you become more comfortable with Flask, you can step out of -this structure and take full advantage of Flask's flexibility. - -.. image:: flaskr_edit.png - :align: center - :class: screenshot - :alt: screenshot of edit page - -:gh:`The tutorial project is available as an example in the Flask -repository `, if you want to compare your project -with the final product as you follow the tutorial. - -Continue to :doc:`layout`. + introduction + folders + schema + setup + dbinit + dbcon + views + templates + css + testing diff --git a/docs/tutorial/install.rst b/docs/tutorial/install.rst deleted file mode 100644 index db83e106..00000000 --- a/docs/tutorial/install.rst +++ /dev/null @@ -1,89 +0,0 @@ -Make the Project Installable -============================ - -Making your project installable means that you can build a *wheel* file and install that -in another environment, just like you installed Flask in your project's environment. -This makes deploying your project the same as installing any other library, so you're -using all the standard Python tools to manage everything. - -Installing also comes with other benefits that might not be obvious from -the tutorial or as a new Python user, including: - -* Currently, Python and Flask understand how to use the ``flaskr`` - package only because you're running from your project's directory. - Installing means you can import it no matter where you run from. - -* You can manage your project's dependencies just like other packages - do, so ``pip install yourproject.whl`` installs them. - -* Test tools can isolate your test environment from your development - environment. - -.. note:: - This is being introduced late in the tutorial, but in your future - projects you should always start with this. - - -Describe the Project --------------------- - -The ``pyproject.toml`` file describes your project and how to build it. - -.. code-block:: toml - :caption: ``pyproject.toml`` - - [project] - name = "flaskr" - version = "1.0.0" - description = "The basic blog app built in the Flask tutorial." - dependencies = [ - "flask", - ] - - [build-system] - requires = ["flit_core<4"] - build-backend = "flit_core.buildapi" - -See the official `Packaging tutorial `_ for more -explanation of the files and options used. - -.. _packaging tutorial: https://packaging.python.org/tutorials/packaging-projects/ - - -Install the Project -------------------- - -Use ``pip`` to install your project in the virtual environment. - -.. code-block:: none - - $ pip install -e . - -This tells pip to find ``pyproject.toml`` in the current directory and install the -project in *editable* or *development* mode. Editable mode means that as you make -changes to your local code, you'll only need to re-install if you change the metadata -about the project, such as its dependencies. - -You can observe that the project is now installed with ``pip list``. - -.. code-block:: none - - $ pip list - - Package Version Location - -------------- --------- ---------------------------------- - click 6.7 - Flask 1.0 - flaskr 1.0.0 /home/user/Projects/flask-tutorial - itsdangerous 0.24 - Jinja2 2.10 - MarkupSafe 1.0 - pip 9.0.3 - Werkzeug 0.14.1 - -Nothing changes from how you've been running your project so far. -``--app`` is still set to ``flaskr`` and ``flask run`` still runs -the application, but you can call it from anywhere, not just the -``flask-tutorial`` directory. - -Continue to :doc:`tests`. diff --git a/docs/tutorial/introduction.rst b/docs/tutorial/introduction.rst new file mode 100644 index 00000000..c72bbd7d --- /dev/null +++ b/docs/tutorial/introduction.rst @@ -0,0 +1,33 @@ +.. _tutorial-introduction: + +Introducing Flaskr +================== + +We will call our blogging application flaskr here, feel free to chose a +less web-2.0-ish name ;) Basically we want it to do the following things: + +1. let the user sign in and out with credentials specified in the + configuration. Only one user is supported. +2. when the user is logged in they can add new entries to the page + consisting of a text-only title and some HTML for the text. This HTML + is not sanitized because we trust the user here. +3. the page shows all entries so far in reverse order (newest on top) and + the user can add new ones from there if logged in. + +We will be using SQLite3 directly for that application because it's good +enough for an application of that size. For larger applications however +it makes a lot of sense to use `SQLAlchemy`_ that handles database +connections in a more intelligent way, allows you to target different +relational databases at once and more. You might also want to consider +one of the popular NoSQL databases if your data is more suited for those. + +Here a screenshot from the final application: + +.. image:: ../_static/flaskr.png + :align: center + :class: screenshot + :alt: screenshot of the final application + +Continue with :ref:`tutorial-folders`. + +.. _SQLAlchemy: http://www.sqlalchemy.org/ diff --git a/docs/tutorial/layout.rst b/docs/tutorial/layout.rst deleted file mode 100644 index 9f510927..00000000 --- a/docs/tutorial/layout.rst +++ /dev/null @@ -1,105 +0,0 @@ -Project Layout -============== - -Create a project directory and enter it: - -.. code-block:: none - - $ mkdir flask-tutorial - $ cd flask-tutorial - -Then follow the :doc:`installation instructions ` to set -up a Python virtual environment and install Flask for your project. - -The tutorial will assume you're working from the ``flask-tutorial`` -directory from now on. The file names at the top of each code block are -relative to this directory. - ----- - -A Flask application can be as simple as a single file. - -.. code-block:: python - :caption: ``hello.py`` - - from flask import Flask - - app = Flask(__name__) - - - @app.route('/') - def hello(): - return 'Hello, World!' - -However, as a project gets bigger, it becomes overwhelming to keep all -the code in one file. Python projects use *packages* to organize code -into multiple modules that can be imported where needed, and the -tutorial will do this as well. - -The project directory will contain: - -* ``flaskr/``, a Python package containing your application code and - files. -* ``tests/``, a directory containing test modules. -* ``.venv/``, a Python virtual environment where Flask and other - dependencies are installed. -* Installation files telling Python how to install your project. -* Version control config, such as `git`_. You should make a habit of - using some type of version control for all your projects, no matter - the size. -* Any other project files you might add in the future. - -.. _git: https://git-scm.com/ - -By the end, your project layout will look like this: - -.. code-block:: none - - /home/user/Projects/flask-tutorial - ├── flaskr/ - │ ├── __init__.py - │ ├── db.py - │ ├── schema.sql - │ ├── auth.py - │ ├── blog.py - │ ├── templates/ - │ │ ├── base.html - │ │ ├── auth/ - │ │ │ ├── login.html - │ │ │ └── register.html - │ │ └── blog/ - │ │ ├── create.html - │ │ ├── index.html - │ │ └── update.html - │ └── static/ - │ └── style.css - ├── tests/ - │ ├── conftest.py - │ ├── data.sql - │ ├── test_factory.py - │ ├── test_db.py - │ ├── test_auth.py - │ └── test_blog.py - ├── .venv/ - └── pyproject.toml - -If you're using version control, the following files that are generated -while running your project should be ignored. There may be other files -based on the editor you use. In general, ignore files that you didn't -write. For example, with git: - -.. code-block:: none - :caption: ``.gitignore`` - - .venv/ - - *.pyc - __pycache__/ - - instance/ - - .pytest_cache/ - .coverage - htmlcov/ - -Continue to :doc:`factory`. diff --git a/docs/tutorial/next.rst b/docs/tutorial/next.rst deleted file mode 100644 index d41e8ef2..00000000 --- a/docs/tutorial/next.rst +++ /dev/null @@ -1,38 +0,0 @@ -Keep Developing! -================ - -You've learned about quite a few Flask and Python concepts throughout -the tutorial. Go back and review the tutorial and compare your code with -the steps you took to get there. Compare your project to the -:gh:`example project `, which might look a bit -different due to the step-by-step nature of the tutorial. - -There's a lot more to Flask than what you've seen so far. Even so, -you're now equipped to start developing your own web applications. Check -out the :doc:`/quickstart` for an overview of what Flask can do, then -dive into the docs to keep learning. Flask uses `Jinja`_, `Click`_, -`Werkzeug`_, and `ItsDangerous`_ behind the scenes, and they all have -their own documentation too. You'll also be interested in -:doc:`/extensions` which make tasks like working with the database or -validating form data easier and more powerful. - -If you want to keep developing your Flaskr project, here are some ideas -for what to try next: - -* A detail view to show a single post. Click a post's title to go to - its page. -* Like / unlike a post. -* Comments. -* Tags. Clicking a tag shows all the posts with that tag. -* A search box that filters the index page by name. -* Paged display. Only show 5 posts per page. -* Upload an image to go along with a post. -* Format posts using Markdown. -* An RSS feed of new posts. - -Have fun and make awesome applications! - -.. _Jinja: https://palletsprojects.com/p/jinja/ -.. _Click: https://palletsprojects.com/p/click/ -.. _Werkzeug: https://palletsprojects.com/p/werkzeug/ -.. _ItsDangerous: https://palletsprojects.com/p/itsdangerous/ diff --git a/docs/tutorial/schema.rst b/docs/tutorial/schema.rst new file mode 100644 index 00000000..c078667e --- /dev/null +++ b/docs/tutorial/schema.rst @@ -0,0 +1,25 @@ +.. _tutorial-schema: + +Step 1: Database Schema +======================= + +First we want to create the database schema. For this application only a +single table is needed and we only want to support SQLite so that is quite +easy. Just put the following contents into a file named `schema.sql` in +the just created `flaskr` folder: + +.. sourcecode:: sql + + drop table if exists entries; + create table entries ( + id integer primary key autoincrement, + title string not null, + text string not null + ); + +This schema consists of a single table called `entries` and each row in +this table has an `id`, a `title` and a `text`. The `id` is an +automatically incrementing integer and a primary key, the other two are +strings that must not be null. + +Continue with :ref:`tutorial-setup`. diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst new file mode 100644 index 00000000..e9e4d679 --- /dev/null +++ b/docs/tutorial/setup.rst @@ -0,0 +1,90 @@ +.. _tutorial-setup: + +Step 2: Application Setup Code +============================== + +Now that we have the schema in place we can create the application module. +Let's call it `flaskr.py` inside the `flaskr` folder. For starters we +will add the imports we will need as well as the config section. For +small applications it's a possibility to drop the configuration directly +into the module which we will be doing here. However a cleaner solution +would be to create a separate `.ini` or `.py` file and load that or import +the values from there. + +:: + + # all the imports + import sqlite3 + from flask import Flask, request, session, g, redirect, url_for, \ + abort, render_template, flash + + # configuration + DATABASE = '/tmp/flaskr.db' + DEBUG = True + SECRET_KEY = 'development key' + USERNAME = 'admin' + PASSWORD = 'default' + +Next we can create our actual application and initialize it with the +config from the same file:: + + # create our little application :) + app = Flask(__name__) + app.config.from_object(__name__) + +:meth:`~flask.Config.from_object` will look at the given object (if it's a +string it will import it) and then look for all uppercase variables +defined there. In our case, the configuration we just wrote a few lines +of code above. You can also move that into a separate file. + +It is also a good idea to be able to load a configuration from a +configurable file. This is what :meth:`~flask.Config.from_envvar` can +do:: + + app.config.from_envvar('FLASKR_SETTINGS', silent=True) + +That way someone can set an environment variable called +:envvar:`FLASKR_SETTINGS` to specify a config file to be loaded which will +then override the default values. The silent switch just tells Flask to +not complain if no such environment key is set. + +The `secret_key` is needed to keep the client-side sessions secure. +Choose that key wisely and as hard to guess and complex as possible. The +debug flag enables or disables the interactive debugger. Never leave +debug mode activated in a production system because it will allow users to +execute code on the server! + +We also add a method to easily connect to the database specified. That +can be used to open a connection on request and also from the interactive +Python shell or a script. This will come in handy later. + +:: + + def connect_db(): + return sqlite3.connect(app.config['DATABASE']) + +Finally we just add a line to the bottom of the file that fires up the +server if we want to run that file as a standalone application:: + + if __name__ == '__main__': + app.run() + +With that out of the way you should be able to start up the application +without problems. Do this with the following command:: + + python flaskr.py + +You will see a message telling you that server has started along with +the address at which you can access it. + +When you head over to the server in your browser you will get an 404 +page not found error because we don't have any views yet. But we will +focus on that a little later. First we should get the database working. + +.. admonition:: Externally Visible Server + + Want your server to be publicly available? Check out the + :ref:`externally visible server ` section for more + information. + +Continue with :ref:`tutorial-dbinit`. diff --git a/docs/tutorial/static.rst b/docs/tutorial/static.rst deleted file mode 100644 index 8e76c409..00000000 --- a/docs/tutorial/static.rst +++ /dev/null @@ -1,72 +0,0 @@ -Static Files -============ - -The authentication views and templates work, but they look very plain -right now. Some `CSS`_ can be added to add style to the HTML layout you -constructed. The style won't change, so it's a *static* file rather than -a template. - -Flask automatically adds a ``static`` view that takes a path relative -to the ``flaskr/static`` directory and serves it. The ``base.html`` -template already has a link to the ``style.css`` file: - -.. code-block:: html+jinja - - {{ url_for('static', filename='style.css') }} - -Besides CSS, other types of static files might be files with JavaScript -functions, or a logo image. They are all placed under the -``flaskr/static`` directory and referenced with -``url_for('static', filename='...')``. - -This tutorial isn't focused on how to write CSS, so you can just copy -the following into the ``flaskr/static/style.css`` file: - -.. code-block:: css - :caption: ``flaskr/static/style.css`` - - html { font-family: sans-serif; background: #eee; padding: 1rem; } - body { max-width: 960px; margin: 0 auto; background: white; } - h1 { font-family: serif; color: #377ba8; margin: 1rem 0; } - a { color: #377ba8; } - hr { border: none; border-top: 1px solid lightgray; } - nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; } - nav h1 { flex: auto; margin: 0; } - nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; } - nav ul { display: flex; list-style: none; margin: 0; padding: 0; } - nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; } - .content { padding: 0 1rem 1rem; } - .content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; } - .content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; } - .flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; } - .post > header { display: flex; align-items: flex-end; font-size: 0.85em; } - .post > header > div:first-of-type { flex: auto; } - .post > header h1 { font-size: 1.5em; margin-bottom: 0; } - .post .about { color: slategray; font-style: italic; } - .post .body { white-space: pre-line; } - .content:last-child { margin-bottom: 0; } - .content form { margin: 1em 0; display: flex; flex-direction: column; } - .content label { font-weight: bold; margin-bottom: 0.5em; } - .content input, .content textarea { margin-bottom: 1em; } - .content textarea { min-height: 12em; resize: vertical; } - input.danger { color: #cc2f2e; } - input[type=submit] { align-self: start; min-width: 10em; } - -You can find a less compact version of ``style.css`` in the -:gh:`example code `. - -Go to http://127.0.0.1:5000/auth/login and the page should look like the -screenshot below. - -.. image:: flaskr_login.png - :align: center - :class: screenshot - :alt: screenshot of login page - -You can read more about CSS from `Mozilla's documentation `_. If -you change a static file, refresh the browser page. If the change -doesn't show up, try clearing your browser's cache. - -.. _CSS: https://developer.mozilla.org/docs/Web/CSS - -Continue to :doc:`blog`. diff --git a/docs/tutorial/templates.rst b/docs/tutorial/templates.rst index ca9d4b32..5ec5584d 100644 --- a/docs/tutorial/templates.rst +++ b/docs/tutorial/templates.rst @@ -1,187 +1,111 @@ -.. currentmodule:: flask +.. _tutorial-templates: -Templates -========= +Step 6: The Templates +===================== -You've written the authentication views for your application, but if -you're running the server and try to go to any of the URLs, you'll see a -``TemplateNotFound`` error. That's because the views are calling -:func:`render_template`, but you haven't written the templates yet. -The template files will be stored in the ``templates`` directory inside -the ``flaskr`` package. +Now we should start working on the templates. If we request the URLs now +we would only get an exception that Flask cannot find the templates. The +templates are using `Jinja2`_ syntax and have autoescaping enabled by +default. This means that unless you mark a value in the code with +:class:`~flask.Markup` or with the ``|safe`` filter in the template, +Jinja2 will ensure that special characters such as ``<`` or ``>`` are +escaped with their XML equivalents. -Templates are files that contain static data as well as placeholders -for dynamic data. A template is rendered with specific data to produce a -final document. Flask uses the `Jinja`_ template library to render -templates. +We are also using template inheritance which makes it possible to reuse +the layout of the website in all pages. -In your application, you will use templates to render `HTML`_ which -will display in the user's browser. In Flask, Jinja is configured to -*autoescape* any data that is rendered in HTML templates. This means -that it's safe to render user input; any characters they've entered that -could mess with the HTML, such as ``<`` and ``>`` will be *escaped* with -*safe* values that look the same in the browser but don't cause unwanted -effects. +Put the following templates into the `templates` folder: -Jinja looks and behaves mostly like Python. Special delimiters are used -to distinguish Jinja syntax from the static data in the template. -Anything between ``{{`` and ``}}`` is an expression that will be output -to the final document. ``{%`` and ``%}`` denotes a control flow -statement like ``if`` and ``for``. Unlike Python, blocks are denoted -by start and end tags rather than indentation since static text within -a block could change indentation. +.. _Jinja2: http://jinja.pocoo.org/2/documentation/templates -.. _Jinja: https://jinja.palletsprojects.com/templates/ -.. _HTML: https://developer.mozilla.org/docs/Web/HTML +layout.html +----------- +This template contains the HTML skeleton, the header and a link to log in +(or log out if the user was already logged in). It also displays the +flashed messages if there are any. The ``{% block body %}`` block can be +replaced by a block of the same name (``body``) in a child template. -The Base Layout ---------------- +The :class:`~flask.session` dict is available in the template as well and +you can use that to check if the user is logged in or not. Note that in +Jinja you can access missing attributes and items of objects / dicts which +makes the following code work, even if there is no ``'logged_in'`` key in +the session: -Each page in the application will have the same basic layout around a -different body. Instead of writing the entire HTML structure in each -template, each template will *extend* a base template and override -specific sections. - -.. code-block:: html+jinja - :caption: ``flaskr/templates/base.html`` +.. sourcecode:: html+jinja - {% block title %}{% endblock %} - Flaskr - - -
-
- {% block header %}{% endblock %} -
+
+ {% if not session.logged_in %} + log in + {% else %} + log out + {% endif %} +
{% for message in get_flashed_messages() %} -
{{ message }}
+
{{ message }}
{% endfor %} - {% block content %}{% endblock %} -
+ {% block body %}{% endblock %} +
-:data:`.g` is automatically available in templates. Based on if -``g.user`` is set (from ``load_logged_in_user``), either the username -and a log out link are displayed, or links to register and log in -are displayed. :func:`url_for` is also automatically available, and is -used to generate URLs to views instead of writing them out manually. +show_entries.html +----------------- -After the page title, and before the content, the template loops over -each message returned by :func:`get_flashed_messages`. You used -:func:`flash` in the views to show error messages, and this is the code -that will display them. +This template extends the `layout.html` template from above to display the +messages. Note that the `for` loop iterates over the messages we passed +in with the :func:`~flask.render_template` function. We also tell the +form to submit to your `add_entry` function and use `POST` as `HTTP` +method: -There are three blocks defined here that will be overridden in the other -templates: +.. sourcecode:: html+jinja -#. ``{% block title %}`` will change the title displayed in the - browser's tab and window title. - -#. ``{% block header %}`` is similar to ``title`` but will change the - title displayed on the page. - -#. ``{% block content %}`` is where the content of each page goes, such - as the login form or a blog post. - -The base template is directly in the ``templates`` directory. To keep -the others organized, the templates for a blueprint will be placed in a -directory with the same name as the blueprint. - - -Register --------- - -.. code-block:: html+jinja - :caption: ``flaskr/templates/auth/register.html`` - - {% extends 'base.html' %} - - {% block header %} -

{% block title %}Register{% endblock %}

+ {% extends "layout.html" %} + {% block body %} + {% if session.logged_in %} +
+
+
Title: +
+
Text: +
+
+
+
+ {% endif %} +
    + {% for entry in entries %} +
  • {{ entry.title }}

    {{ entry.text|safe }} + {% else %} +
  • Unbelievable. No entries here so far + {% endfor %} +
{% endblock %} - {% block content %} -
- - - - - +login.html +---------- + +Finally the login template which basically just displays a form to allow +the user to login: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block body %} +

Login

+ {% if error %}

Error: {{ error }}{% endif %} + +

+
Username: +
+
Password: +
+
+
{% endblock %} -``{% extends 'base.html' %}`` tells Jinja that this template should -replace the blocks from the base template. All the rendered content must -appear inside ``{% block %}`` tags that override blocks from the base -template. - -A useful pattern used here is to place ``{% block title %}`` inside -``{% block header %}``. This will set the title block and then output -the value of it into the header block, so that both the window and page -share the same title without writing it twice. - -The ``input`` tags are using the ``required`` attribute here. This tells -the browser not to submit the form until those fields are filled in. If -the user is using an older browser that doesn't support that attribute, -or if they are using something besides a browser to make requests, you -still want to validate the data in the Flask view. It's important to -always fully validate the data on the server, even if the client does -some validation as well. - - -Log In ------- - -This is identical to the register template except for the title and -submit button. - -.. code-block:: html+jinja - :caption: ``flaskr/templates/auth/login.html`` - - {% extends 'base.html' %} - - {% block header %} -

{% block title %}Log In{% endblock %}

- {% endblock %} - - {% block content %} -
- - - - - -
- {% endblock %} - - -Register A User ---------------- - -Now that the authentication templates are written, you can register a -user. Make sure the server is still running (``flask run`` if it's not), -then go to http://127.0.0.1:5000/auth/register. - -Try clicking the "Register" button without filling out the form and see -that the browser shows an error message. Try removing the ``required`` -attributes from the ``register.html`` template and click "Register" -again. Instead of the browser showing an error, the page will reload and -the error from :func:`flash` in the view will be shown. - -Fill out a username and password and you'll be redirected to the login -page. Try entering an incorrect username, or the correct username and -incorrect password. If you log in you'll get an error because there's -no ``index`` view to redirect to yet. - -Continue to :doc:`static`. +Continue with :ref:`tutorial-css`. diff --git a/docs/tutorial/testing.rst b/docs/tutorial/testing.rst new file mode 100644 index 00000000..34edd791 --- /dev/null +++ b/docs/tutorial/testing.rst @@ -0,0 +1,10 @@ +.. _tutorial-testing: + +Bonus: Testing the Application +============================== + +Now that you have finished the application and everything works as +expected, it's probably not a bad idea to add automated tests to simplify +modifications in the future. The application above is used as a basic +example of how to perform unittesting in the :ref:`testing` section of the +documentation. Go there to see how easy it is to test Flask applications. diff --git a/docs/tutorial/tests.rst b/docs/tutorial/tests.rst deleted file mode 100644 index 8958e773..00000000 --- a/docs/tutorial/tests.rst +++ /dev/null @@ -1,559 +0,0 @@ -.. currentmodule:: flask - -Test Coverage -============= - -Writing unit tests for your application lets you check that the code -you wrote works the way you expect. Flask provides a test client that -simulates requests to the application and returns the response data. - -You should test as much of your code as possible. Code in functions only -runs when the function is called, and code in branches, such as ``if`` -blocks, only runs when the condition is met. You want to make sure that -each function is tested with data that covers each branch. - -The closer you get to 100% coverage, the more comfortable you can be -that making a change won't unexpectedly change other behavior. However, -100% coverage doesn't guarantee that your application doesn't have bugs. -In particular, it doesn't test how the user interacts with the -application in the browser. Despite this, test coverage is an important -tool to use during development. - -.. note:: - This is being introduced late in the tutorial, but in your future - projects you should test as you develop. - -You'll use `pytest`_ and `coverage`_ to test and measure your code. -Install them both: - -.. code-block:: none - - $ pip install pytest coverage - -.. _pytest: https://pytest.readthedocs.io/ -.. _coverage: https://coverage.readthedocs.io/ - - -Setup and Fixtures ------------------- - -The test code is located in the ``tests`` directory. This directory is -*next to* the ``flaskr`` package, not inside it. The -``tests/conftest.py`` file contains setup functions called *fixtures* -that each test will use. Tests are in Python modules that start with -``test_``, and each test function in those modules also starts with -``test_``. - -Each test will create a new temporary database file and populate some -data that will be used in the tests. Write a SQL file to insert that -data. - -.. code-block:: sql - :caption: ``tests/data.sql`` - - INSERT INTO user (username, password) - VALUES - ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), - ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); - - INSERT INTO post (title, body, author_id, created) - VALUES - ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); - -The ``app`` fixture will call the factory and pass ``test_config`` to -configure the application and database for testing instead of using your -local development configuration. - -.. code-block:: python - :caption: ``tests/conftest.py`` - - import os - import tempfile - - import pytest - from flaskr import create_app - from flaskr.db import get_db, init_db - - with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f: - _data_sql = f.read().decode('utf8') - - - @pytest.fixture - def app(): - db_fd, db_path = tempfile.mkstemp() - - app = create_app({ - 'TESTING': True, - 'DATABASE': db_path, - }) - - with app.app_context(): - init_db() - get_db().executescript(_data_sql) - - yield app - - os.close(db_fd) - os.unlink(db_path) - - - @pytest.fixture - def client(app): - return app.test_client() - - - @pytest.fixture - def runner(app): - return app.test_cli_runner() - -:func:`tempfile.mkstemp` creates and opens a temporary file, returning -the file descriptor and the path to it. The ``DATABASE`` path is -overridden so it points to this temporary path instead of the instance -folder. After setting the path, the database tables are created and the -test data is inserted. After the test is over, the temporary file is -closed and removed. - -:data:`TESTING` tells Flask that the app is in test mode. Flask changes -some internal behavior so it's easier to test, and other extensions can -also use the flag to make testing them easier. - -The ``client`` fixture calls -:meth:`app.test_client() ` with the application -object created by the ``app`` fixture. Tests will use the client to make -requests to the application without running the server. - -The ``runner`` fixture is similar to ``client``. -:meth:`app.test_cli_runner() ` creates a runner -that can call the Click commands registered with the application. - -Pytest uses fixtures by matching their function names with the names -of arguments in the test functions. For example, the ``test_hello`` -function you'll write next takes a ``client`` argument. Pytest matches -that with the ``client`` fixture function, calls it, and passes the -returned value to the test function. - - -Factory -------- - -There's not much to test about the factory itself. Most of the code will -be executed for each test already, so if something fails the other tests -will notice. - -The only behavior that can change is passing test config. If config is -not passed, there should be some default configuration, otherwise the -configuration should be overridden. - -.. code-block:: python - :caption: ``tests/test_factory.py`` - - from flaskr import create_app - - - def test_config(): - assert not create_app().testing - assert create_app({'TESTING': True}).testing - - - def test_hello(client): - response = client.get('/hello') - assert response.data == b'Hello, World!' - -You added the ``hello`` route as an example when writing the factory at -the beginning of the tutorial. It returns "Hello, World!", so the test -checks that the response data matches. - - -Database --------- - -Within an application context, ``get_db`` should return the same -connection each time it's called. After the context, the connection -should be closed. - -.. code-block:: python - :caption: ``tests/test_db.py`` - - import sqlite3 - - import pytest - from flaskr.db import get_db - - - def test_get_close_db(app): - with app.app_context(): - db = get_db() - assert db is get_db() - - with pytest.raises(sqlite3.ProgrammingError) as e: - db.execute('SELECT 1') - - assert 'closed' in str(e.value) - -The ``init-db`` command should call the ``init_db`` function and output -a message. - -.. code-block:: python - :caption: ``tests/test_db.py`` - - def test_init_db_command(runner, monkeypatch): - class Recorder(object): - called = False - - def fake_init_db(): - Recorder.called = True - - monkeypatch.setattr('flaskr.db.init_db', fake_init_db) - result = runner.invoke(args=['init-db']) - assert 'Initialized' in result.output - assert Recorder.called - -This test uses Pytest's ``monkeypatch`` fixture to replace the -``init_db`` function with one that records that it's been called. The -``runner`` fixture you wrote above is used to call the ``init-db`` -command by name. - - -Authentication --------------- - -For most of the views, a user needs to be logged in. The easiest way to -do this in tests is to make a ``POST`` request to the ``login`` view -with the client. Rather than writing that out every time, you can write -a class with methods to do that, and use a fixture to pass it the client -for each test. - -.. code-block:: python - :caption: ``tests/conftest.py`` - - class AuthActions(object): - def __init__(self, client): - self._client = client - - def login(self, username='test', password='test'): - return self._client.post( - '/auth/login', - data={'username': username, 'password': password} - ) - - def logout(self): - return self._client.get('/auth/logout') - - - @pytest.fixture - def auth(client): - return AuthActions(client) - -With the ``auth`` fixture, you can call ``auth.login()`` in a test to -log in as the ``test`` user, which was inserted as part of the test -data in the ``app`` fixture. - -The ``register`` view should render successfully on ``GET``. On ``POST`` -with valid form data, it should redirect to the login URL and the user's -data should be in the database. Invalid data should display error -messages. - -.. code-block:: python - :caption: ``tests/test_auth.py`` - - import pytest - from flask import g, session - from flaskr.db import get_db - - - def test_register(client, app): - assert client.get('/auth/register').status_code == 200 - response = client.post( - '/auth/register', data={'username': 'a', 'password': 'a'} - ) - assert response.headers["Location"] == "/auth/login" - - with app.app_context(): - assert get_db().execute( - "SELECT * FROM user WHERE username = 'a'", - ).fetchone() is not None - - - @pytest.mark.parametrize(('username', 'password', 'message'), ( - ('', '', b'Username is required.'), - ('a', '', b'Password is required.'), - ('test', 'test', b'already registered'), - )) - def test_register_validate_input(client, username, password, message): - response = client.post( - '/auth/register', - data={'username': username, 'password': password} - ) - assert message in response.data - -:meth:`client.get() ` makes a ``GET`` request -and returns the :class:`Response` object returned by Flask. Similarly, -:meth:`client.post() ` makes a ``POST`` -request, converting the ``data`` dict into form data. - -To test that the page renders successfully, a simple request is made and -checked for a ``200 OK`` :attr:`~Response.status_code`. If -rendering failed, Flask would return a ``500 Internal Server Error`` -code. - -:attr:`~Response.headers` will have a ``Location`` header with the login -URL when the register view redirects to the login view. - -:attr:`~Response.data` contains the body of the response as bytes. If -you expect a certain value to render on the page, check that it's in -``data``. Bytes must be compared to bytes. If you want to compare text, -use :meth:`get_data(as_text=True) ` -instead. - -``pytest.mark.parametrize`` tells Pytest to run the same test function -with different arguments. You use it here to test different invalid -input and error messages without writing the same code three times. - -The tests for the ``login`` view are very similar to those for -``register``. Rather than testing the data in the database, -:data:`.session` should have ``user_id`` set after logging in. - -.. code-block:: python - :caption: ``tests/test_auth.py`` - - def test_login(client, auth): - assert client.get('/auth/login').status_code == 200 - response = auth.login() - assert response.headers["Location"] == "/" - - with client: - client.get('/') - assert session['user_id'] == 1 - assert g.user['username'] == 'test' - - - @pytest.mark.parametrize(('username', 'password', 'message'), ( - ('a', 'test', b'Incorrect username.'), - ('test', 'a', b'Incorrect password.'), - )) - def test_login_validate_input(auth, username, password, message): - response = auth.login(username, password) - assert message in response.data - -Using ``client`` in a ``with`` block allows accessing context variables -such as :data:`.session` after the response is returned. Normally, -accessing ``session`` outside of a request would raise an error. - -Testing ``logout`` is the opposite of ``login``. :data:`.session` should -not contain ``user_id`` after logging out. - -.. code-block:: python - :caption: ``tests/test_auth.py`` - - def test_logout(client, auth): - auth.login() - - with client: - auth.logout() - assert 'user_id' not in session - - -Blog ----- - -All the blog views use the ``auth`` fixture you wrote earlier. Call -``auth.login()`` and subsequent requests from the client will be logged -in as the ``test`` user. - -The ``index`` view should display information about the post that was -added with the test data. When logged in as the author, there should be -a link to edit the post. - -You can also test some more authentication behavior while testing the -``index`` view. When not logged in, each page shows links to log in or -register. When logged in, there's a link to log out. - -.. code-block:: python - :caption: ``tests/test_blog.py`` - - import pytest - from flaskr.db import get_db - - - def test_index(client, auth): - response = client.get('/') - assert b"Log In" in response.data - assert b"Register" in response.data - - auth.login() - response = client.get('/') - assert b'Log Out' in response.data - assert b'test title' in response.data - assert b'by test on 2018-01-01' in response.data - assert b'test\nbody' in response.data - assert b'href="/1/update"' in response.data - -A user must be logged in to access the ``create``, ``update``, and -``delete`` views. The logged in user must be the author of the post to -access ``update`` and ``delete``, otherwise a ``403 Forbidden`` status -is returned. If a ``post`` with the given ``id`` doesn't exist, -``update`` and ``delete`` should return ``404 Not Found``. - -.. code-block:: python - :caption: ``tests/test_blog.py`` - - @pytest.mark.parametrize('path', ( - '/create', - '/1/update', - '/1/delete', - )) - def test_login_required(client, path): - response = client.post(path) - assert response.headers["Location"] == "/auth/login" - - - def test_author_required(app, client, auth): - # change the post author to another user - with app.app_context(): - db = get_db() - db.execute('UPDATE post SET author_id = 2 WHERE id = 1') - db.commit() - - auth.login() - # current user can't modify other user's post - assert client.post('/1/update').status_code == 403 - assert client.post('/1/delete').status_code == 403 - # current user doesn't see edit link - assert b'href="/1/update"' not in client.get('/').data - - - @pytest.mark.parametrize('path', ( - '/2/update', - '/2/delete', - )) - def test_exists_required(client, auth, path): - auth.login() - assert client.post(path).status_code == 404 - -The ``create`` and ``update`` views should render and return a -``200 OK`` status for a ``GET`` request. When valid data is sent in a -``POST`` request, ``create`` should insert the new post data into the -database, and ``update`` should modify the existing data. Both pages -should show an error message on invalid data. - -.. code-block:: python - :caption: ``tests/test_blog.py`` - - def test_create(client, auth, app): - auth.login() - assert client.get('/create').status_code == 200 - client.post('/create', data={'title': 'created', 'body': ''}) - - with app.app_context(): - db = get_db() - count = db.execute('SELECT COUNT(id) FROM post').fetchone()[0] - assert count == 2 - - - def test_update(client, auth, app): - auth.login() - assert client.get('/1/update').status_code == 200 - client.post('/1/update', data={'title': 'updated', 'body': ''}) - - with app.app_context(): - db = get_db() - post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() - assert post['title'] == 'updated' - - - @pytest.mark.parametrize('path', ( - '/create', - '/1/update', - )) - def test_create_update_validate(client, auth, path): - auth.login() - response = client.post(path, data={'title': '', 'body': ''}) - assert b'Title is required.' in response.data - -The ``delete`` view should redirect to the index URL and the post should -no longer exist in the database. - -.. code-block:: python - :caption: ``tests/test_blog.py`` - - def test_delete(client, auth, app): - auth.login() - response = client.post('/1/delete') - assert response.headers["Location"] == "/" - - with app.app_context(): - db = get_db() - post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() - assert post is None - - -Running the Tests ------------------ - -Some extra configuration, which is not required but makes running tests with coverage -less verbose, can be added to the project's ``pyproject.toml`` file. - -.. code-block:: toml - :caption: ``pyproject.toml`` - - [tool.pytest.ini_options] - testpaths = ["tests"] - - [tool.coverage.run] - branch = true - source = ["flaskr"] - -To run the tests, use the ``pytest`` command. It will find and run all -the test functions you've written. - -.. code-block:: none - - $ pytest - - ========================= test session starts ========================== - platform linux -- Python 3.6.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0 - rootdir: /home/user/Projects/flask-tutorial - collected 23 items - - tests/test_auth.py ........ [ 34%] - tests/test_blog.py ............ [ 86%] - tests/test_db.py .. [ 95%] - tests/test_factory.py .. [100%] - - ====================== 24 passed in 0.64 seconds ======================= - -If any tests fail, pytest will show the error that was raised. You can -run ``pytest -v`` to get a list of each test function rather than dots. - -To measure the code coverage of your tests, use the ``coverage`` command -to run pytest instead of running it directly. - -.. code-block:: none - - $ coverage run -m pytest - -You can either view a simple coverage report in the terminal: - -.. code-block:: none - - $ coverage report - - Name Stmts Miss Branch BrPart Cover - ------------------------------------------------------ - flaskr/__init__.py 21 0 2 0 100% - flaskr/auth.py 54 0 22 0 100% - flaskr/blog.py 54 0 16 0 100% - flaskr/db.py 24 0 4 0 100% - ------------------------------------------------------ - TOTAL 153 0 44 0 100% - -An HTML report allows you to see which lines were covered in each file: - -.. code-block:: none - - $ coverage html - -This generates files in the ``htmlcov`` directory. Open -``htmlcov/index.html`` in your browser to see the report. - -Continue to :doc:`deploy`. diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index 6626628a..93bec3bf 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -1,305 +1,98 @@ -.. currentmodule:: flask +.. _tutorial-views: -Blueprints and Views -==================== +Step 5: The View Functions +========================== -A view function is the code you write to respond to requests to your -application. Flask uses patterns to match the incoming request URL to -the view that should handle it. The view returns data that Flask turns -into an outgoing response. Flask can also go the other direction and -generate a URL to a view based on its name and arguments. +Now that the database connections are working we can start writing the +view functions. We will need four of them: +Show Entries +------------ -Create a Blueprint ------------------- +This view shows all the entries stored in the database. It listens on the +root of the application and will select title and text from the database. +The one with the highest id (the newest entry) will be on top. The rows +returned from the cursor are tuples with the columns ordered like specified +in the select statement. This is good enough for small applications like +here, but you might want to convert them into a dict. If you are +interested in how to do that, check out the :ref:`easy-querying` example. -A :class:`Blueprint` is a way to organize a group of related views and -other code. Rather than registering views and other code directly with -an application, they are registered with a blueprint. Then the blueprint -is registered with the application when it is available in the factory -function. +The view function will pass the entries as dicts to the +`show_entries.html` template and return the rendered one:: -Flaskr will have two blueprints, one for authentication functions and -one for the blog posts functions. The code for each blueprint will go -in a separate module. Since the blog needs to know about authentication, -you'll write the authentication one first. + @app.route('/') + def show_entries(): + cur = g.db.execute('select title, text from entries order by id desc') + entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()] + return render_template('show_entries.html', entries=entries) -.. code-block:: python - :caption: ``flaskr/auth.py`` +Add New Entry +------------- - import functools +This view lets the user add new entries if they are logged in. This only +responds to `POST` requests, the actual form is shown on the +`show_entries` page. If everything worked out well we will +:func:`~flask.flash` an information message to the next request and +redirect back to the `show_entries` page:: - from flask import ( - Blueprint, flash, g, redirect, render_template, request, session, url_for - ) - from werkzeug.security import check_password_hash, generate_password_hash + @app.route('/add', methods=['POST']) + def add_entry(): + if not session.get('logged_in'): + abort(401) + g.db.execute('insert into entries (title, text) values (?, ?)', + [request.form['title'], request.form['text']]) + g.db.commit() + flash('New entry was successfully posted') + return redirect(url_for('show_entries')) - from flaskr.db import get_db +Note that we check that the user is logged in here (the `logged_in` key is +present in the session and `True`). - bp = Blueprint('auth', __name__, url_prefix='/auth') +.. admonition:: Security Note -This creates a :class:`Blueprint` named ``'auth'``. Like the application -object, the blueprint needs to know where it's defined, so ``__name__`` -is passed as the second argument. The ``url_prefix`` will be prepended -to all the URLs associated with the blueprint. + Be sure to use question marks when building SQL statements, as done in the + example above. Otherwise, your app will be vulnerable to SQL injection when + you use string formatting to build SQL statements. + See :ref:`sqlite3` for more. -Import and register the blueprint from the factory using -:meth:`app.register_blueprint() `. Place the -new code at the end of the factory function before returning the app. +Login and Logout +---------------- -.. code-block:: python - :caption: ``flaskr/__init__.py`` +These functions are used to sign the user in and out. Login checks the +username and password against the ones from the configuration and sets the +`logged_in` key in the session. If the user logged in successfully, that +key is set to `True`, and the user is redirected back to the `show_entries` +page. In addition, a message is flashed that informs the user that he or +she was logged in successfully. If an error occurred, the template is +notified about that, and the user is asked again:: - def create_app(): - app = ... - # existing code omitted - - from . import auth - app.register_blueprint(auth.bp) - - return app - -The authentication blueprint will have views to register new users and -to log in and log out. - - -The First View: Register ------------------------- - -When the user visits the ``/auth/register`` URL, the ``register`` view -will return `HTML`_ with a form for them to fill out. When they submit -the form, it will validate their input and either show the form again -with an error message or create the new user and go to the login page. - -.. _HTML: https://developer.mozilla.org/docs/Web/HTML - -For now you will just write the view code. On the next page, you'll -write templates to generate the HTML form. - -.. code-block:: python - :caption: ``flaskr/auth.py`` - - @bp.route('/register', methods=('GET', 'POST')) - def register(): - if request.method == 'POST': - username = request.form['username'] - password = request.form['password'] - db = get_db() - error = None - - if not username: - error = 'Username is required.' - elif not password: - error = 'Password is required.' - - if error is None: - try: - db.execute( - "INSERT INTO user (username, password) VALUES (?, ?)", - (username, generate_password_hash(password)), - ) - db.commit() - except db.IntegrityError: - error = f"User {username} is already registered." - else: - return redirect(url_for("auth.login")) - - flash(error) - - return render_template('auth/register.html') - -Here's what the ``register`` view function is doing: - -#. :meth:`@bp.route ` associates the URL ``/register`` - with the ``register`` view function. When Flask receives a request - to ``/auth/register``, it will call the ``register`` view and use - the return value as the response. - -#. If the user submitted the form, - :attr:`request.method ` will be ``'POST'``. In this - case, start validating the input. - -#. :attr:`request.form ` is a special type of - :class:`dict` mapping submitted form keys and values. The user will - input their ``username`` and ``password``. - -#. Validate that ``username`` and ``password`` are not empty. - -#. If validation succeeds, insert the new user data into the database. - - - :meth:`db.execute ` takes a SQL - query with ``?`` placeholders for any user input, and a tuple of - values to replace the placeholders with. The database library - will take care of escaping the values so you are not vulnerable - to a *SQL injection attack*. - - - For security, passwords should never be stored in the database - directly. Instead, - :func:`~werkzeug.security.generate_password_hash` is used to - securely hash the password, and that hash is stored. Since this - query modifies data, - :meth:`db.commit() ` needs to be - called afterwards to save the changes. - - - An :exc:`sqlite3.IntegrityError` will occur if the username - already exists, which should be shown to the user as another - validation error. - -#. After storing the user, they are redirected to the login page. - :func:`url_for` generates the URL for the login view based on its - name. This is preferable to writing the URL directly as it allows - you to change the URL later without changing all code that links to - it. :func:`redirect` generates a redirect response to the generated - URL. - -#. If validation fails, the error is shown to the user. :func:`flash` - stores messages that can be retrieved when rendering the template. - -#. When the user initially navigates to ``auth/register``, or - there was a validation error, an HTML page with the registration - form should be shown. :func:`render_template` will render a template - containing the HTML, which you'll write in the next step of the - tutorial. - - -Login ------ - -This view follows the same pattern as the ``register`` view above. - -.. code-block:: python - :caption: ``flaskr/auth.py`` - - @bp.route('/login', methods=('GET', 'POST')) + @app.route('/login', methods=['GET', 'POST']) def login(): + error = None if request.method == 'POST': - username = request.form['username'] - password = request.form['password'] - db = get_db() - error = None - user = db.execute( - 'SELECT * FROM user WHERE username = ?', (username,) - ).fetchone() + if request.form['username'] != app.config['USERNAME']: + error = 'Invalid username' + elif request.form['password'] != app.config['PASSWORD']: + error = 'Invalid password' + else: + session['logged_in'] = True + flash('You were logged in') + return redirect(url_for('show_entries')) + return render_template('login.html', error=error) - if user is None: - error = 'Incorrect username.' - elif not check_password_hash(user['password'], password): - error = 'Incorrect password.' +The logout function, on the other hand, removes that key from the session +again. We use a neat trick here: if you use the :meth:`~dict.pop` method +of the dict and pass a second parameter to it (the default), the method +will delete the key from the dictionary if present or do nothing when that +key is not in there. This is helpful because now we don't have to check +if the user was logged in. - if error is None: - session.clear() - session['user_id'] = user['id'] - return redirect(url_for('index')) +:: - flash(error) - - return render_template('auth/login.html') - -There are a few differences from the ``register`` view: - -#. The user is queried first and stored in a variable for later use. - - :meth:`~sqlite3.Cursor.fetchone` returns one row from the query. - If the query returned no results, it returns ``None``. Later, - :meth:`~sqlite3.Cursor.fetchall` will be used, which returns a list - of all results. - -#. :func:`~werkzeug.security.check_password_hash` hashes the submitted - password in the same way as the stored hash and securely compares - them. If they match, the password is valid. - -#. :data:`.session` is a :class:`dict` that stores data across requests. - When validation succeeds, the user's ``id`` is stored in a new - session. The data is stored in a *cookie* that is sent to the - browser, and the browser then sends it back with subsequent requests. - Flask securely *signs* the data so that it can't be tampered with. - -Now that the user's ``id`` is stored in the :data:`.session`, it will be -available on subsequent requests. At the beginning of each request, if -a user is logged in their information should be loaded and made -available to other views. - -.. code-block:: python - :caption: ``flaskr/auth.py`` - - @bp.before_app_request - def load_logged_in_user(): - user_id = session.get('user_id') - - if user_id is None: - g.user = None - else: - g.user = get_db().execute( - 'SELECT * FROM user WHERE id = ?', (user_id,) - ).fetchone() - -:meth:`bp.before_app_request() ` registers -a function that runs before the view function, no matter what URL is -requested. ``load_logged_in_user`` checks if a user id is stored in the -:data:`.session` and gets that user's data from the database, storing it -on :data:`g.user `, which lasts for the length of the request. If -there is no user id, or if the id doesn't exist, ``g.user`` will be -``None``. - - -Logout ------- - -To log out, you need to remove the user id from the :data:`.session`. -Then ``load_logged_in_user`` won't load a user on subsequent requests. - -.. code-block:: python - :caption: ``flaskr/auth.py`` - - @bp.route('/logout') + @app.route('/logout') def logout(): - session.clear() - return redirect(url_for('index')) + session.pop('logged_in', None) + flash('You were logged out') + return redirect(url_for('show_entries')) - -Require Authentication in Other Views -------------------------------------- - -Creating, editing, and deleting blog posts will require a user to be -logged in. A *decorator* can be used to check this for each view it's -applied to. - -.. code-block:: python - :caption: ``flaskr/auth.py`` - - def login_required(view): - @functools.wraps(view) - def wrapped_view(**kwargs): - if g.user is None: - return redirect(url_for('auth.login')) - - return view(**kwargs) - - return wrapped_view - -This decorator returns a new view function that wraps the original view -it's applied to. The new function checks if a user is loaded and -redirects to the login page otherwise. If a user is loaded the original -view is called and continues normally. You'll use this decorator when -writing the blog views. - -Endpoints and URLs ------------------- - -The :func:`url_for` function generates the URL to a view based on a name -and arguments. The name associated with a view is also called the -*endpoint*, and by default it's the same as the name of the view -function. - -For example, the ``hello()`` view that was added to the app -factory earlier in the tutorial has the name ``'hello'`` and can be -linked to with ``url_for('hello')``. If it took an argument, which -you'll see later, it would be linked to using -``url_for('hello', who='World')``. - -When using a blueprint, the name of the blueprint is prepended to the -name of the function, so the endpoint for the ``login`` function you -wrote above is ``'auth.login'`` because you added it to the ``'auth'`` -blueprint. - -Continue to :doc:`templates`. +Continue with :ref:`tutorial-templates`. diff --git a/docs/unicode.rst b/docs/unicode.rst new file mode 100644 index 00000000..413ea84d --- /dev/null +++ b/docs/unicode.rst @@ -0,0 +1,107 @@ +Unicode in Flask +================ + +Flask like Jinja2 and Werkzeug is totally Unicode based when it comes to +text. Not only these libraries, also the majority of web related Python +libraries that deal with text. If you don't know Unicode so far, you +should probably read `The Absolute Minimum Every Software Developer +Absolutely, Positively Must Know About Unicode and Character Sets +`_. This part of the +documentation just tries to cover the very basics so that you have a +pleasant experience with Unicode related things. + +Automatic Conversion +-------------------- + +Flask has a few assumptions about your application (which you can change +of course) that give you basic and painless Unicode support: + +- the encoding for text on your website is UTF-8 +- internally you will always use Unicode exclusively for text except + for literal strings with only ASCII character points. +- encoding and decoding happens whenever you are talking over a protocol + that requires bytes to be transmitted. + +So what does this mean to you? + +HTTP is based on bytes. Not only the protocol, also the system used to +address documents on servers (so called URIs or URLs). However HTML which +is usually transmitted on top of HTTP supports a large variety of +character sets and which ones are used, are transmitted in an HTTP header. +To not make this too complex Flask just assumes that if you are sending +Unicode out you want it to be UTF-8 encoded. Flask will do the encoding +and setting of the appropriate headers for you. + +The same is true if you are talking to databases with the help of +SQLAlchemy or a similar ORM system. Some databases have a protocol that +already transmits Unicode and if they do not, SQLAlchemy or your other ORM +should take care of that. + +The Golden Rule +--------------- + +So the rule of thumb: if you are not dealing with binary data, work with +Unicode. What does working with Unicode in Python 2.x mean? + +- as long as you are using ASCII charpoints only (basically numbers, + some special characters of latin letters without umlauts or anything + fancy) you can use regular string literals (``'Hello World'``). +- if you need anything else than ASCII in a string you have to mark + this string as Unicode string by prefixing it with a lowercase `u`. + (like ``u'Hänsel und Gretel'``) +- if you are using non-Unicode characters in your Python files you have + to tell Python which encoding your file uses. Again, I recommend + UTF-8 for this purpose. To tell the interpreter your encoding you can + put the ``# -*- coding: utf-8 -*-`` into the first or second line of + your Python source file. +- Jinja is configured to decode the template files from UTF-8. So make + sure to tell your editor to save the file as UTF-8 there as well. + +Encoding and Decoding Yourself +------------------------------ + +If you are talking with a filesystem or something that is not really based +on Unicode you will have to ensure that you decode properly when working +with Unicode interface. So for example if you want to load a file on the +filesystem and embed it into a Jinja2 template you will have to decode it +from the encoding of that file. Here the old problem that text files do +not specify their encoding comes into play. So do yourself a favour and +limit yourself to UTF-8 for text files as well. + +Anyways. To load such a file with Unicode you can use the built-in +:meth:`str.decode` method:: + + def read_file(filename, charset='utf-8'): + with open(filename, 'r') as f: + return f.read().decode(charset) + +To go from Unicode into a specific charset such as UTF-8 you can use the +:meth:`unicode.encode` method:: + + def write_file(filename, contents, charset='utf-8'): + with open(filename, 'w') as f: + f.write(contents.encode(charset)) + +Configuring Editors +------------------- + +Most editors save as UTF-8 by default nowadays but in case your editor is +not configured to do this you have to change it. Here some common ways to +set your editor to store as UTF-8: + +- Vim: put ``set enc=utf-8`` to your ``.vimrc`` file. + +- Emacs: either use an encoding cookie or put this into your ``.emacs`` + file:: + + (prefer-coding-system 'utf-8) + (setq default-buffer-file-coding-system 'utf-8) + +- Notepad++: + + 1. Go to *Settings -> Preferences ...* + 2. Select the "New Document/Default Directory" tab + 3. Select "UTF-8 without BOM" as encoding + + It is also recommended to use the Unix newline format, you can select + it in the same panel but this is not a requirement. diff --git a/docs/upgrading.rst b/docs/upgrading.rst new file mode 100644 index 00000000..0ba46c13 --- /dev/null +++ b/docs/upgrading.rst @@ -0,0 +1,324 @@ +Upgrading to Newer Releases +=========================== + +Flask itself is changing like any software is changing over time. Most of +the changes are the nice kind, the kind where you don't have to change +anything in your code to profit from a new release. + +However every once in a while there are changes that do require some +changes in your code or there are changes that make it possible for you to +improve your own code quality by taking advantage of new features in +Flask. + +This section of the documentation enumerates all the changes in Flask from +release to release and how you can change your code to have a painless +updating experience. + +If you want to use the `easy_install` command to upgrade your Flask +installation, make sure to pass it the ``-U`` parameter:: + + $ easy_install -U Flask + +Version 0.8 +----------- + +Flask introduced a new session interface system. We also noticed that +there was a naming collision between `flask.session` the module that +implements sessions and :data:`flask.session` which is the global session +object. With that introduction we moved the implementation details for +the session system into a new module called :mod:`flask.sessions`. If you +used the previously undocumented session support we urge you to upgrade. + +If invalid JSON data was submitted Flask will now raise a +:exc:`~werkzeug.exceptions.BadRequest` exception instead of letting the +default :exc:`ValueError` bubble up. This has the advantage that you no +longer have to handle that error to avoid an internal server error showing +up for the user. If you were catching this down explicitly in the past +as `ValueError` you will need to change this. + +Due to a bug in the test client Flask 0.7 did not trigger teardown +handlers when the test client was used in a with statement. This was +since fixed but might require some changes in your testsuites if you +relied on this behavior. + +Version 0.7 +----------- + +In Flask 0.7 we cleaned up the code base internally a lot and did some +backwards incompatible changes that make it easier to implement larger +applications with Flask. Because we want to make upgrading as easy as +possible we tried to counter the problems arising from these changes by +providing a script that can ease the transition. + +The script scans your whole application and generates an unified diff with +changes it assumes are safe to apply. However as this is an automated +tool it won't be able to find all use cases and it might miss some. We +internally spread a lot of deprecation warnings all over the place to make +it easy to find pieces of code that it was unable to upgrade. + +We strongly recommend that you hand review the generated patchfile and +only apply the chunks that look good. + +If you are using git as version control system for your project we +recommend applying the patch with ``path -p1 < patchfile.diff`` and then +using the interactive commit feature to only apply the chunks that look +good. + +To apply the upgrade script do the following: + +1. Download the script: `flask-07-upgrade.py + `_ +2. Run it in the directory of your application:: + + python flask-07-upgrade.py > patchfile.diff + +3. Review the generated patchfile. +4. Apply the patch:: + + patch -p1 < patchfile.diff + +5. If you were using per-module template folders you need to move some + templates around. Previously if you had a folder named ``templates`` + next to a blueprint named ``admin`` the implicit template path + automatically was ``admin/index.html`` for a template file called + ``templates/index.html``. This no longer is the case. Now you need + to name the template ``templates/admin/index.html``. The tool will + not detect this so you will have to do that on your own. + +Please note that deprecation warnings are disabled by default starting +with Python 2.7. In order to see the deprecation warnings that might be +emitted you have to enabled them with the :mod:`warnings` module. + +If you are working with windows and you lack the `patch` command line +utility you can get it as part of various Unix runtime environments for +windows including cygwin, msysgit or ming32. Also source control systems +like svn, hg or git have builtin support for applying unified diffs as +generated by the tool. Check the manual of your version control system +for more information. + +Bug in Request Locals +````````````````````` + +Due to a bug in earlier implementations the request local proxies now +raise a :exc:`RuntimeError` instead of an :exc:`AttributeError` when they +are unbound. If you caught these exceptions with :exc:`AttributeError` +before, you should catch them with :exc:`RuntimeError` now. + +Additionally the :func:`~flask.send_file` function is now issuing +deprecation warnings if you depend on functionality that will be removed +in Flask 1.0. Previously it was possible to use etags and mimetypes +when file objects were passed. This was unreliable and caused issues +for a few setups. If you get a deprecation warning, make sure to +update your application to work with either filenames there or disable +etag attaching and attach them yourself. + +Old code:: + + return send_file(my_file_object) + return send_file(my_file_object) + +New code:: + + return send_file(my_file_object, add_etags=False) + +.. _upgrading-to-new-teardown-handling: + +Upgrading to new Teardown Handling +`````````````````````````````````` + +We streamlined the behavior of the callbacks for request handling. For +things that modify the response the :meth:`~flask.Flask.after_request` +decorators continue to work as expected, but for things that absolutely +must happen at the end of request we introduced the new +:meth:`~flask.Flask.teardown_request` decorator. Unfortunately that +change also made after-request work differently under error conditions. +It's not consistently skipped if exceptions happen whereas previously it +might have been called twice to ensure it is executed at the end of the +request. + +If you have database connection code that looks like this:: + + @app.after_request + def after_request(response): + g.db.close() + return response + +You are now encouraged to use this instead:: + + @app.teardown_request + def after_request(exception): + if hasattr(g, 'db'): + g.db.close() + +On the upside this change greatly improves the internal code flow and +makes it easier to customize the dispatching and error handling. This +makes it now a lot easier to write unit tests as you can prevent closing +down of database connections for a while. You can take advantage of the +fact that the teardown callbacks are called when the response context is +removed from the stack so a test can query the database after request +handling:: + + with app.test_client() as client: + resp = client.get('/') + # g.db is still bound if there is such a thing + + # and here it's gone + +Manual Error Handler Attaching +`````````````````````````````` + +While it is still possible to attach error handlers to +:attr:`Flask.error_handlers` it's discouraged to do so and in fact +deprecated. In generaly we no longer recommend custom error handler +attaching via assignments to the underlying dictionary due to the more +complex internal handling to support arbitrary exception classes and +blueprints. See :meth:`Flask.errorhandler` for more information. + +The proper upgrade is to change this:: + + app.error_handlers[403] = handle_error + +Into this:: + + app.register_error_handler(403, handle_error) + +Alternatively you should just attach the function with a decorator:: + + @app.errorhandler(403) + def handle_error(e): + ... + +(Note that :meth:`register_error_handler` is new in Flask 0.7) + +Blueprint Support +````````````````` + +Blueprints replace the previous concept of “Modules” in Flask. They +provide better semantics for various features and work better with large +applications. The update script provided should be able to upgrade your +applications automatically, but there might be some cases where it fails +to upgrade. What changed? + +- Blueprints need explicit names. Modules had an automatic name + guesssing scheme where the shortname for the module was taken from the + last part of the import module. The upgrade script tries to guess + that name but it might fail as this information could change at + runtime. +- Blueprints have an inverse behavior for :meth:`url_for`. Previously + ``.foo`` told :meth:`url_for` that it should look for the endpoint + `foo` on the application. Now it means “relative to current module”. + The script will inverse all calls to :meth:`url_for` automatically for + you. It will do this in a very eager way so you might end up with + some unnecessary leading dots in your code if you're not using + modules. +- Blueprints do not automatically provide static folders. They will + also no longer automatically export templates from a folder called + `templates` next to their location however but it can be enabled from + the constructor. Same with static files: if you want to continue + serving static files you need to tell the constructor explicitly the + path to the static folder (which can be relative to the blueprint's + module path). +- Rendering templates was simplified. Now the blueprints can provide + template folders which are added to a general template searchpath. + This means that you need to add another subfolder with the blueprint's + name into that folder if you want ``blueprintname/template.html`` as + the template name. + +If you continue to use the `Module` object which is deprecated, Flask will +restore the previous behavior as good as possible. However we strongly +recommend upgrading to the new blueprints as they provide a lot of useful +improvement such as the ability to attach a blueprint multiple times, +blueprint specific error handlers and a lot more. + + +Version 0.6 +----------- + +Flask 0.6 comes with a backwards incompatible change which affects the +order of after-request handlers. Previously they were called in the order +of the registration, now they are called in reverse order. This change +was made so that Flask behaves more like people expected it to work and +how other systems handle request pre- and postprocessing. If you +depend on the order of execution of post-request functions, be sure to +change the order. + +Another change that breaks backwards compatibility is that context +processors will no longer override values passed directly to the template +rendering function. If for example `request` is as variable passed +directly to the template, the default context processor will not override +it with the current request object. This makes it easier to extend +context processors later to inject additional variables without breaking +existing template not expecting them. + +Version 0.5 +----------- + +Flask 0.5 is the first release that comes as a Python package instead of a +single module. There were a couple of internal refactoring so if you +depend on undocumented internal details you probably have to adapt the +imports. + +The following changes may be relevant to your application: + +- autoescaping no longer happens for all templates. Instead it is + configured to only happen on files ending with ``.html``, ``.htm``, + ``.xml`` and ``.xhtml``. If you have templates with different + extensions you should override the + :meth:`~flask.Flask.select_jinja_autoescape` method. +- Flask no longer supports zipped applications in this release. This + functionality might come back in future releases if there is demand + for this feature. Removing support for this makes the Flask internal + code easier to understand and fixes a couple of small issues that make + debugging harder than necessary. +- The `create_jinja_loader` function is gone. If you want to customize + the Jinja loader now, use the + :meth:`~flask.Flask.create_jinja_environment` method instead. + +Version 0.4 +----------- + +For application developers there are no changes that require changes in +your code. In case you are developing on a Flask extension however, and +that extension has a unittest-mode you might want to link the activation +of that mode to the new ``TESTING`` flag. + +Version 0.3 +----------- + +Flask 0.3 introduces configuration support and logging as well as +categories for flashing messages. All these are features that are 100% +backwards compatible but you might want to take advantage of them. + +Configuration Support +````````````````````` + +The configuration support makes it easier to write any kind of application +that requires some sort of configuration. (Which most likely is the case +for any application out there). + +If you previously had code like this:: + + app.debug = DEBUG + app.secret_key = SECRET_KEY + +You no longer have to do that, instead you can just load a configuration +into the config object. How this works is outlined in :ref:`config`. + +Logging Integration +``````````````````` + +Flask now configures a logger for you with some basic and useful defaults. +If you run your application in production and want to profit from +automatic error logging, you might be interested in attaching a proper log +handler. Also you can start logging warnings and errors into the logger +when appropriately. For more information on that, read +:ref:`application-errors`. + +Categories for Flash Messages +````````````````````````````` + +Flash messages can now have categories attached. This makes it possible +to render errors, warnings or regular messages differently for example. +This is an opt-in feature because it requires some rethinking in the code. + +Read all about that in the :ref:`message-flashing-pattern` pattern. diff --git a/docs/views.rst b/docs/views.rst index f2210270..441620a6 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -1,324 +1,227 @@ -Class-based Views -================= +.. _views: -.. currentmodule:: flask.views +Pluggable Views +=============== -This page introduces using the :class:`View` and :class:`MethodView` -classes to write class-based views. +.. versionadded:: 0.7 -A class-based view is a class that acts as a view function. Because it -is a class, different instances of the class can be created with -different arguments, to change the behavior of the view. This is also -known as generic, reusable, or pluggable views. +Flask 0.7 introduces pluggable views inspired by the generic views from +Django which are based on classes instead of functions. The main +intention is that you can replace parts of the implementations and this +way have customizable pluggable views. -An example of where this is useful is defining a class that creates an -API based on the database model it is initialized with. +Basic Principle +--------------- -For more complex API behavior and customization, look into the various -API extensions for Flask. +Consider you have a function that loads a list of objects from the +database and renders into a template:: - -Basic Reusable View -------------------- - -Let's walk through an example converting a view function to a view -class. We start with a view function that queries a list of users then -renders a template to show the list. - -.. code-block:: python - - @app.route("/users/") - def user_list(): + @app.route('/users/') + def show_users(page): users = User.query.all() - return render_template("users.html", users=users) + return render_template('users.html', users=users) -This works for the user model, but let's say you also had more models -that needed list pages. You'd need to write another view function for -each model, even though the only thing that would change is the model -and template name. +This is simple and flexible, but if you want to provide this view in a +generic fashion that can be adapted to other models and templates as well +you might want more flexibility. This is where pluggable class based +views come into place. As the first step to convert this into a class +based view you would do this:: -Instead, you can write a :class:`View` subclass that will query a model -and render a template. As the first step, we'll convert the view to a -class without any customization. - -.. code-block:: python from flask.views import View - class UserList(View): + class ShowUsers(View): + def dispatch_request(self): users = User.query.all() - return render_template("users.html", objects=users) + return render_template('users.html', objects=users) - app.add_url_rule("/users/", view_func=UserList.as_view("user_list")) + app.add_url_rule('/users/', ShowUsers.as_view('show_users')) -The :meth:`View.dispatch_request` method is the equivalent of the view -function. Calling :meth:`View.as_view` method will create a view -function that can be registered on the app with its -:meth:`~flask.Flask.add_url_rule` method. The first argument to -``as_view`` is the name to use to refer to the view with -:func:`~flask.url_for`. +As you can see what you have to do is to create a subclass of +:class:`flask.views.View` and implement +:meth:`~flask.views.View.dispatch_request`. Then we have to convert that +class into an actual view function by using the +:meth:`~flask.views.View.as_view` class method. The string you pass to +that function is the name of the endpoint that view will then have. But +this by itself is not helpful, so let's refactor the code a bit:: -.. note:: - - You can't decorate the class with ``@app.route()`` the way you'd - do with a basic view function. - -Next, we need to be able to register the same view class for different -models and templates, to make it more useful than the original function. -The class will take two arguments, the model and template, and store -them on ``self``. Then ``dispatch_request`` can reference these instead -of hard-coded values. - -.. code-block:: python + + from flask.views import View class ListView(View): - def __init__(self, model, template): - self.model = model - self.template = template + + def get_template_name(self): + raise NotImplementedError() + + def render_template(self, context): + return render_template(self.get_template_name(), **context) def dispatch_request(self): - items = self.model.query.all() - return render_template(self.template, items=items) + context = {'objects': self.get_objects()} + return self.render_template(context) -Remember, we create the view function with ``View.as_view()`` instead of -creating the class directly. Any extra arguments passed to ``as_view`` -are then passed when creating the class. Now we can register the same -view to handle multiple models. + class UserView(ListView): -.. code-block:: python + def get_template_name(self): + return 'users.html' - app.add_url_rule( - "/users/", - view_func=ListView.as_view("user_list", User, "users.html"), - ) - app.add_url_rule( - "/stories/", - view_func=ListView.as_view("story_list", Story, "stories.html"), - ) + def get_objects(self): + return User.query.all() +This of course is not that helpful for such a small example, but it's good +enough to explain the basic principle. When you have a class based view +the question comes up what `self` points to. The way this works is that +whenever the request is dispatched a new instance of the class is created +and the :meth:`~flask.views.View.dispatch_request` method is called with +the parameters from the URL rule. The class itself is instanciated with +the parameters passed to the :meth:`~flask.views.View.as_view` function. +For instance you can write a class like this:: -URL Variables -------------- - -Any variables captured by the URL are passed as keyword arguments to the -``dispatch_request`` method, as they would be for a regular view -function. - -.. code-block:: python - - class DetailView(View): - def __init__(self, model): - self.model = model - self.template = f"{model.__name__.lower()}/detail.html" - - def dispatch_request(self, id) - item = self.model.query.get_or_404(id) - return render_template(self.template, item=item) - - app.add_url_rule( - "/users/", - view_func=DetailView.as_view("user_detail", User) - ) - - -View Lifetime and ``self`` --------------------------- - -By default, a new instance of the view class is created every time a -request is handled. This means that it is safe to write other data to -``self`` during the request, since the next request will not see it, -unlike other forms of global state. - -However, if your view class needs to do a lot of complex initialization, -doing it for every request is unnecessary and can be inefficient. To -avoid this, set :attr:`View.init_every_request` to ``False``, which will -only create one instance of the class and use it for every request. In -this case, writing to ``self`` is not safe. If you need to store data -during the request, use :data:`~flask.g` instead. - -In the ``ListView`` example, nothing writes to ``self`` during the -request, so it is more efficient to create a single instance. - -.. code-block:: python - - class ListView(View): - init_every_request = False - - def __init__(self, model, template): - self.model = model - self.template = template - + class RenderTemplateView(View): + def __init__(self, template_name): + self.template_name = template_name def dispatch_request(self): - items = self.model.query.all() - return render_template(self.template, items=items) + return render_template(self.template_name) -Different instances will still be created each for each ``as_view`` -call, but not for each request to those views. - - -View Decorators ---------------- - -The view class itself is not the view function. View decorators need to -be applied to the view function returned by ``as_view``, not the class -itself. Set :attr:`View.decorators` to a list of decorators to apply. - -.. code-block:: python - - class UserList(View): - decorators = [cache(minutes=2), login_required] - - app.add_url_rule('/users/', view_func=UserList.as_view()) - -If you didn't set ``decorators``, you could apply them manually instead. -This is equivalent to: - -.. code-block:: python - - view = UserList.as_view("users_list") - view = cache(minutes=2)(view) - view = login_required(view) - app.add_url_rule('/users/', view_func=view) - -Keep in mind that order matters. If you're used to ``@decorator`` style, -this is equivalent to: - -.. code-block:: python - - @app.route("/users/") - @login_required - @cache(minutes=2) - def user_list(): - ... +And then you can register it like this:: + app.add_url_rule('/about', view_func=RenderTemplateView.as_view( + 'about_page', template_name='about.html')) Method Hints ------------ -A common pattern is to register a view with ``methods=["GET", "POST"]``, -then check ``request.method == "POST"`` to decide what to do. Setting -:attr:`View.methods` is equivalent to passing the list of methods to -``add_url_rule`` or ``route``. - -.. code-block:: python +Pluggable views are attached to the application like a regular function by +either using :func:`~flask.Flask.route` or better +:meth:`~flask.Flask.add_url_rule`. That however also means that you would +have to provide the names of the HTTP methods the view supports when you +attach this. In order to move that information to the class you can +provide a :attr:`~flask.views.View.methods` attribute that has this +information:: class MyView(View): - methods = ["GET", "POST"] + methods = ['GET', 'POST'] def dispatch_request(self): - if request.method == "POST": + if request.method == 'POST': ... ... - app.add_url_rule('/my-view', view_func=MyView.as_view('my-view')) + app.add_url_rule('/myview', view_func=MyView.as_view('myview')) -This is equivalent to the following, except further subclasses can -inherit or change the methods. +Method Based Dispatching +------------------------ -.. code-block:: python - - app.add_url_rule( - "/my-view", - view_func=MyView.as_view("my-view"), - methods=["GET", "POST"], - ) - - -Method Dispatching and APIs ---------------------------- - -For APIs it can be helpful to use a different function for each HTTP -method. :class:`MethodView` extends the basic :class:`View` to dispatch -to different methods of the class based on the request method. Each HTTP -method maps to a method of the class with the same (lowercase) name. - -:class:`MethodView` automatically sets :attr:`View.methods` based on the -methods defined by the class. It even knows how to handle subclasses -that override or define other methods. - -We can make a generic ``ItemAPI`` class that provides get (detail), -patch (edit), and delete methods for a given model. A ``GroupAPI`` can -provide get (list) and post (create) methods. - -.. code-block:: python +For RESTful APIs it's especially helpful to execute a different function +for each HTTP method. With the :class:`flask.views.MethodView` you can +easily do that. Each HTTP method maps to a function with the same name +(just in lowercase):: from flask.views import MethodView - class ItemAPI(MethodView): - init_every_request = False - - def __init__(self, model): - self.model = model - self.validator = generate_validator(model) - - def _get_item(self, id): - return self.model.query.get_or_404(id) - - def get(self, id): - item = self._get_item(id) - return jsonify(item.to_json()) - - def patch(self, id): - item = self._get_item(id) - errors = self.validator.validate(item, request.json) - - if errors: - return jsonify(errors), 400 - - item.update_from_json(request.json) - db.session.commit() - return jsonify(item.to_json()) - - def delete(self, id): - item = self._get_item(id) - db.session.delete(item) - db.session.commit() - return "", 204 - - class GroupAPI(MethodView): - init_every_request = False - - def __init__(self, model): - self.model = model - self.validator = generate_validator(model, create=True) + class UserAPI(MethodView): def get(self): - items = self.model.query.all() - return jsonify([item.to_json() for item in items]) + users = User.query.all() + ... def post(self): - errors = self.validator.validate(request.json) + user = User.from_form_data(request.form) + ... - if errors: - return jsonify(errors), 400 + app.add_url_rule('/users/', view_func=UserAPI.as_view('users')) - db.session.add(self.model.from_json(request.json)) - db.session.commit() - return jsonify(item.to_json()) +That way you also don't have to provide the +:attr:`~flask.views.View.methods` attribute. It's automatically set based +on the methods defined in the class. - def register_api(app, model, name): - item = ItemAPI.as_view(f"{name}-item", model) - group = GroupAPI.as_view(f"{name}-group", model) - app.add_url_rule(f"/{name}/", view_func=item) - app.add_url_rule(f"/{name}/", view_func=group) +Decorating Views +---------------- - register_api(app, User, "users") - register_api(app, Story, "stories") +Since the view class itself is not the view function that is added to the +routing system it does not make much sense to decorate the class itself. +Instead you either have to decorate the return value of +:meth:`~flask.views.View.as_view` by hand:: -This produces the following views, a standard REST API! + view = rate_limited(UserAPI.as_view('users')) + app.add_url_rule('/users/', view_func=view) -================= ========== =================== -URL Method Description ------------------ ---------- ------------------- -``/users/`` ``GET`` List all users -``/users/`` ``POST`` Create a new user -``/users/`` ``GET`` Show a single user -``/users/`` ``PATCH`` Update a user -``/users/`` ``DELETE`` Delete a user -``/stories/`` ``GET`` List all stories -``/stories/`` ``POST`` Create a new story -``/stories/`` ``GET`` Show a single story -``/stories/`` ``PATCH`` Update a story -``/stories/`` ``DELETE`` Delete a story -================= ========== =================== +Starting with Flask 0.8 there is also an alternative way where you can +specify a list of decorators to apply in the class declaration:: + + class UserAPI(MethodView): + decorators = [rate_limited] + +Due to the implicit self from the caller's perspective you cannot use +regular view decorators on the individual methods of the view however, +keep this in mind. + +Method Views for APIs +--------------------- + +Web APIs are often working very closely with HTTP verbs so it makes a lot +of sense to implement such an API based on the +:class:`~flask.views.MethodView`. That said, you will notice that the API +will require different URL rules that go to the same method view most of +the time. For instance consider that you are exposing a user object on +the web: + +=============== =============== ====================================== +URL Method Description +--------------- --------------- -------------------------------------- +``/users/`` ``GET`` Gives a list of all users +``/users/`` ``POST`` Creates a new user +``/users/`` ``GET`` Shows a single user +``/users/`` ``PUT`` Updates a single user +``/users/`` ``DELETE`` Deletes a single user +=============== =============== ====================================== + +So how would you go about doing that with the +:class:`~flask.views.MethodView`? The trick is to take advantage of the +fact that you can provide multiple rules to the same view. + +Let's assume for the moment the view would look like this:: + + class UserAPI(MethodView): + + def get(self, user_id): + if user_id is None: + # return a list of users + pass + else: + # expose a single user + pass + + def post(self): + # create a new user + pass + + def delete(self, user_id): + # delete a single user + pass + + def put(self, user_id): + # update a single user + pass + +So how do we hook this up with the routing system? By adding two rules +and explicitly mentioning the methods for each:: + + user_view = UserAPI.as_view('user_api') + app.add_url_rule('/users/', defaults={'user_id': None}, + view_func=user_view, methods=['GET', 'POST']) + app.add_url_rule('/users/', view_func=user_view, + methods=['GET', 'PUT', 'DELETE']) + +If you have a lot of APIs that look similar you can refactor that +registration code:: + + def register_api(view, endpoint, url, pk='id', pk_type='int'): + view_func = view.as_view(endpoint) + app.add_url_rule(url, defaults={pk: None}, + view_func=view_func, methods=['GET', 'POST']) + app.add_url_rule('%s<%s:%s>' % (url, pk), view_func=view_func, + methods=['GET', 'PUT', 'DELETE']) + + register_api(UserAPI, 'user_api', '/users/', pk='user_id') diff --git a/docs/web-security.rst b/docs/web-security.rst deleted file mode 100644 index 4118b5ec..00000000 --- a/docs/web-security.rst +++ /dev/null @@ -1,316 +0,0 @@ -Security Considerations -======================= - -Web applications face many types of potential security problems, and it can be -hard to get everything right, or even to know what "right" is in general. Flask -tries to solve a few of these things by default, but there are other parts you -may have to take care of yourself. Many of these solutions are tradeoffs, and -will depend on each application's specific needs and threat model. Many hosting -platforms may take care of certain types of problems without the need for the -Flask application to handle them. - -Resource Use ------------- - -A common category of attacks is "Denial of Service" (DoS or DDoS). This is a -very broad category, and different variants target different layers in a -deployed application. In general, something is done to increase how much -processing time or memory is used to handle each request, to the point where -there are not enough resources to handle legitimate requests. - -Flask provides a few configuration options to handle resource use. They can -also be set on individual requests to customize only that request. The -documentation for each goes into more detail. - -- :data:`MAX_CONTENT_LENGTH` or :attr:`.Request.max_content_length` controls - how much data will be read from a request. It is not set by default, - although it will still block truly unlimited streams unless the WSGI server - indicates support. -- :data:`MAX_FORM_MEMORY_SIZE` or :attr:`.Request.max_form_memory_size` - controls how large any non-file ``multipart/form-data`` field can be. It is - set to 500kB by default. -- :data:`MAX_FORM_PARTS` or :attr:`.Request.max_form_parts` controls how many - ``multipart/form-data`` fields can be parsed. It is set to 1000 by default. - Combined with the default `max_form_memory_size`, this means that a form - will occupy at most 500MB of memory. - -Regardless of these settings, you should also review what settings are available -from your operating system, container deployment (Docker etc), WSGI server, HTTP -server, and hosting platform. They typically have ways to set process resource -limits, timeouts, and other checks regardless of how Flask is configured. - -.. _security-xss: - -Cross-Site Scripting (XSS) --------------------------- - -Cross site scripting is the concept of injecting arbitrary HTML (and with -it JavaScript) into the context of a website. To remedy this, developers -have to properly escape text so that it cannot include arbitrary HTML -tags. For more information on that have a look at the Wikipedia article -on `Cross-Site Scripting -`_. - -Flask configures Jinja to automatically escape all values unless -explicitly told otherwise. This should rule out all XSS problems caused -in templates, but there are still other places where you have to be -careful: - -- generating HTML without the help of Jinja -- calling :class:`~markupsafe.Markup` on data submitted by users -- sending out HTML from uploaded files, never do that, use the - ``Content-Disposition: attachment`` header to prevent that problem. -- sending out textfiles from uploaded files. Some browsers are using - content-type guessing based on the first few bytes so users could - trick a browser to execute HTML. - -Another thing that is very important are unquoted attributes. While -Jinja can protect you from XSS issues by escaping HTML, there is one -thing it cannot protect you from: XSS by attribute injection. To counter -this possible attack vector, be sure to always quote your attributes with -either double or single quotes when using Jinja expressions in them: - -.. sourcecode:: html+jinja - - - -Why is this necessary? Because if you would not be doing that, an -attacker could easily inject custom JavaScript handlers. For example an -attacker could inject this piece of HTML+JavaScript: - -.. sourcecode:: html - - onmouseover=alert(document.cookie) - -When the user would then move with the mouse over the input, the cookie -would be presented to the user in an alert window. But instead of showing -the cookie to the user, a good attacker might also execute any other -JavaScript code. In combination with CSS injections the attacker might -even make the element fill out the entire page so that the user would -just have to have the mouse anywhere on the page to trigger the attack. - -There is one class of XSS issues that Jinja's escaping does not protect -against. The ``a`` tag's ``href`` attribute can contain a `javascript:` URI, -which the browser will execute when clicked if not secured properly. - -.. sourcecode:: html - - click here - click here - -To prevent this, you'll need to set the :ref:`security-csp` response header. - -Cross-Site Request Forgery (CSRF) ---------------------------------- - -Another big problem is CSRF. This is a very complex topic and I won't -outline it here in detail just mention what it is and how to theoretically -prevent it. - -If your authentication information is stored in cookies, you have implicit -state management. The state of "being logged in" is controlled by a -cookie, and that cookie is sent with each request to a page. -Unfortunately that includes requests triggered by 3rd party sites. If you -don't keep that in mind, some people might be able to trick your -application's users with social engineering to do stupid things without -them knowing. - -Say you have a specific URL that, when you sent ``POST`` requests to will -delete a user's profile (say ``http://example.com/user/delete``). If an -attacker now creates a page that sends a post request to that page with -some JavaScript they just have to trick some users to load that page and -their profiles will end up being deleted. - -Imagine you were to run Facebook with millions of concurrent users and -someone would send out links to images of little kittens. When users -would go to that page, their profiles would get deleted while they are -looking at images of fluffy cats. - -How can you prevent that? Basically for each request that modifies -content on the server you would have to either use a one-time token and -store that in the cookie **and** also transmit it with the form data. -After receiving the data on the server again, you would then have to -compare the two tokens and ensure they are equal. - -Why does Flask not do that for you? The ideal place for this to happen is -the form validation framework, which does not exist in Flask. - -.. _security-json: - -JSON Security -------------- - -In Flask 0.10 and lower, :func:`~flask.jsonify` did not serialize top-level -arrays to JSON. This was because of a security vulnerability in ECMAScript 4. - -ECMAScript 5 closed this vulnerability, so only extremely old browsers are -still vulnerable. All of these browsers have `other more serious -vulnerabilities -`_, so -this behavior was changed and :func:`~flask.jsonify` now supports serializing -arrays. - -Security Headers ----------------- - -Browsers recognize various response headers in order to control security. We -recommend reviewing each of the headers below for use in your application. -The `Flask-Talisman`_ extension can be used to manage HTTPS and the security -headers for you. - -.. _Flask-Talisman: https://github.com/wntrblm/flask-talisman - -HTTP Strict Transport Security (HSTS) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Tells the browser to convert all HTTP requests to HTTPS, preventing -man-in-the-middle (MITM) attacks. :: - - response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' - -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security - -.. _security-csp: - -Content Security Policy (CSP) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Tell the browser where it can load various types of resource from. This header -should be used whenever possible, but requires some work to define the correct -policy for your site. A very strict policy would be:: - - response.headers['Content-Security-Policy'] = "default-src 'self'" - -- https://csp.withgoogle.com/docs/index.html -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy - -X-Content-Type-Options -~~~~~~~~~~~~~~~~~~~~~~ - -Forces the browser to honor the response content type instead of trying to -detect it, which can be abused to generate a cross-site scripting (XSS) -attack. :: - - response.headers['X-Content-Type-Options'] = 'nosniff' - -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options - -X-Frame-Options -~~~~~~~~~~~~~~~ - -Prevents external sites from embedding your site in an ``iframe``. This -prevents a class of attacks where clicks in the outer frame can be translated -invisibly to clicks on your page's elements. This is also known as -"clickjacking". :: - - response.headers['X-Frame-Options'] = 'SAMEORIGIN' - -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options - -.. _security-cookie: - -Set-Cookie options -~~~~~~~~~~~~~~~~~~ - -These options can be added to a ``Set-Cookie`` header to improve their -security. Flask has configuration options to set these on the session cookie. -They can be set on other cookies too. - -- ``Secure`` limits cookies to HTTPS traffic only. -- ``HttpOnly`` protects the contents of cookies from being read with - JavaScript. -- ``SameSite`` restricts how cookies are sent with requests from - external sites. Can be set to ``'Lax'`` (recommended) or ``'Strict'``. - ``Lax`` prevents sending cookies with CSRF-prone requests from - external sites, such as submitting a form. ``Strict`` prevents sending - cookies with all external requests, including following regular links. - -:: - - app.config.update( - SESSION_COOKIE_SECURE=True, - SESSION_COOKIE_HTTPONLY=True, - SESSION_COOKIE_SAMESITE='Lax', - ) - - response.set_cookie('username', 'flask', secure=True, httponly=True, samesite='Lax') - -Specifying ``Expires`` or ``Max-Age`` options, will remove the cookie after -the given time, or the current time plus the age, respectively. If neither -option is set, the cookie will be removed when the browser is closed. :: - - # cookie expires after 10 minutes - response.set_cookie('snakes', '3', max_age=600) - -For the session cookie, if :attr:`session.permanent ` -is set, then :data:`PERMANENT_SESSION_LIFETIME` is used to set the expiration. -Flask's default cookie implementation validates that the cryptographic -signature is not older than this value. Lowering this value may help mitigate -replay attacks, where intercepted cookies can be sent at a later time. :: - - app.config.update( - PERMANENT_SESSION_LIFETIME=600 - ) - - @app.route('/login', methods=['POST']) - def login(): - ... - session.clear() - session['user_id'] = user.id - session.permanent = True - ... - -Use :class:`itsdangerous.TimedSerializer` to sign and validate other cookie -values (or any values that need secure signatures). - -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie - -.. _samesite_support: https://caniuse.com/#feat=same-site-cookie-attribute - - -Host Header Validation ----------------------- - -The ``Host`` header is used by the client to indicate what host name the request -was made to. This is used, for example, by ``url_for(..., _external=True)`` to -generate full URLs, for use in email or other messages outside the browser -window. - -By default the app doesn't know what host(s) it is allowed to be accessed -through, and assumes any host is valid. Although browsers do not allow setting -the ``Host`` header, requests made by attackers in other scenarios could set -the ``Host`` header to a value they want. - -When deploying your application, set :data:`TRUSTED_HOSTS` to restrict what -values the ``Host`` header may be. - -The ``Host`` header may be modified by proxies in between the client and your -application. See :doc:`deploying/proxy_fix` to tell your app which proxy values -to trust. - - -Copy/Paste to Terminal ----------------------- - -Hidden characters such as the backspace character (``\b``, ``^H``) can -cause text to render differently in HTML than how it is interpreted if -`pasted into a terminal `__. - -For example, ``import y\bose\bm\bi\bt\be\b`` renders as -``import yosemite`` in HTML, but the backspaces are applied when pasted -into a terminal, and it becomes ``import os``. - -If you expect users to copy and paste untrusted code from your site, -such as from comments posted by users on a technical blog, consider -applying extra filtering, such as replacing all ``\b`` characters. - -.. code-block:: python - - body = body.replace("\b", "") - -Most modern terminals will warn about and remove hidden characters when -pasting, so this isn't strictly necessary. It's also possible to craft -dangerous commands in other ways that aren't possible to filter. -Depending on your site's use case, it may be good to show a warning -about copying code in general. diff --git a/examples/celery/README.md b/examples/celery/README.md deleted file mode 100644 index 038eb51e..00000000 --- a/examples/celery/README.md +++ /dev/null @@ -1,27 +0,0 @@ -Background Tasks with Celery -============================ - -This example shows how to configure Celery with Flask, how to set up an API for -submitting tasks and polling results, and how to use that API with JavaScript. See -[Flask's documentation about Celery](https://flask.palletsprojects.com/patterns/celery/). - -From this directory, create a virtualenv and install the application into it. Then run a -Celery worker. - -```shell -$ python3 -m venv .venv -$ . ./.venv/bin/activate -$ pip install -r requirements.txt && pip install -e . -$ celery -A make_celery worker --loglevel INFO -``` - -In a separate terminal, activate the virtualenv and run the Flask development server. - -```shell -$ . ./.venv/bin/activate -$ flask -A task_app run --debug -``` - -Go to http://localhost:5000/ and use the forms to submit tasks. You can see the polling -requests in the browser dev tools and the Flask logs. You can see the tasks submitting -and completing in the Celery logs. diff --git a/examples/celery/make_celery.py b/examples/celery/make_celery.py deleted file mode 100644 index f7d138e6..00000000 --- a/examples/celery/make_celery.py +++ /dev/null @@ -1,4 +0,0 @@ -from task_app import create_app - -flask_app = create_app() -celery_app = flask_app.extensions["celery"] diff --git a/examples/celery/pyproject.toml b/examples/celery/pyproject.toml deleted file mode 100644 index cca36d8c..00000000 --- a/examples/celery/pyproject.toml +++ /dev/null @@ -1,17 +0,0 @@ -[project] -name = "flask-example-celery" -version = "1.0.0" -description = "Example Flask application with Celery background tasks." -readme = "README.md" -classifiers = ["Private :: Do Not Upload"] -dependencies = ["flask", "celery[redis]"] - -[build-system] -requires = ["flit_core<4"] -build-backend = "flit_core.buildapi" - -[tool.flit.module] -name = "task_app" - -[tool.ruff] -src = ["src"] diff --git a/examples/celery/requirements.txt b/examples/celery/requirements.txt deleted file mode 100644 index 29075ab5..00000000 --- a/examples/celery/requirements.txt +++ /dev/null @@ -1,58 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --resolver=backtracking pyproject.toml -# -amqp==5.1.1 - # via kombu -async-timeout==4.0.2 - # via redis -billiard==3.6.4.0 - # via celery -blinker==1.6.2 - # via flask -celery[redis]==5.2.7 - # via flask-example-celery (pyproject.toml) -click==8.1.3 - # via - # celery - # click-didyoumean - # click-plugins - # click-repl - # flask -click-didyoumean==0.3.0 - # via celery -click-plugins==1.1.1 - # via celery -click-repl==0.2.0 - # via celery -flask==2.3.2 - # via flask-example-celery (pyproject.toml) -itsdangerous==2.1.2 - # via flask -jinja2==3.1.2 - # via flask -kombu==5.2.4 - # via celery -markupsafe==2.1.2 - # via - # jinja2 - # werkzeug -prompt-toolkit==3.0.38 - # via click-repl -pytz==2023.3 - # via celery -redis==4.5.4 - # via celery -six==1.16.0 - # via click-repl -vine==5.0.0 - # via - # amqp - # celery - # kombu -wcwidth==0.2.6 - # via prompt-toolkit -werkzeug==2.3.3 - # via flask diff --git a/examples/celery/src/task_app/__init__.py b/examples/celery/src/task_app/__init__.py deleted file mode 100644 index dafff8aa..00000000 --- a/examples/celery/src/task_app/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -from celery import Celery -from celery import Task -from flask import Flask -from flask import render_template - - -def create_app() -> Flask: - app = Flask(__name__) - app.config.from_mapping( - CELERY=dict( - broker_url="redis://localhost", - result_backend="redis://localhost", - task_ignore_result=True, - ), - ) - app.config.from_prefixed_env() - celery_init_app(app) - - @app.route("/") - def index() -> str: - return render_template("index.html") - - from . import views - - app.register_blueprint(views.bp) - return app - - -def celery_init_app(app: Flask) -> Celery: - class FlaskTask(Task): - def __call__(self, *args: object, **kwargs: object) -> object: - with app.app_context(): - return self.run(*args, **kwargs) - - celery_app = Celery(app.name, task_cls=FlaskTask) - celery_app.config_from_object(app.config["CELERY"]) - celery_app.set_default() - app.extensions["celery"] = celery_app - return celery_app diff --git a/examples/celery/src/task_app/tasks.py b/examples/celery/src/task_app/tasks.py deleted file mode 100644 index b6b3595d..00000000 --- a/examples/celery/src/task_app/tasks.py +++ /dev/null @@ -1,23 +0,0 @@ -import time - -from celery import shared_task -from celery import Task - - -@shared_task(ignore_result=False) -def add(a: int, b: int) -> int: - return a + b - - -@shared_task() -def block() -> None: - time.sleep(5) - - -@shared_task(bind=True, ignore_result=False) -def process(self: Task, total: int) -> object: - for i in range(total): - self.update_state(state="PROGRESS", meta={"current": i + 1, "total": total}) - time.sleep(1) - - return {"current": total, "total": total} diff --git a/examples/celery/src/task_app/templates/index.html b/examples/celery/src/task_app/templates/index.html deleted file mode 100644 index 4e1145cb..00000000 --- a/examples/celery/src/task_app/templates/index.html +++ /dev/null @@ -1,108 +0,0 @@ - - - - - Celery Example - - -

Celery Example

-Execute background tasks with Celery. Submits tasks and shows results using JavaScript. - -
-

Add

-

Start a task to add two numbers, then poll for the result. -

-
-
- -
-

Result:

- -
-

Block

-

Start a task that takes 5 seconds. However, the response will return immediately. -

- -
-

- -
-

Process

-

Start a task that counts, waiting one second each time, showing progress. -

-
- -
-

- - - - diff --git a/examples/celery/src/task_app/views.py b/examples/celery/src/task_app/views.py deleted file mode 100644 index 99cf92dc..00000000 --- a/examples/celery/src/task_app/views.py +++ /dev/null @@ -1,38 +0,0 @@ -from celery.result import AsyncResult -from flask import Blueprint -from flask import request - -from . import tasks - -bp = Blueprint("tasks", __name__, url_prefix="/tasks") - - -@bp.get("/result/") -def result(id: str) -> dict[str, object]: - result = AsyncResult(id) - ready = result.ready() - return { - "ready": ready, - "successful": result.successful() if ready else None, - "value": result.get() if ready else result.result, - } - - -@bp.post("/add") -def add() -> dict[str, object]: - a = request.form.get("a", type=int) - b = request.form.get("b", type=int) - result = tasks.add.delay(a, b) - return {"result_id": result.id} - - -@bp.post("/block") -def block() -> dict[str, object]: - result = tasks.block.delay() - return {"result_id": result.id} - - -@bp.post("/process") -def process() -> dict[str, object]: - result = tasks.process.delay(total=request.form.get("total", type=int)) - return {"result_id": result.id} diff --git a/examples/flaskr/README b/examples/flaskr/README new file mode 100644 index 00000000..9ab20589 --- /dev/null +++ b/examples/flaskr/README @@ -0,0 +1,28 @@ + + / Flaskr / + + a minimal blog application + + + ~ What is Flaskr? + + A sqlite powered thumble blog application + + ~ How do I use it? + + 1. edit the configuration in the flaskr.py file or + export an FLASKR_SETTINGS environment variable + pointing to a configuration file. + + 2. fire up a python shell and run this: + + >>> from flaskr import init_db; init_db() + + 3. now you can run the flaskr.py file with your + python interpreter and the application will + greet you on http://localhost:5000/ + + ~ Is it tested? + + You betcha. Run the `flaskr_tests.py` file to see + the tests pass. diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py new file mode 100644 index 00000000..6f9b06fc --- /dev/null +++ b/examples/flaskr/flaskr.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +""" + Flaskr + ~~~~~~ + + A microblog example application written as Flask tutorial with + Flask and sqlite3. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from __future__ import with_statement +from sqlite3 import dbapi2 as sqlite3 +from contextlib import closing +from flask import Flask, request, session, g, redirect, url_for, abort, \ + render_template, flash + +# configuration +DATABASE = '/tmp/flaskr.db' +DEBUG = True +SECRET_KEY = 'development key' +USERNAME = 'admin' +PASSWORD = 'default' + +# create our little application :) +app = Flask(__name__) +app.config.from_object(__name__) +app.config.from_envvar('FLASKR_SETTINGS', silent=True) + + +def connect_db(): + """Returns a new connection to the database.""" + return sqlite3.connect(app.config['DATABASE']) + + +def init_db(): + """Creates the database tables.""" + with closing(connect_db()) as db: + with app.open_resource('schema.sql') as f: + db.cursor().executescript(f.read()) + db.commit() + + +@app.before_request +def before_request(): + """Make sure we are connected to the database each request.""" + g.db = connect_db() + + +@app.teardown_request +def teardown_request(exception): + """Closes the database again at the end of the request.""" + if hasattr(g, 'db'): + g.db.close() + + +@app.route('/') +def show_entries(): + cur = g.db.execute('select title, text from entries order by id desc') + entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()] + return render_template('show_entries.html', entries=entries) + + +@app.route('/add', methods=['POST']) +def add_entry(): + if not session.get('logged_in'): + abort(401) + g.db.execute('insert into entries (title, text) values (?, ?)', + [request.form['title'], request.form['text']]) + g.db.commit() + flash('New entry was successfully posted') + return redirect(url_for('show_entries')) + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + error = None + if request.method == 'POST': + if request.form['username'] != app.config['USERNAME']: + error = 'Invalid username' + elif request.form['password'] != app.config['PASSWORD']: + error = 'Invalid password' + else: + session['logged_in'] = True + flash('You were logged in') + return redirect(url_for('show_entries')) + return render_template('login.html', error=error) + + +@app.route('/logout') +def logout(): + session.pop('logged_in', None) + flash('You were logged out') + return redirect(url_for('show_entries')) + + +if __name__ == '__main__': + app.run() diff --git a/examples/flaskr/flaskr_tests.py b/examples/flaskr/flaskr_tests.py new file mode 100644 index 00000000..cfac3782 --- /dev/null +++ b/examples/flaskr/flaskr_tests.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +""" + Flaskr Tests + ~~~~~~~~~~~~ + + Tests the Flaskr application. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import flaskr +import unittest +import tempfile + + +class FlaskrTestCase(unittest.TestCase): + + def setUp(self): + """Before each test, set up a blank database""" + self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() + flaskr.app.config['TESTING'] = True + self.app = flaskr.app.test_client() + flaskr.init_db() + + def tearDown(self): + """Get rid of the database again after each test.""" + os.close(self.db_fd) + os.unlink(flaskr.app.config['DATABASE']) + + def login(self, username, password): + return self.app.post('/login', data=dict( + username=username, + password=password + ), follow_redirects=True) + + def logout(self): + return self.app.get('/logout', follow_redirects=True) + + # testing functions + + def test_empty_db(self): + """Start with a blank database.""" + rv = self.app.get('/') + assert 'No entries here so far' in rv.data + + def test_login_logout(self): + """Make sure login and logout works""" + rv = self.login(flaskr.app.config['USERNAME'], + flaskr.app.config['PASSWORD']) + assert 'You were logged in' in rv.data + rv = self.logout() + assert 'You were logged out' in rv.data + rv = self.login(flaskr.app.config['USERNAME'] + 'x', + flaskr.app.config['PASSWORD']) + assert 'Invalid username' in rv.data + rv = self.login(flaskr.app.config['USERNAME'], + flaskr.app.config['PASSWORD'] + 'x') + assert 'Invalid password' in rv.data + + def test_messages(self): + """Test that messages work""" + self.login(flaskr.app.config['USERNAME'], + flaskr.app.config['PASSWORD']) + rv = self.app.post('/add', data=dict( + title='', + text='HTML allowed here' + ), follow_redirects=True) + assert 'No entries here so far' not in rv.data + assert '<Hello>' in rv.data + assert 'HTML allowed here' in rv.data + + +if __name__ == '__main__': + unittest.main() diff --git a/examples/flaskr/schema.sql b/examples/flaskr/schema.sql new file mode 100644 index 00000000..970cca77 --- /dev/null +++ b/examples/flaskr/schema.sql @@ -0,0 +1,6 @@ +drop table if exists entries; +create table entries ( + id integer primary key autoincrement, + title string not null, + text string not null +); diff --git a/examples/flaskr/static/style.css b/examples/flaskr/static/style.css new file mode 100644 index 00000000..4f3b71d8 --- /dev/null +++ b/examples/flaskr/static/style.css @@ -0,0 +1,18 @@ +body { font-family: sans-serif; background: #eee; } +a, h1, h2 { color: #377BA8; } +h1, h2 { font-family: 'Georgia', serif; margin: 0; } +h1 { border-bottom: 2px solid #eee; } +h2 { font-size: 1.2em; } + +.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; + padding: 0.8em; background: white; } +.entries { list-style: none; margin: 0; padding: 0; } +.entries li { margin: 0.8em 1.2em; } +.entries li h2 { margin-left: -1em; } +.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } +.add-entry dl { font-weight: bold; } +.metanav { text-align: right; font-size: 0.8em; padding: 0.3em; + margin-bottom: 1em; background: #fafafa; } +.flash { background: #CEE5F5; padding: 0.5em; + border: 1px solid #AACBE2; } +.error { background: #F0D6D6; padding: 0.5em; } diff --git a/examples/flaskr/templates/layout.html b/examples/flaskr/templates/layout.html new file mode 100644 index 00000000..cbdb9650 --- /dev/null +++ b/examples/flaskr/templates/layout.html @@ -0,0 +1,17 @@ + +Flaskr + +
+

Flaskr

+
+ {% if not session.logged_in %} + log in + {% else %} + log out + {% endif %} +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block body %}{% endblock %} +
diff --git a/examples/flaskr/templates/login.html b/examples/flaskr/templates/login.html new file mode 100644 index 00000000..6f70bb76 --- /dev/null +++ b/examples/flaskr/templates/login.html @@ -0,0 +1,14 @@ +{% extends "layout.html" %} +{% block body %} +

Login

+ {% if error %}

Error: {{ error }}{% endif %} +

+
+
Username: +
+
Password: +
+
+
+
+{% endblock %} diff --git a/examples/flaskr/templates/show_entries.html b/examples/flaskr/templates/show_entries.html new file mode 100644 index 00000000..fabe65ec --- /dev/null +++ b/examples/flaskr/templates/show_entries.html @@ -0,0 +1,21 @@ +{% extends "layout.html" %} +{% block body %} + {% if session.logged_in %} +
+
+
Title: +
+
Text: +
+
+
+
+ {% endif %} +
    + {% for entry in entries %} +
  • {{ entry.title }}

    {{ entry.text|safe }} + {% else %} +
  • Unbelievable. No entries here so far + {% endfor %} +
+{% endblock %} diff --git a/examples/javascript/.gitignore b/examples/javascript/.gitignore deleted file mode 100644 index a306afbc..00000000 --- a/examples/javascript/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -.venv/ -*.pyc -__pycache__/ -instance/ -.cache/ -.pytest_cache/ -.coverage -htmlcov/ -dist/ -build/ -*.egg-info/ -.idea/ -*.swp -*~ diff --git a/examples/javascript/LICENSE.txt b/examples/javascript/LICENSE.txt deleted file mode 100644 index 9d227a0c..00000000 --- a/examples/javascript/LICENSE.txt +++ /dev/null @@ -1,28 +0,0 @@ -Copyright 2010 Pallets - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/javascript/README.rst b/examples/javascript/README.rst deleted file mode 100644 index f5f66912..00000000 --- a/examples/javascript/README.rst +++ /dev/null @@ -1,48 +0,0 @@ -JavaScript Ajax Example -======================= - -Demonstrates how to post form data and process a JSON response using -JavaScript. This allows making requests without navigating away from the -page. Demonstrates using |fetch|_, |XMLHttpRequest|_, and -|jQuery.ajax|_. See the `Flask docs`_ about JavaScript and Ajax. - -.. |fetch| replace:: ``fetch`` -.. _fetch: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch - -.. |XMLHttpRequest| replace:: ``XMLHttpRequest`` -.. _XMLHttpRequest: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest - -.. |jQuery.ajax| replace:: ``jQuery.ajax`` -.. _jQuery.ajax: https://api.jquery.com/jQuery.ajax/ - -.. _Flask docs: https://flask.palletsprojects.com/patterns/javascript/ - - -Install -------- - -.. code-block:: text - - $ python3 -m venv .venv - $ . .venv/bin/activate - $ pip install -e . - - -Run ---- - -.. code-block:: text - - $ flask --app js_example run - -Open http://127.0.0.1:5000 in a browser. - - -Test ----- - -.. code-block:: text - - $ pip install -e '.[test]' - $ coverage run -m pytest - $ coverage report diff --git a/examples/javascript/js_example/__init__.py b/examples/javascript/js_example/__init__.py deleted file mode 100644 index 0ec3ca21..00000000 --- a/examples/javascript/js_example/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from flask import Flask - -app = Flask(__name__) - -from js_example import views # noqa: E402, F401 diff --git a/examples/javascript/js_example/templates/base.html b/examples/javascript/js_example/templates/base.html deleted file mode 100644 index a4d35bd7..00000000 --- a/examples/javascript/js_example/templates/base.html +++ /dev/null @@ -1,33 +0,0 @@ - -JavaScript Example - - - - -
-

{% block intro %}{% endblock %}

-
-
- - + - - -
-= -{% block script %}{% endblock %} diff --git a/examples/javascript/js_example/templates/fetch.html b/examples/javascript/js_example/templates/fetch.html deleted file mode 100644 index e2944b85..00000000 --- a/examples/javascript/js_example/templates/fetch.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends 'base.html' %} - -{% block intro %} - fetch - is the modern plain JavaScript way to make requests. It's - supported in all modern browsers. -{% endblock %} - -{% block script %} - -{% endblock %} diff --git a/examples/javascript/js_example/templates/jquery.html b/examples/javascript/js_example/templates/jquery.html deleted file mode 100644 index 48f0c11c..00000000 --- a/examples/javascript/js_example/templates/jquery.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends 'base.html' %} - -{% block intro %} - jQuery is a popular library that - adds cross browser APIs for common tasks. However, it requires loading - an extra library. -{% endblock %} - -{% block script %} - - -{% endblock %} diff --git a/examples/javascript/js_example/templates/xhr.html b/examples/javascript/js_example/templates/xhr.html deleted file mode 100644 index 1672d4d6..00000000 --- a/examples/javascript/js_example/templates/xhr.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'base.html' %} - -{% block intro %} - XMLHttpRequest - is the original JavaScript way to make requests. It's natively supported - by all browsers, but has been superseded by - fetch. -{% endblock %} - -{% block script %} - -{% endblock %} diff --git a/examples/javascript/js_example/views.py b/examples/javascript/js_example/views.py deleted file mode 100644 index 9f0d26c5..00000000 --- a/examples/javascript/js_example/views.py +++ /dev/null @@ -1,18 +0,0 @@ -from flask import jsonify -from flask import render_template -from flask import request - -from . import app - - -@app.route("/", defaults={"js": "fetch"}) -@app.route("/") -def index(js): - return render_template(f"{js}.html", js=js) - - -@app.route("/add", methods=["POST"]) -def add(): - a = request.form.get("a", 0, type=float) - b = request.form.get("b", 0, type=float) - return jsonify(result=a + b) diff --git a/examples/javascript/pyproject.toml b/examples/javascript/pyproject.toml deleted file mode 100644 index ea0efabd..00000000 --- a/examples/javascript/pyproject.toml +++ /dev/null @@ -1,33 +0,0 @@ -[project] -name = "js_example" -version = "1.1.0" -description = "Demonstrates making AJAX requests to Flask." -readme = "README.rst" -license = {file = "LICENSE.txt"} -maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] -classifiers = ["Private :: Do Not Upload"] -dependencies = ["flask"] - -[project.urls] -Documentation = "https://flask.palletsprojects.com/patterns/javascript/" - -[project.optional-dependencies] -test = ["pytest"] - -[build-system] -requires = ["flit_core<4"] -build-backend = "flit_core.buildapi" - -[tool.flit.module] -name = "js_example" - -[tool.pytest.ini_options] -testpaths = ["tests"] -filterwarnings = ["error"] - -[tool.coverage.run] -branch = true -source = ["js_example", "tests"] - -[tool.ruff] -src = ["src"] diff --git a/examples/javascript/tests/conftest.py b/examples/javascript/tests/conftest.py deleted file mode 100644 index e0cabbfd..00000000 --- a/examples/javascript/tests/conftest.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from js_example import app - - -@pytest.fixture(name="app") -def fixture_app(): - app.testing = True - yield app - app.testing = False - - -@pytest.fixture -def client(app): - return app.test_client() diff --git a/examples/javascript/tests/test_js_example.py b/examples/javascript/tests/test_js_example.py deleted file mode 100644 index 856f5f77..00000000 --- a/examples/javascript/tests/test_js_example.py +++ /dev/null @@ -1,27 +0,0 @@ -import pytest -from flask import template_rendered - - -@pytest.mark.parametrize( - ("path", "template_name"), - ( - ("/", "fetch.html"), - ("/plain", "xhr.html"), - ("/fetch", "fetch.html"), - ("/jquery", "jquery.html"), - ), -) -def test_index(app, client, path, template_name): - def check(sender, template, context): - assert template.name == template_name - - with template_rendered.connected_to(check, app): - client.get(path) - - -@pytest.mark.parametrize( - ("a", "b", "result"), ((2, 3, 5), (2.5, 3, 5.5), (2, None, 2), (2, "b", 2)) -) -def test_add(client, a, b, result): - response = client.post("/add", data={"a": a, "b": b}) - assert response.get_json()["result"] == result diff --git a/examples/jqueryexample/jqueryexample.py b/examples/jqueryexample/jqueryexample.py new file mode 100644 index 00000000..0e8caf3e --- /dev/null +++ b/examples/jqueryexample/jqueryexample.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" + jQuery Example + ~~~~~~~~~~~~~~ + + A simple application that shows how Flask and jQuery get along. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from flask import Flask, jsonify, render_template, request +app = Flask(__name__) + + +@app.route('/_add_numbers') +def add_numbers(): + """Add two numbers server side, ridiculous but well...""" + a = request.args.get('a', 0, type=int) + b = request.args.get('b', 0, type=int) + return jsonify(result=a + b) + + +@app.route('/') +def index(): + return render_template('index.html') + + +if __name__ == '__main__': + app.run() diff --git a/examples/jqueryexample/templates/index.html b/examples/jqueryexample/templates/index.html new file mode 100644 index 00000000..0545516d --- /dev/null +++ b/examples/jqueryexample/templates/index.html @@ -0,0 +1,21 @@ +{% extends "layout.html" %} +{% block body %} + +

jQuery Example

+

+ + = + ? +

calculate server side +{% endblock %} diff --git a/examples/jqueryexample/templates/layout.html b/examples/jqueryexample/templates/layout.html new file mode 100644 index 00000000..3e2ed69b --- /dev/null +++ b/examples/jqueryexample/templates/layout.html @@ -0,0 +1,8 @@ + +jQuery Example + + +{% block body %}{% endblock %} diff --git a/examples/minitwit/README b/examples/minitwit/README new file mode 100644 index 00000000..ab946295 --- /dev/null +++ b/examples/minitwit/README @@ -0,0 +1,28 @@ + + / MiniTwit / + + because writing todo lists is not fun + + + ~ What is MiniTwit? + + A SQLite and Flask powered twitter clone + + ~ How do I use it? + + 1. edit the configuration in the minitwit.py file or + export an MINITWIT_SETTINGS environment variable + pointing to a configuration file. + + 2. fire up a python shell and run this: + + >>> from minitwit import init_db; init_db() + + 3. now you can run the minitwit.py file with your + python interpreter and the application will + greet you on http://localhost:5000/ + + ~ Is it tested? + + You betcha. Run the `minitwit_tests.py` file to + see the tests pass. diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py new file mode 100644 index 00000000..fee023fd --- /dev/null +++ b/examples/minitwit/minitwit.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +""" + MiniTwit + ~~~~~~~~ + + A microblogging application written with Flask and sqlite3. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from __future__ import with_statement +import time +from sqlite3 import dbapi2 as sqlite3 +from hashlib import md5 +from datetime import datetime +from contextlib import closing +from flask import Flask, request, session, url_for, redirect, \ + render_template, abort, g, flash +from werkzeug import check_password_hash, generate_password_hash + + +# configuration +DATABASE = '/tmp/minitwit.db' +PER_PAGE = 30 +DEBUG = True +SECRET_KEY = 'development key' + +# create our little application :) +app = Flask(__name__) +app.config.from_object(__name__) +app.config.from_envvar('MINITWIT_SETTINGS', silent=True) + + +def connect_db(): + """Returns a new connection to the database.""" + return sqlite3.connect(app.config['DATABASE']) + + +def init_db(): + """Creates the database tables.""" + with closing(connect_db()) as db: + with app.open_resource('schema.sql') as f: + db.cursor().executescript(f.read()) + db.commit() + + +def query_db(query, args=(), one=False): + """Queries the database and returns a list of dictionaries.""" + cur = g.db.execute(query, args) + rv = [dict((cur.description[idx][0], value) + for idx, value in enumerate(row)) for row in cur.fetchall()] + return (rv[0] if rv else None) if one else rv + + +def get_user_id(username): + """Convenience method to look up the id for a username.""" + rv = g.db.execute('select user_id from user where username = ?', + [username]).fetchone() + return rv[0] if rv else None + + +def format_datetime(timestamp): + """Format a timestamp for display.""" + return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M') + + +def gravatar_url(email, size=80): + """Return the gravatar image for the given email address.""" + return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ + (md5(email.strip().lower().encode('utf-8')).hexdigest(), size) + + +@app.before_request +def before_request(): + """Make sure we are connected to the database each request and look + up the current user so that we know he's there. + """ + g.db = connect_db() + g.user = None + if 'user_id' in session: + g.user = query_db('select * from user where user_id = ?', + [session['user_id']], one=True) + + +@app.teardown_request +def teardown_request(exception): + """Closes the database again at the end of the request.""" + if hasattr(g, 'db'): + g.db.close() + + +@app.route('/') +def timeline(): + """Shows a users timeline or if no user is logged in it will + redirect to the public timeline. This timeline shows the user's + messages as well as all the messages of followed users. + """ + if not g.user: + return redirect(url_for('public_timeline')) + return render_template('timeline.html', messages=query_db(''' + select message.*, user.* from message, user + where message.author_id = user.user_id and ( + user.user_id = ? or + user.user_id in (select whom_id from follower + where who_id = ?)) + order by message.pub_date desc limit ?''', + [session['user_id'], session['user_id'], PER_PAGE])) + + +@app.route('/public') +def public_timeline(): + """Displays the latest messages of all users.""" + return render_template('timeline.html', messages=query_db(''' + select message.*, user.* from message, user + where message.author_id = user.user_id + order by message.pub_date desc limit ?''', [PER_PAGE])) + + +@app.route('/') +def user_timeline(username): + """Display's a users tweets.""" + profile_user = query_db('select * from user where username = ?', + [username], one=True) + if profile_user is None: + abort(404) + followed = False + if g.user: + followed = query_db('''select 1 from follower where + follower.who_id = ? and follower.whom_id = ?''', + [session['user_id'], profile_user['user_id']], + one=True) is not None + return render_template('timeline.html', messages=query_db(''' + select message.*, user.* from message, user where + user.user_id = message.author_id and user.user_id = ? + order by message.pub_date desc limit ?''', + [profile_user['user_id'], PER_PAGE]), followed=followed, + profile_user=profile_user) + + +@app.route('//follow') +def follow_user(username): + """Adds the current user as follower of the given user.""" + if not g.user: + abort(401) + whom_id = get_user_id(username) + if whom_id is None: + abort(404) + g.db.execute('insert into follower (who_id, whom_id) values (?, ?)', + [session['user_id'], whom_id]) + g.db.commit() + flash('You are now following "%s"' % username) + return redirect(url_for('user_timeline', username=username)) + + +@app.route('//unfollow') +def unfollow_user(username): + """Removes the current user as follower of the given user.""" + if not g.user: + abort(401) + whom_id = get_user_id(username) + if whom_id is None: + abort(404) + g.db.execute('delete from follower where who_id=? and whom_id=?', + [session['user_id'], whom_id]) + g.db.commit() + flash('You are no longer following "%s"' % username) + return redirect(url_for('user_timeline', username=username)) + + +@app.route('/add_message', methods=['POST']) +def add_message(): + """Registers a new message for the user.""" + if 'user_id' not in session: + abort(401) + if request.form['text']: + g.db.execute('''insert into message (author_id, text, pub_date) + values (?, ?, ?)''', (session['user_id'], request.form['text'], + int(time.time()))) + g.db.commit() + flash('Your message was recorded') + return redirect(url_for('timeline')) + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + """Logs the user in.""" + if g.user: + return redirect(url_for('timeline')) + error = None + if request.method == 'POST': + user = query_db('''select * from user where + username = ?''', [request.form['username']], one=True) + if user is None: + error = 'Invalid username' + elif not check_password_hash(user['pw_hash'], + request.form['password']): + error = 'Invalid password' + else: + flash('You were logged in') + session['user_id'] = user['user_id'] + return redirect(url_for('timeline')) + return render_template('login.html', error=error) + + +@app.route('/register', methods=['GET', 'POST']) +def register(): + """Registers the user.""" + if g.user: + return redirect(url_for('timeline')) + error = None + if request.method == 'POST': + if not request.form['username']: + error = 'You have to enter a username' + elif not request.form['email'] or \ + '@' not in request.form['email']: + error = 'You have to enter a valid email address' + elif not request.form['password']: + error = 'You have to enter a password' + elif request.form['password'] != request.form['password2']: + error = 'The two passwords do not match' + elif get_user_id(request.form['username']) is not None: + error = 'The username is already taken' + else: + g.db.execute('''insert into user ( + username, email, pw_hash) values (?, ?, ?)''', + [request.form['username'], request.form['email'], + generate_password_hash(request.form['password'])]) + g.db.commit() + flash('You were successfully registered and can login now') + return redirect(url_for('login')) + return render_template('register.html', error=error) + + +@app.route('/logout') +def logout(): + """Logs the user out.""" + flash('You were logged out') + session.pop('user_id', None) + return redirect(url_for('public_timeline')) + + +# add some filters to jinja +app.jinja_env.filters['datetimeformat'] = format_datetime +app.jinja_env.filters['gravatar'] = gravatar_url + + +if __name__ == '__main__': + app.run() diff --git a/examples/minitwit/minitwit_tests.py b/examples/minitwit/minitwit_tests.py new file mode 100644 index 00000000..87741165 --- /dev/null +++ b/examples/minitwit/minitwit_tests.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +""" + MiniTwit Tests + ~~~~~~~~~~~~~~ + + Tests the MiniTwit application. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import minitwit +import unittest +import tempfile + + +class MiniTwitTestCase(unittest.TestCase): + + def setUp(self): + """Before each test, set up a blank database""" + self.db_fd, minitwit.app.config['DATABASE'] = tempfile.mkstemp() + self.app = minitwit.app.test_client() + minitwit.init_db() + + def tearDown(self): + """Get rid of the database again after each test.""" + os.close(self.db_fd) + os.unlink(minitwit.app.config['DATABASE']) + + # helper functions + + def register(self, username, password, password2=None, email=None): + """Helper function to register a user""" + if password2 is None: + password2 = password + if email is None: + email = username + '@example.com' + return self.app.post('/register', data={ + 'username': username, + 'password': password, + 'password2': password2, + 'email': email, + }, follow_redirects=True) + + def login(self, username, password): + """Helper function to login""" + return self.app.post('/login', data={ + 'username': username, + 'password': password + }, follow_redirects=True) + + def register_and_login(self, username, password): + """Registers and logs in in one go""" + self.register(username, password) + return self.login(username, password) + + def logout(self): + """Helper function to logout""" + return self.app.get('/logout', follow_redirects=True) + + def add_message(self, text): + """Records a message""" + rv = self.app.post('/add_message', data={'text': text}, + follow_redirects=True) + if text: + assert 'Your message was recorded' in rv.data + return rv + + # testing functions + + def test_register(self): + """Make sure registering works""" + rv = self.register('user1', 'default') + assert 'You were successfully registered ' \ + 'and can login now' in rv.data + rv = self.register('user1', 'default') + assert 'The username is already taken' in rv.data + rv = self.register('', 'default') + assert 'You have to enter a username' in rv.data + rv = self.register('meh', '') + assert 'You have to enter a password' in rv.data + rv = self.register('meh', 'x', 'y') + assert 'The two passwords do not match' in rv.data + rv = self.register('meh', 'foo', email='broken') + assert 'You have to enter a valid email address' in rv.data + + def test_login_logout(self): + """Make sure logging in and logging out works""" + rv = self.register_and_login('user1', 'default') + assert 'You were logged in' in rv.data + rv = self.logout() + assert 'You were logged out' in rv.data + rv = self.login('user1', 'wrongpassword') + assert 'Invalid password' in rv.data + rv = self.login('user2', 'wrongpassword') + assert 'Invalid username' in rv.data + + def test_message_recording(self): + """Check if adding messages works""" + self.register_and_login('foo', 'default') + self.add_message('test message 1') + self.add_message('') + rv = self.app.get('/') + assert 'test message 1' in rv.data + assert '<test message 2>' in rv.data + + def test_timelines(self): + """Make sure that timelines work""" + self.register_and_login('foo', 'default') + self.add_message('the message by foo') + self.logout() + self.register_and_login('bar', 'default') + self.add_message('the message by bar') + rv = self.app.get('/public') + assert 'the message by foo' in rv.data + assert 'the message by bar' in rv.data + + # bar's timeline should just show bar's message + rv = self.app.get('/') + assert 'the message by foo' not in rv.data + assert 'the message by bar' in rv.data + + # now let's follow foo + rv = self.app.get('/foo/follow', follow_redirects=True) + assert 'You are now following "foo"' in rv.data + + # we should now see foo's message + rv = self.app.get('/') + assert 'the message by foo' in rv.data + assert 'the message by bar' in rv.data + + # but on the user's page we only want the user's message + rv = self.app.get('/bar') + assert 'the message by foo' not in rv.data + assert 'the message by bar' in rv.data + rv = self.app.get('/foo') + assert 'the message by foo' in rv.data + assert 'the message by bar' not in rv.data + + # now unfollow and check if that worked + rv = self.app.get('/foo/unfollow', follow_redirects=True) + assert 'You are no longer following "foo"' in rv.data + rv = self.app.get('/') + assert 'the message by foo' not in rv.data + assert 'the message by bar' in rv.data + + +if __name__ == '__main__': + unittest.main() diff --git a/examples/minitwit/schema.sql b/examples/minitwit/schema.sql new file mode 100644 index 00000000..b64afbed --- /dev/null +++ b/examples/minitwit/schema.sql @@ -0,0 +1,21 @@ +drop table if exists user; +create table user ( + user_id integer primary key autoincrement, + username string not null, + email string not null, + pw_hash string not null +); + +drop table if exists follower; +create table follower ( + who_id integer, + whom_id integer +); + +drop table if exists message; +create table message ( + message_id integer primary key autoincrement, + author_id integer not null, + text string not null, + pub_date integer +); diff --git a/examples/minitwit/static/style.css b/examples/minitwit/static/style.css new file mode 100644 index 00000000..ebbed8c9 --- /dev/null +++ b/examples/minitwit/static/style.css @@ -0,0 +1,178 @@ +body { + background: #CAECE9; + font-family: 'Trebuchet MS', sans-serif; + font-size: 14px; +} + +a { + color: #26776F; +} + +a:hover { + color: #333; +} + +input[type="text"], +input[type="password"] { + background: white; + border: 1px solid #BFE6E2; + padding: 2px; + font-family: 'Trebuchet MS', sans-serif; + font-size: 14px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + color: #105751; +} + +input[type="submit"] { + background: #105751; + border: 1px solid #073B36; + padding: 1px 3px; + font-family: 'Trebuchet MS', sans-serif; + font-size: 14px; + font-weight: bold; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + color: white; +} + +div.page { + background: white; + border: 1px solid #6ECCC4; + width: 700px; + margin: 30px auto; +} + +div.page h1 { + background: #6ECCC4; + margin: 0; + padding: 10px 14px; + color: white; + letter-spacing: 1px; + text-shadow: 0 0 3px #24776F; + font-weight: normal; +} + +div.page div.navigation { + background: #DEE9E8; + padding: 4px 10px; + border-top: 1px solid #ccc; + border-bottom: 1px solid #eee; + color: #888; + font-size: 12px; + letter-spacing: 0.5px; +} + +div.page div.navigation a { + color: #444; + font-weight: bold; +} + +div.page h2 { + margin: 0 0 15px 0; + color: #105751; + text-shadow: 0 1px 2px #ccc; +} + +div.page div.body { + padding: 10px; +} + +div.page div.footer { + background: #eee; + color: #888; + padding: 5px 10px; + font-size: 12px; +} + +div.page div.followstatus { + border: 1px solid #ccc; + background: #E3EBEA; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + padding: 3px; + font-size: 13px; +} + +div.page ul.messages { + list-style: none; + margin: 0; + padding: 0; +} + +div.page ul.messages li { + margin: 10px 0; + padding: 5px; + background: #F0FAF9; + border: 1px solid #DBF3F1; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + min-height: 48px; +} + +div.page ul.messages p { + margin: 0; +} + +div.page ul.messages li img { + float: left; + padding: 0 10px 0 0; +} + +div.page ul.messages li small { + font-size: 0.9em; + color: #888; +} + +div.page div.twitbox { + margin: 10px 0; + padding: 5px; + background: #F0FAF9; + border: 1px solid #94E2DA; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +div.page div.twitbox h3 { + margin: 0; + font-size: 1em; + color: #2C7E76; +} + +div.page div.twitbox p { + margin: 0; +} + +div.page div.twitbox input[type="text"] { + width: 585px; +} + +div.page div.twitbox input[type="submit"] { + width: 70px; + margin-left: 5px; +} + +ul.flashes { + list-style: none; + margin: 10px 10px 0 10px; + padding: 0; +} + +ul.flashes li { + background: #B9F3ED; + border: 1px solid #81CEC6; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + padding: 4px; + font-size: 13px; +} + +div.error { + margin: 10px 0; + background: #FAE4E4; + border: 1px solid #DD6F6F; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + padding: 4px; + font-size: 13px; +} diff --git a/examples/minitwit/templates/layout.html b/examples/minitwit/templates/layout.html new file mode 100644 index 00000000..668e3895 --- /dev/null +++ b/examples/minitwit/templates/layout.html @@ -0,0 +1,32 @@ + +{% block title %}Welcome{% endblock %} | MiniTwit + +

+

MiniTwit

+ + {% with flashes = get_flashed_messages() %} + {% if flashes %} +
    + {% for message in flashes %} +
  • {{ message }} + {% endfor %} +
+ {% endif %} + {% endwith %} +
+ {% block body %}{% endblock %} +
+ +
diff --git a/examples/minitwit/templates/login.html b/examples/minitwit/templates/login.html new file mode 100644 index 00000000..ae776714 --- /dev/null +++ b/examples/minitwit/templates/login.html @@ -0,0 +1,16 @@ +{% extends "layout.html" %} +{% block title %}Sign In{% endblock %} +{% block body %} +

Sign In

+ {% if error %}
Error: {{ error }}
{% endif %} +
+
+
Username: +
+
Password: +
+
+
+
+{% endblock %} + diff --git a/examples/minitwit/templates/register.html b/examples/minitwit/templates/register.html new file mode 100644 index 00000000..ccb345d5 --- /dev/null +++ b/examples/minitwit/templates/register.html @@ -0,0 +1,19 @@ +{% extends "layout.html" %} +{% block title %}Sign Up{% endblock %} +{% block body %} +

Sign Up

+ {% if error %}
Error: {{ error }}
{% endif %} +
+
+
Username: +
+
E-Mail: +
+
Password: +
+
Password (repeat): +
+
+
+
+{% endblock %} diff --git a/examples/minitwit/templates/timeline.html b/examples/minitwit/templates/timeline.html new file mode 100644 index 00000000..ea7d751b --- /dev/null +++ b/examples/minitwit/templates/timeline.html @@ -0,0 +1,49 @@ +{% extends "layout.html" %} +{% block title %} + {% if request.endpoint == 'public_timeline' %} + Public Timeline + {% elif request.endpoint == 'user_timeline' %} + {{ profile_user.username }}'s Timeline + {% else %} + My Timeline + {% endif %} +{% endblock %} +{% block body %} +

{{ self.title() }}

+ {% if g.user %} + {% if request.endpoint == 'user_timeline' %} +
+ {% if g.user.user_id == profile_user.user_id %} + This is you! + {% elif followed %} + You are currently following this user. + Unfollow user. + {% else %} + You are not yet following this user. + . + {% endif %} +
+ {% elif request.endpoint == 'timeline' %} +
+

What's on your mind {{ g.user.username }}?

+
+

+

+
+ {% endif %} + {% endif %} +
    + {% for message in messages %} +
  • + {{ message.username }} + {{ message.text }} + — {{ message.pub_date|datetimeformat }} + {% else %} +

  • There's no message so far. + {% endfor %} +
+{% endblock %} diff --git a/examples/tutorial/.gitignore b/examples/tutorial/.gitignore deleted file mode 100644 index a306afbc..00000000 --- a/examples/tutorial/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -.venv/ -*.pyc -__pycache__/ -instance/ -.cache/ -.pytest_cache/ -.coverage -htmlcov/ -dist/ -build/ -*.egg-info/ -.idea/ -*.swp -*~ diff --git a/examples/tutorial/LICENSE.txt b/examples/tutorial/LICENSE.txt deleted file mode 100644 index 9d227a0c..00000000 --- a/examples/tutorial/LICENSE.txt +++ /dev/null @@ -1,28 +0,0 @@ -Copyright 2010 Pallets - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/tutorial/README.rst b/examples/tutorial/README.rst deleted file mode 100644 index 653c2167..00000000 --- a/examples/tutorial/README.rst +++ /dev/null @@ -1,68 +0,0 @@ -Flaskr -====== - -The basic blog app built in the Flask `tutorial`_. - -.. _tutorial: https://flask.palletsprojects.com/tutorial/ - - -Install -------- - -**Be sure to use the same version of the code as the version of the docs -you're reading.** You probably want the latest tagged version, but the -default Git version is the main branch. :: - - # clone the repository - $ git clone https://github.com/pallets/flask - $ cd flask - # checkout the correct version - $ git tag # shows the tagged versions - $ git checkout latest-tag-found-above - $ cd examples/tutorial - -Create a virtualenv and activate it:: - - $ python3 -m venv .venv - $ . .venv/bin/activate - -Or on Windows cmd:: - - $ py -3 -m venv .venv - $ .venv\Scripts\activate.bat - -Install Flaskr:: - - $ pip install -e . - -Or if you are using the main branch, install Flask from source before -installing Flaskr:: - - $ pip install -e ../.. - $ pip install -e . - - -Run ---- - -.. code-block:: text - - $ flask --app flaskr init-db - $ flask --app flaskr run --debug - -Open http://127.0.0.1:5000 in a browser. - - -Test ----- - -:: - - $ pip install '.[test]' - $ pytest - -Run with coverage report:: - - $ coverage run -m pytest - $ coverage report - $ coverage html # open htmlcov/index.html in a browser diff --git a/examples/tutorial/flaskr/__init__.py b/examples/tutorial/flaskr/__init__.py deleted file mode 100644 index ab96e719..00000000 --- a/examples/tutorial/flaskr/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -import os - -from flask import Flask - - -def create_app(test_config=None): - """Create and configure an instance of the Flask application.""" - app = Flask(__name__, instance_relative_config=True) - app.config.from_mapping( - # a default secret that should be overridden by instance config - SECRET_KEY="dev", - # store the database in the instance folder - DATABASE=os.path.join(app.instance_path, "flaskr.sqlite"), - ) - - if test_config is None: - # load the instance config, if it exists, when not testing - app.config.from_pyfile("config.py", silent=True) - else: - # load the test config if passed in - app.config.update(test_config) - - # ensure the instance folder exists - os.makedirs(app.instance_path, exist_ok=True) - - @app.route("/hello") - def hello(): - return "Hello, World!" - - # register the database commands - from . import db - - db.init_app(app) - - # apply the blueprints to the app - from . import auth - from . import blog - - app.register_blueprint(auth.bp) - app.register_blueprint(blog.bp) - - # make url_for('index') == url_for('blog.index') - # in another app, you might define a separate main index here with - # app.route, while giving the blog blueprint a url_prefix, but for - # the tutorial the blog will be the main index - app.add_url_rule("/", endpoint="index") - - return app diff --git a/examples/tutorial/flaskr/auth.py b/examples/tutorial/flaskr/auth.py deleted file mode 100644 index 34c03a20..00000000 --- a/examples/tutorial/flaskr/auth.py +++ /dev/null @@ -1,116 +0,0 @@ -import functools - -from flask import Blueprint -from flask import flash -from flask import g -from flask import redirect -from flask import render_template -from flask import request -from flask import session -from flask import url_for -from werkzeug.security import check_password_hash -from werkzeug.security import generate_password_hash - -from .db import get_db - -bp = Blueprint("auth", __name__, url_prefix="/auth") - - -def login_required(view): - """View decorator that redirects anonymous users to the login page.""" - - @functools.wraps(view) - def wrapped_view(**kwargs): - if g.user is None: - return redirect(url_for("auth.login")) - - return view(**kwargs) - - return wrapped_view - - -@bp.before_app_request -def load_logged_in_user(): - """If a user id is stored in the session, load the user object from - the database into ``g.user``.""" - user_id = session.get("user_id") - - if user_id is None: - g.user = None - else: - g.user = ( - get_db().execute("SELECT * FROM user WHERE id = ?", (user_id,)).fetchone() - ) - - -@bp.route("/register", methods=("GET", "POST")) -def register(): - """Register a new user. - - Validates that the username is not already taken. Hashes the - password for security. - """ - if request.method == "POST": - username = request.form["username"] - password = request.form["password"] - db = get_db() - error = None - - if not username: - error = "Username is required." - elif not password: - error = "Password is required." - - if error is None: - try: - db.execute( - "INSERT INTO user (username, password) VALUES (?, ?)", - (username, generate_password_hash(password)), - ) - db.commit() - except db.IntegrityError: - # The username was already taken, which caused the - # commit to fail. Show a validation error. - error = f"User {username} is already registered." - else: - # Success, go to the login page. - return redirect(url_for("auth.login")) - - flash(error) - - return render_template("auth/register.html") - - -@bp.route("/login", methods=("GET", "POST")) -def login(): - """Log in a registered user by adding the user id to the session.""" - if request.method == "POST": - username = request.form["username"] - password = request.form["password"] - db = get_db() - error = None - user = db.execute( - "SELECT * FROM user WHERE username = ?", (username,) - ).fetchone() - - if user is None: - error = "Incorrect username." - elif not check_password_hash(user["password"], password): - error = "Incorrect password." - - if error is None: - # store the user id in a new session and return to the index - session.clear() - session["user_id"] = user["id"] - return redirect(url_for("index")) - - flash(error) - - return render_template("auth/login.html") - - -@bp.route("/logout") -def logout(): - """Clear the current session, including the stored user id.""" - session.clear() - return redirect(url_for("index")) diff --git a/examples/tutorial/flaskr/blog.py b/examples/tutorial/flaskr/blog.py deleted file mode 100644 index be0d92c4..00000000 --- a/examples/tutorial/flaskr/blog.py +++ /dev/null @@ -1,125 +0,0 @@ -from flask import Blueprint -from flask import flash -from flask import g -from flask import redirect -from flask import render_template -from flask import request -from flask import url_for -from werkzeug.exceptions import abort - -from .auth import login_required -from .db import get_db - -bp = Blueprint("blog", __name__) - - -@bp.route("/") -def index(): - """Show all the posts, most recent first.""" - db = get_db() - posts = db.execute( - "SELECT p.id, title, body, created, author_id, username" - " FROM post p JOIN user u ON p.author_id = u.id" - " ORDER BY created DESC" - ).fetchall() - return render_template("blog/index.html", posts=posts) - - -def get_post(id, check_author=True): - """Get a post and its author by id. - - Checks that the id exists and optionally that the current user is - the author. - - :param id: id of post to get - :param check_author: require the current user to be the author - :return: the post with author information - :raise 404: if a post with the given id doesn't exist - :raise 403: if the current user isn't the author - """ - post = ( - get_db() - .execute( - "SELECT p.id, title, body, created, author_id, username" - " FROM post p JOIN user u ON p.author_id = u.id" - " WHERE p.id = ?", - (id,), - ) - .fetchone() - ) - - if post is None: - abort(404, f"Post id {id} doesn't exist.") - - if check_author and post["author_id"] != g.user["id"]: - abort(403) - - return post - - -@bp.route("/create", methods=("GET", "POST")) -@login_required -def create(): - """Create a new post for the current user.""" - if request.method == "POST": - title = request.form["title"] - body = request.form["body"] - error = None - - if not title: - error = "Title is required." - - if error is not None: - flash(error) - else: - db = get_db() - db.execute( - "INSERT INTO post (title, body, author_id) VALUES (?, ?, ?)", - (title, body, g.user["id"]), - ) - db.commit() - return redirect(url_for("blog.index")) - - return render_template("blog/create.html") - - -@bp.route("//update", methods=("GET", "POST")) -@login_required -def update(id): - """Update a post if the current user is the author.""" - post = get_post(id) - - if request.method == "POST": - title = request.form["title"] - body = request.form["body"] - error = None - - if not title: - error = "Title is required." - - if error is not None: - flash(error) - else: - db = get_db() - db.execute( - "UPDATE post SET title = ?, body = ? WHERE id = ?", (title, body, id) - ) - db.commit() - return redirect(url_for("blog.index")) - - return render_template("blog/update.html", post=post) - - -@bp.route("//delete", methods=("POST",)) -@login_required -def delete(id): - """Delete a post. - - Ensures that the post exists and that the logged in user is the - author of the post. - """ - get_post(id) - db = get_db() - db.execute("DELETE FROM post WHERE id = ?", (id,)) - db.commit() - return redirect(url_for("blog.index")) diff --git a/examples/tutorial/flaskr/db.py b/examples/tutorial/flaskr/db.py deleted file mode 100644 index dec22fde..00000000 --- a/examples/tutorial/flaskr/db.py +++ /dev/null @@ -1,56 +0,0 @@ -import sqlite3 -from datetime import datetime - -import click -from flask import current_app -from flask import g - - -def get_db(): - """Connect to the application's configured database. The connection - is unique for each request and will be reused if this is called - again. - """ - if "db" not in g: - g.db = sqlite3.connect( - current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES - ) - g.db.row_factory = sqlite3.Row - - return g.db - - -def close_db(e=None): - """If this request connected to the database, close the - connection. - """ - db = g.pop("db", None) - - if db is not None: - db.close() - - -def init_db(): - """Clear existing data and create new tables.""" - db = get_db() - - with current_app.open_resource("schema.sql") as f: - db.executescript(f.read().decode("utf8")) - - -@click.command("init-db") -def init_db_command(): - """Clear existing data and create new tables.""" - init_db() - click.echo("Initialized the database.") - - -sqlite3.register_converter("timestamp", lambda v: datetime.fromisoformat(v.decode())) - - -def init_app(app): - """Register database functions with the Flask app. This is called by - the application factory. - """ - app.teardown_appcontext(close_db) - app.cli.add_command(init_db_command) diff --git a/examples/tutorial/flaskr/schema.sql b/examples/tutorial/flaskr/schema.sql deleted file mode 100644 index dd4c8660..00000000 --- a/examples/tutorial/flaskr/schema.sql +++ /dev/null @@ -1,20 +0,0 @@ --- Initialize the database. --- Drop any existing data and create empty tables. - -DROP TABLE IF EXISTS user; -DROP TABLE IF EXISTS post; - -CREATE TABLE user ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password TEXT NOT NULL -); - -CREATE TABLE post ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - author_id INTEGER NOT NULL, - created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - title TEXT NOT NULL, - body TEXT NOT NULL, - FOREIGN KEY (author_id) REFERENCES user (id) -); diff --git a/examples/tutorial/flaskr/static/style.css b/examples/tutorial/flaskr/static/style.css deleted file mode 100644 index 2f1f4d0c..00000000 --- a/examples/tutorial/flaskr/static/style.css +++ /dev/null @@ -1,134 +0,0 @@ -html { - font-family: sans-serif; - background: #eee; - padding: 1rem; -} - -body { - max-width: 960px; - margin: 0 auto; - background: white; -} - -h1, h2, h3, h4, h5, h6 { - font-family: serif; - color: #377ba8; - margin: 1rem 0; -} - -a { - color: #377ba8; -} - -hr { - border: none; - border-top: 1px solid lightgray; -} - -nav { - background: lightgray; - display: flex; - align-items: center; - padding: 0 0.5rem; -} - -nav h1 { - flex: auto; - margin: 0; -} - -nav h1 a { - text-decoration: none; - padding: 0.25rem 0.5rem; -} - -nav ul { - display: flex; - list-style: none; - margin: 0; - padding: 0; -} - -nav ul li a, nav ul li span, header .action { - display: block; - padding: 0.5rem; -} - -.content { - padding: 0 1rem 1rem; -} - -.content > header { - border-bottom: 1px solid lightgray; - display: flex; - align-items: flex-end; -} - -.content > header h1 { - flex: auto; - margin: 1rem 0 0.25rem 0; -} - -.flash { - margin: 1em 0; - padding: 1em; - background: #cae6f6; - border: 1px solid #377ba8; -} - -.post > header { - display: flex; - align-items: flex-end; - font-size: 0.85em; -} - -.post > header > div:first-of-type { - flex: auto; -} - -.post > header h1 { - font-size: 1.5em; - margin-bottom: 0; -} - -.post .about { - color: slategray; - font-style: italic; -} - -.post .body { - white-space: pre-line; -} - -.content:last-child { - margin-bottom: 0; -} - -.content form { - margin: 1em 0; - display: flex; - flex-direction: column; -} - -.content label { - font-weight: bold; - margin-bottom: 0.5em; -} - -.content input, .content textarea { - margin-bottom: 1em; -} - -.content textarea { - min-height: 12em; - resize: vertical; -} - -input.danger { - color: #cc2f2e; -} - -input[type=submit] { - align-self: start; - min-width: 10em; -} diff --git a/examples/tutorial/flaskr/templates/auth/login.html b/examples/tutorial/flaskr/templates/auth/login.html deleted file mode 100644 index b326b5a6..00000000 --- a/examples/tutorial/flaskr/templates/auth/login.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}Log In{% endblock %}

-{% endblock %} - -{% block content %} -
- - - - - -
-{% endblock %} diff --git a/examples/tutorial/flaskr/templates/auth/register.html b/examples/tutorial/flaskr/templates/auth/register.html deleted file mode 100644 index 4320e17e..00000000 --- a/examples/tutorial/flaskr/templates/auth/register.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}Register{% endblock %}

-{% endblock %} - -{% block content %} -
- - - - - -
-{% endblock %} diff --git a/examples/tutorial/flaskr/templates/base.html b/examples/tutorial/flaskr/templates/base.html deleted file mode 100644 index f09e9268..00000000 --- a/examples/tutorial/flaskr/templates/base.html +++ /dev/null @@ -1,24 +0,0 @@ - -{% block title %}{% endblock %} - Flaskr - - -
-
- {% block header %}{% endblock %} -
- {% for message in get_flashed_messages() %} -
{{ message }}
- {% endfor %} - {% block content %}{% endblock %} -
diff --git a/examples/tutorial/flaskr/templates/blog/create.html b/examples/tutorial/flaskr/templates/blog/create.html deleted file mode 100644 index 88e31e44..00000000 --- a/examples/tutorial/flaskr/templates/blog/create.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}New Post{% endblock %}

-{% endblock %} - -{% block content %} -
- - - - - -
-{% endblock %} diff --git a/examples/tutorial/flaskr/templates/blog/index.html b/examples/tutorial/flaskr/templates/blog/index.html deleted file mode 100644 index 3481b8e1..00000000 --- a/examples/tutorial/flaskr/templates/blog/index.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}Posts{% endblock %}

- {% if g.user %} - New - {% endif %} -{% endblock %} - -{% block content %} - {% for post in posts %} -
-
-
-

{{ post['title'] }}

-
by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}
-
- {% if g.user['id'] == post['author_id'] %} - Edit - {% endif %} -
-

{{ post['body'] }}

-
- {% if not loop.last %} -
- {% endif %} - {% endfor %} -{% endblock %} diff --git a/examples/tutorial/flaskr/templates/blog/update.html b/examples/tutorial/flaskr/templates/blog/update.html deleted file mode 100644 index 2c405e63..00000000 --- a/examples/tutorial/flaskr/templates/blog/update.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'base.html' %} - -{% block header %} -

{% block title %}Edit "{{ post['title'] }}"{% endblock %}

-{% endblock %} - -{% block content %} -
- - - - - -
-
-
- -
-{% endblock %} diff --git a/examples/tutorial/pyproject.toml b/examples/tutorial/pyproject.toml deleted file mode 100644 index ca31e53d..00000000 --- a/examples/tutorial/pyproject.toml +++ /dev/null @@ -1,40 +0,0 @@ -[project] -name = "flaskr" -version = "1.0.0" -description = "The basic blog app built in the Flask tutorial." -readme = "README.rst" -license = {file = "LICENSE.txt"} -maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] -classifiers = ["Private :: Do Not Upload"] -dependencies = [ - "flask", -] - -[project.urls] -Documentation = "https://flask.palletsprojects.com/tutorial/" - -[project.optional-dependencies] -test = ["pytest"] - -[build-system] -requires = ["flit_core<4"] -build-backend = "flit_core.buildapi" - -[tool.flit.module] -name = "flaskr" - -[tool.flit.sdist] -include = [ - "tests/", -] - -[tool.pytest.ini_options] -testpaths = ["tests"] -filterwarnings = ["error"] - -[tool.coverage.run] -branch = true -source = ["flaskr", "tests"] - -[tool.ruff] -src = ["src"] diff --git a/examples/tutorial/tests/conftest.py b/examples/tutorial/tests/conftest.py deleted file mode 100644 index 6bf62f0a..00000000 --- a/examples/tutorial/tests/conftest.py +++ /dev/null @@ -1,62 +0,0 @@ -import os -import tempfile - -import pytest - -from flaskr import create_app -from flaskr.db import get_db -from flaskr.db import init_db - -# read in SQL for populating test data -with open(os.path.join(os.path.dirname(__file__), "data.sql"), "rb") as f: - _data_sql = f.read().decode("utf8") - - -@pytest.fixture -def app(): - """Create and configure a new app instance for each test.""" - # create a temporary file to isolate the database for each test - db_fd, db_path = tempfile.mkstemp() - # create the app with common test config - app = create_app({"TESTING": True, "DATABASE": db_path}) - - # create the database and load test data - with app.app_context(): - init_db() - get_db().executescript(_data_sql) - - yield app - - # close and remove the temporary database - os.close(db_fd) - os.unlink(db_path) - - -@pytest.fixture -def client(app): - """A test client for the app.""" - return app.test_client() - - -@pytest.fixture -def runner(app): - """A test runner for the app's Click commands.""" - return app.test_cli_runner() - - -class AuthActions: - def __init__(self, client): - self._client = client - - def login(self, username="test", password="test"): - return self._client.post( - "/auth/login", data={"username": username, "password": password} - ) - - def logout(self): - return self._client.get("/auth/logout") - - -@pytest.fixture -def auth(client): - return AuthActions(client) diff --git a/examples/tutorial/tests/data.sql b/examples/tutorial/tests/data.sql deleted file mode 100644 index 9b680065..00000000 --- a/examples/tutorial/tests/data.sql +++ /dev/null @@ -1,8 +0,0 @@ -INSERT INTO user (username, password) -VALUES - ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'), - ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79'); - -INSERT INTO post (title, body, author_id, created) -VALUES - ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00'); diff --git a/examples/tutorial/tests/test_auth.py b/examples/tutorial/tests/test_auth.py deleted file mode 100644 index 76db62f7..00000000 --- a/examples/tutorial/tests/test_auth.py +++ /dev/null @@ -1,69 +0,0 @@ -import pytest -from flask import g -from flask import session - -from flaskr.db import get_db - - -def test_register(client, app): - # test that viewing the page renders without template errors - assert client.get("/auth/register").status_code == 200 - - # test that successful registration redirects to the login page - response = client.post("/auth/register", data={"username": "a", "password": "a"}) - assert response.headers["Location"] == "/auth/login" - - # test that the user was inserted into the database - with app.app_context(): - assert ( - get_db().execute("SELECT * FROM user WHERE username = 'a'").fetchone() - is not None - ) - - -@pytest.mark.parametrize( - ("username", "password", "message"), - ( - ("", "", b"Username is required."), - ("a", "", b"Password is required."), - ("test", "test", b"already registered"), - ), -) -def test_register_validate_input(client, username, password, message): - response = client.post( - "/auth/register", data={"username": username, "password": password} - ) - assert message in response.data - - -def test_login(client, auth): - # test that viewing the page renders without template errors - assert client.get("/auth/login").status_code == 200 - - # test that successful login redirects to the index page - response = auth.login() - assert response.headers["Location"] == "/" - - # login request set the user_id in the session - # check that the user is loaded from the session - with client: - client.get("/") - assert session["user_id"] == 1 - assert g.user["username"] == "test" - - -@pytest.mark.parametrize( - ("username", "password", "message"), - (("a", "test", b"Incorrect username."), ("test", "a", b"Incorrect password.")), -) -def test_login_validate_input(auth, username, password, message): - response = auth.login(username, password) - assert message in response.data - - -def test_logout(client, auth): - auth.login() - - with client: - auth.logout() - assert "user_id" not in session diff --git a/examples/tutorial/tests/test_blog.py b/examples/tutorial/tests/test_blog.py deleted file mode 100644 index 55c769d8..00000000 --- a/examples/tutorial/tests/test_blog.py +++ /dev/null @@ -1,83 +0,0 @@ -import pytest - -from flaskr.db import get_db - - -def test_index(client, auth): - response = client.get("/") - assert b"Log In" in response.data - assert b"Register" in response.data - - auth.login() - response = client.get("/") - assert b"test title" in response.data - assert b"by test on 2018-01-01" in response.data - assert b"test\nbody" in response.data - assert b'href="/1/update"' in response.data - - -@pytest.mark.parametrize("path", ("/create", "/1/update", "/1/delete")) -def test_login_required(client, path): - response = client.post(path) - assert response.headers["Location"] == "/auth/login" - - -def test_author_required(app, client, auth): - # change the post author to another user - with app.app_context(): - db = get_db() - db.execute("UPDATE post SET author_id = 2 WHERE id = 1") - db.commit() - - auth.login() - # current user can't modify other user's post - assert client.post("/1/update").status_code == 403 - assert client.post("/1/delete").status_code == 403 - # current user doesn't see edit link - assert b'href="/1/update"' not in client.get("/").data - - -@pytest.mark.parametrize("path", ("/2/update", "/2/delete")) -def test_exists_required(client, auth, path): - auth.login() - assert client.post(path).status_code == 404 - - -def test_create(client, auth, app): - auth.login() - assert client.get("/create").status_code == 200 - client.post("/create", data={"title": "created", "body": ""}) - - with app.app_context(): - db = get_db() - count = db.execute("SELECT COUNT(id) FROM post").fetchone()[0] - assert count == 2 - - -def test_update(client, auth, app): - auth.login() - assert client.get("/1/update").status_code == 200 - client.post("/1/update", data={"title": "updated", "body": ""}) - - with app.app_context(): - db = get_db() - post = db.execute("SELECT * FROM post WHERE id = 1").fetchone() - assert post["title"] == "updated" - - -@pytest.mark.parametrize("path", ("/create", "/1/update")) -def test_create_update_validate(client, auth, path): - auth.login() - response = client.post(path, data={"title": "", "body": ""}) - assert b"Title is required." in response.data - - -def test_delete(client, auth, app): - auth.login() - response = client.post("/1/delete") - assert response.headers["Location"] == "/" - - with app.app_context(): - db = get_db() - post = db.execute("SELECT * FROM post WHERE id = 1").fetchone() - assert post is None diff --git a/examples/tutorial/tests/test_db.py b/examples/tutorial/tests/test_db.py deleted file mode 100644 index 2363bf81..00000000 --- a/examples/tutorial/tests/test_db.py +++ /dev/null @@ -1,29 +0,0 @@ -import sqlite3 - -import pytest - -from flaskr.db import get_db - - -def test_get_close_db(app): - with app.app_context(): - db = get_db() - assert db is get_db() - - with pytest.raises(sqlite3.ProgrammingError) as e: - db.execute("SELECT 1") - - assert "closed" in str(e.value) - - -def test_init_db_command(runner, monkeypatch): - class Recorder: - called = False - - def fake_init_db(): - Recorder.called = True - - monkeypatch.setattr("flaskr.db.init_db", fake_init_db) - result = runner.invoke(args=["init-db"]) - assert "Initialized" in result.output - assert Recorder.called diff --git a/examples/tutorial/tests/test_factory.py b/examples/tutorial/tests/test_factory.py deleted file mode 100644 index 9b7ca57f..00000000 --- a/examples/tutorial/tests/test_factory.py +++ /dev/null @@ -1,12 +0,0 @@ -from flaskr import create_app - - -def test_config(): - """Test create_app without passing test config.""" - assert not create_app().testing - assert create_app({"TESTING": True}).testing - - -def test_hello(client): - response = client.get("/hello") - assert response.data == b"Hello, World!" diff --git a/flask/__init__.py b/flask/__init__.py new file mode 100644 index 00000000..98fcbe13 --- /dev/null +++ b/flask/__init__.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" + flask + ~~~~~ + + A microframework based on Werkzeug. It's extensively documented + and follows best practice patterns. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +__version__ = '0.8.1' + +# utilities we import from Werkzeug and Jinja2 that are unused +# in the module but are exported as public interface. +from werkzeug.exceptions import abort +from werkzeug.utils import redirect +from jinja2 import Markup, escape + +from .app import Flask, Request, Response +from .config import Config +from .helpers import url_for, jsonify, json_available, flash, \ + send_file, send_from_directory, get_flashed_messages, \ + get_template_attribute, make_response, safe_join +from .globals import current_app, g, request, session, _request_ctx_stack +from .ctx import has_request_context +from .module import Module +from .blueprints import Blueprint +from .templating import render_template, render_template_string + +# the signals +from .signals import signals_available, template_rendered, request_started, \ + request_finished, got_request_exception, request_tearing_down + +# only import json if it's available +if json_available: + from .helpers import json + +# backwards compat, goes away in 1.0 +from .sessions import SecureCookieSession as Session diff --git a/flask/app.py b/flask/app.py new file mode 100644 index 00000000..ebf4e6a6 --- /dev/null +++ b/flask/app.py @@ -0,0 +1,1518 @@ +# -*- coding: utf-8 -*- +""" + flask.app + ~~~~~~~~~ + + This module implements the central WSGI application object. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import os +import sys +from threading import Lock +from datetime import timedelta +from itertools import chain +from functools import update_wrapper + +from werkzeug.datastructures import ImmutableDict +from werkzeug.routing import Map, Rule, RequestRedirect +from werkzeug.exceptions import HTTPException, InternalServerError, \ + MethodNotAllowed, BadRequest + +from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ + locked_cached_property, _tojson_filter, _endpoint_from_view_func, \ + find_package +from .wrappers import Request, Response +from .config import ConfigAttribute, Config +from .ctx import RequestContext +from .globals import _request_ctx_stack, request +from .sessions import SecureCookieSessionInterface +from .module import blueprint_is_module +from .templating import DispatchingJinjaLoader, Environment, \ + _default_template_ctx_processor +from .signals import request_started, request_finished, got_request_exception, \ + request_tearing_down + +# a lock used for logger initialization +_logger_lock = Lock() + + +def _make_timedelta(value): + if not isinstance(value, timedelta): + return timedelta(seconds=value) + return value + + +def setupmethod(f): + """Wraps a method so that it performs a check in debug mode if the + first request was already handled. + """ + def wrapper_func(self, *args, **kwargs): + if self.debug and self._got_first_request: + raise AssertionError('A setup function was called after the ' + 'first request was handled. This usually indicates a bug ' + 'in the application where a module was not imported ' + 'and decorators or other functionality was called too late.\n' + 'To fix this make sure to import all your view modules, ' + 'database models and everything related at a central place ' + 'before the application starts serving requests.') + return f(self, *args, **kwargs) + return update_wrapper(wrapper_func, f) + + +class Flask(_PackageBoundObject): + """The flask object implements a WSGI application and acts as the central + object. It is passed the name of the module or package of the + application. Once it is created it will act as a central registry for + the view functions, the URL rules, template configuration and much more. + + The name of the package is used to resolve resources from inside the + package or the folder the module is contained in depending on if the + package parameter resolves to an actual python package (a folder with + an `__init__.py` file inside) or a standard module (just a `.py` file). + + For more information about resource loading, see :func:`open_resource`. + + Usually you create a :class:`Flask` instance in your main module or + in the `__init__.py` file of your package like this:: + + from flask import Flask + app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea what + belongs to your application. This name is used to find resources + on the file system, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in `yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplication.app` and not + `yourapplication.views.frontend`) + + .. versionadded:: 0.7 + The `static_url_path`, `static_folder`, and `template_folder` + parameters were added. + + .. versionadded:: 0.8 + The `instance_path` and `instance_relative_config` parameters were + added. + + :param import_name: the name of the application package + :param static_url_path: can be used to specify a different path for the + static files on the web. Defaults to the name + of the `static_folder` folder. + :param static_folder: the folder with static files that should be served + at `static_url_path`. Defaults to the ``'static'`` + folder in the root path of the application. + :param template_folder: the folder that contains the templates that should + be used by the application. Defaults to + ``'templates'`` folder in the root path of the + application. + :param instance_path: An alternative instance path for the application. + By default the folder ``'instance'`` next to the + package or module is assumed to be the instance + path. + :param instance_relative_config: if set to `True` relative filenames + for loading the config are assumed to + be relative to the instance path instead + of the application root. + """ + + #: The class that is used for request objects. See :class:`~flask.Request` + #: for more information. + request_class = Request + + #: The class that is used for response objects. See + #: :class:`~flask.Response` for more information. + response_class = Response + + #: The debug flag. Set this to `True` to enable debugging of the + #: application. In debug mode the debugger will kick in when an unhandled + #: exception ocurrs and the integrated server will automatically reload + #: the application if changes in the code are detected. + #: + #: This attribute can also be configured from the config with the `DEBUG` + #: configuration key. Defaults to `False`. + debug = ConfigAttribute('DEBUG') + + #: The testing flag. Set this to `True` to enable the test mode of + #: Flask extensions (and in the future probably also Flask itself). + #: For example this might activate unittest helpers that have an + #: additional runtime cost which should not be enabled by default. + #: + #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the + #: default it's implicitly enabled. + #: + #: This attribute can also be configured from the config with the + #: `TESTING` configuration key. Defaults to `False`. + testing = ConfigAttribute('TESTING') + + #: If a secret key is set, cryptographic components can use this to + #: sign cookies and other things. Set this to a complex random value + #: when you want to use the secure cookie for instance. + #: + #: This attribute can also be configured from the config with the + #: `SECRET_KEY` configuration key. Defaults to `None`. + secret_key = ConfigAttribute('SECRET_KEY') + + #: The secure cookie uses this for the name of the session cookie. + #: + #: This attribute can also be configured from the config with the + #: `SESSION_COOKIE_NAME` configuration key. Defaults to ``'session'`` + session_cookie_name = ConfigAttribute('SESSION_COOKIE_NAME') + + #: A :class:`~datetime.timedelta` which is used to set the expiration + #: date of a permanent session. The default is 31 days which makes a + #: permanent session survive for roughly one month. + #: + #: This attribute can also be configured from the config with the + #: `PERMANENT_SESSION_LIFETIME` configuration key. Defaults to + #: ``timedelta(days=31)`` + permanent_session_lifetime = ConfigAttribute('PERMANENT_SESSION_LIFETIME', + get_converter=_make_timedelta) + + #: Enable this if you want to use the X-Sendfile feature. Keep in + #: mind that the server has to support this. This only affects files + #: sent with the :func:`send_file` method. + #: + #: .. versionadded:: 0.2 + #: + #: This attribute can also be configured from the config with the + #: `USE_X_SENDFILE` configuration key. Defaults to `False`. + use_x_sendfile = ConfigAttribute('USE_X_SENDFILE') + + #: The name of the logger to use. By default the logger name is the + #: package name passed to the constructor. + #: + #: .. versionadded:: 0.4 + logger_name = ConfigAttribute('LOGGER_NAME') + + #: Enable the deprecated module support? This is active by default + #: in 0.7 but will be changed to False in 0.8. With Flask 1.0 modules + #: will be removed in favor of Blueprints + enable_modules = True + + #: The logging format used for the debug logger. This is only used when + #: the application is in debug mode, otherwise the attached logging + #: handler does the formatting. + #: + #: .. versionadded:: 0.3 + debug_log_format = ( + '-' * 80 + '\n' + + '%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' + + '%(message)s\n' + + '-' * 80 + ) + + #: Options that are passed directly to the Jinja2 environment. + jinja_options = ImmutableDict( + extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] + ) + + #: Default configuration parameters. + default_config = ImmutableDict({ + 'DEBUG': False, + 'TESTING': False, + 'PROPAGATE_EXCEPTIONS': None, + 'PRESERVE_CONTEXT_ON_EXCEPTION': None, + 'SECRET_KEY': None, + 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), + 'USE_X_SENDFILE': False, + 'LOGGER_NAME': None, + 'SERVER_NAME': None, + 'APPLICATION_ROOT': None, + 'SESSION_COOKIE_NAME': 'session', + 'SESSION_COOKIE_DOMAIN': None, + 'SESSION_COOKIE_PATH': None, + 'SESSION_COOKIE_HTTPONLY': True, + 'SESSION_COOKIE_SECURE': False, + 'MAX_CONTENT_LENGTH': None, + 'TRAP_BAD_REQUEST_ERRORS': False, + 'TRAP_HTTP_EXCEPTIONS': False + }) + + #: The rule object to use for URL rules created. This is used by + #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. + #: + #: .. versionadded:: 0.7 + url_rule_class = Rule + + #: the test client that is used with when `test_client` is used. + #: + #: .. versionadded:: 0.7 + test_client_class = None + + #: the session interface to use. By default an instance of + #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. + #: + #: .. versionadded:: 0.8 + session_interface = SecureCookieSessionInterface() + + def __init__(self, import_name, static_path=None, static_url_path=None, + static_folder='static', template_folder='templates', + instance_path=None, instance_relative_config=False): + _PackageBoundObject.__init__(self, import_name, + template_folder=template_folder) + if static_path is not None: + from warnings import warn + warn(DeprecationWarning('static_path is now called ' + 'static_url_path'), stacklevel=2) + static_url_path = static_path + + if static_url_path is not None: + self.static_url_path = static_url_path + if static_folder is not None: + self.static_folder = static_folder + if instance_path is None: + instance_path = self.auto_find_instance_path() + elif not os.path.isabs(instance_path): + raise ValueError('If an instance path is provided it must be ' + 'absolute. A relative path was given instead.') + + #: Holds the path to the instance folder. + #: + #: .. versionadded:: 0.8 + self.instance_path = instance_path + + #: The configuration dictionary as :class:`Config`. This behaves + #: exactly like a regular dictionary but supports additional methods + #: to load a config from files. + self.config = self.make_config(instance_relative_config) + + # Prepare the deferred setup of the logger. + self._logger = None + self.logger_name = self.import_name + + #: A dictionary of all view functions registered. The keys will + #: be function names which are also used to generate URLs and + #: the values are the function objects themselves. + #: To register a view function, use the :meth:`route` decorator. + self.view_functions = {} + + # support for the now deprecated `error_handlers` attribute. The + # :attr:`error_handler_spec` shall be used now. + self._error_handlers = {} + + #: A dictionary of all registered error handlers. The key is `None` + #: for error handlers active on the application, otherwise the key is + #: the name of the blueprint. Each key points to another dictionary + #: where they key is the status code of the http exception. The + #: special key `None` points to a list of tuples where the first item + #: is the class for the instance check and the second the error handler + #: function. + #: + #: To register a error handler, use the :meth:`errorhandler` + #: decorator. + self.error_handler_spec = {None: self._error_handlers} + + #: A dictionary with lists of functions that should be called at the + #: beginning of the request. The key of the dictionary is the name of + #: the blueprint this function is active for, `None` for all requests. + #: This can for example be used to open database connections or + #: getting hold of the currently logged in user. To register a + #: function here, use the :meth:`before_request` decorator. + self.before_request_funcs = {} + + #: A lists of functions that should be called at the beginning of the + #: first request to this instance. To register a function here, use + #: the :meth:`before_first_request` decorator. + #: + #: .. versionadded:: 0.8 + self.before_first_request_funcs = [] + + #: A dictionary with lists of functions that should be called after + #: each request. The key of the dictionary is the name of the blueprint + #: this function is active for, `None` for all requests. This can for + #: example be used to open database connections or getting hold of the + #: currently logged in user. To register a function here, use the + #: :meth:`after_request` decorator. + self.after_request_funcs = {} + + #: A dictionary with lists of functions that are called after + #: each request, even if an exception has occurred. The key of the + #: dictionary is the name of the blueprint this function is active for, + #: `None` for all requests. These functions are not allowed to modify + #: the request, and their return values are ignored. If an exception + #: occurred while processing the request, it gets passed to each + #: teardown_request function. To register a function here, use the + #: :meth:`teardown_request` decorator. + #: + #: .. versionadded:: 0.7 + self.teardown_request_funcs = {} + + #: A dictionary with lists of functions that can be used as URL + #: value processor functions. Whenever a URL is built these functions + #: are called to modify the dictionary of values in place. The key + #: `None` here is used for application wide + #: callbacks, otherwise the key is the name of the blueprint. + #: Each of these functions has the chance to modify the dictionary + #: + #: .. versionadded:: 0.7 + self.url_value_preprocessors = {} + + #: A dictionary with lists of functions that can be used as URL value + #: preprocessors. The key `None` here is used for application wide + #: callbacks, otherwise the key is the name of the blueprint. + #: Each of these functions has the chance to modify the dictionary + #: of URL values before they are used as the keyword arguments of the + #: view function. For each function registered this one should also + #: provide a :meth:`url_defaults` function that adds the parameters + #: automatically again that were removed that way. + #: + #: .. versionadded:: 0.7 + self.url_default_functions = {} + + #: A dictionary with list of functions that are called without argument + #: to populate the template context. The key of the dictionary is the + #: name of the blueprint this function is active for, `None` for all + #: requests. Each returns a dictionary that the template context is + #: updated with. To register a function here, use the + #: :meth:`context_processor` decorator. + self.template_context_processors = { + None: [_default_template_ctx_processor] + } + + #: all the attached blueprints in a directory by name. Blueprints + #: can be attached multiple times so this dictionary does not tell + #: you how often they got attached. + #: + #: .. versionadded:: 0.7 + self.blueprints = {} + + #: a place where extensions can store application specific state. For + #: example this is where an extension could store database engines and + #: similar things. For backwards compatibility extensions should register + #: themselves like this:: + #: + #: if not hasattr(app, 'extensions'): + #: app.extensions = {} + #: app.extensions['extensionname'] = SomeObject() + #: + #: The key must match the name of the `flaskext` module. For example in + #: case of a "Flask-Foo" extension in `flaskext.foo`, the key would be + #: ``'foo'``. + #: + #: .. versionadded:: 0.7 + self.extensions = {} + + #: The :class:`~werkzeug.routing.Map` for this instance. You can use + #: this to change the routing converters after the class was created + #: but before any routes are connected. Example:: + #: + #: from werkzeug.routing import BaseConverter + #: + #: class ListConverter(BaseConverter): + #: def to_python(self, value): + #: return value.split(',') + #: def to_url(self, values): + #: return ','.join(BaseConverter.to_url(value) + #: for value in values) + #: + #: app = Flask(__name__) + #: app.url_map.converters['list'] = ListConverter + self.url_map = Map() + + # tracks internally if the application already handled at least one + # request. + self._got_first_request = False + self._before_request_lock = Lock() + + # register the static folder for the application. Do that even + # if the folder does not exist. First of all it might be created + # while the server is running (usually happens during development) + # but also because google appengine stores static files somewhere + # else when mapped with the .yml file. + if self.has_static_folder: + self.add_url_rule(self.static_url_path + '/', + endpoint='static', + view_func=self.send_static_file) + + def _get_error_handlers(self): + from warnings import warn + warn(DeprecationWarning('error_handlers is deprecated, use the ' + 'new error_handler_spec attribute instead.'), stacklevel=1) + return self._error_handlers + def _set_error_handlers(self, value): + self._error_handlers = value + self.error_handler_spec[None] = value + error_handlers = property(_get_error_handlers, _set_error_handlers) + del _get_error_handlers, _set_error_handlers + + @locked_cached_property + def name(self): + """The name of the application. This is usually the import name + with the difference that it's guessed from the run file if the + import name is main. This name is used as a display name when + Flask needs the name of the application. It can be set and overriden + to change the value. + + .. versionadded:: 0.8 + """ + if self.import_name == '__main__': + fn = getattr(sys.modules['__main__'], '__file__', None) + if fn is None: + return '__main__' + return os.path.splitext(os.path.basename(fn))[0] + return self.import_name + + @property + def propagate_exceptions(self): + """Returns the value of the `PROPAGATE_EXCEPTIONS` configuration + value in case it's set, otherwise a sensible default is returned. + + .. versionadded:: 0.7 + """ + rv = self.config['PROPAGATE_EXCEPTIONS'] + if rv is not None: + return rv + return self.testing or self.debug + + @property + def preserve_context_on_exception(self): + """Returns the value of the `PRESERVE_CONTEXT_ON_EXCEPTION` + configuration value in case it's set, otherwise a sensible default + is returned. + + .. versionadded:: 0.7 + """ + rv = self.config['PRESERVE_CONTEXT_ON_EXCEPTION'] + if rv is not None: + return rv + return self.debug + + @property + def logger(self): + """A :class:`logging.Logger` object for this application. The + default configuration is to log to stderr if the application is + in debug mode. This logger can be used to (surprise) log messages. + Here some examples:: + + app.logger.debug('A value for debugging') + app.logger.warning('A warning ocurred (%d apples)', 42) + app.logger.error('An error occoured') + + .. versionadded:: 0.3 + """ + if self._logger and self._logger.name == self.logger_name: + return self._logger + with _logger_lock: + if self._logger and self._logger.name == self.logger_name: + return self._logger + from flask.logging import create_logger + self._logger = rv = create_logger(self) + return rv + + @locked_cached_property + def jinja_env(self): + """The Jinja2 environment used to load templates.""" + rv = self.create_jinja_environment() + + # Hack to support the init_jinja_globals method which is supported + # until 1.0 but has an API deficiency. + if getattr(self.init_jinja_globals, 'im_func', None) is not \ + Flask.init_jinja_globals.im_func: + from warnings import warn + warn(DeprecationWarning('This flask class uses a customized ' + 'init_jinja_globals() method which is deprecated. ' + 'Move the code from that method into the ' + 'create_jinja_environment() method instead.')) + self.__dict__['jinja_env'] = rv + self.init_jinja_globals() + + return rv + + @property + def got_first_request(self): + """This attribute is set to `True` if the application started + handling the first request. + + .. versionadded:: 0.8 + """ + return self._got_first_request + + def make_config(self, instance_relative=False): + """Used to create the config attribute by the Flask constructor. + The `instance_relative` parameter is passed in from the constructor + of Flask (there named `instance_relative_config`) and indicates if + the config should be relative to the instance path or the root path + of the application. + + .. versionadded:: 0.8 + """ + root_path = self.root_path + if instance_relative: + root_path = self.instance_path + return Config(root_path, self.default_config) + + def auto_find_instance_path(self): + """Tries to locate the instance path if it was not provided to the + constructor of the application class. It will basically calculate + the path to a folder named ``instance`` next to your main file or + the package. + + .. versionadded:: 0.8 + """ + prefix, package_path = find_package(self.import_name) + if prefix is None: + return os.path.join(package_path, 'instance') + return os.path.join(prefix, 'var', self.name + '-instance') + + def open_instance_resource(self, resource, mode='rb'): + """Opens a resource from the application's instance folder + (:attr:`instance_path`). Otherwise works like + :meth:`open_resource`. Instance resources can also be opened for + writing. + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. + """ + return open(os.path.join(self.instance_path, resource), mode) + + def create_jinja_environment(self): + """Creates the Jinja2 environment based on :attr:`jinja_options` + and :meth:`select_jinja_autoescape`. Since 0.7 this also adds + the Jinja2 globals and filters after initialization. Override + this function to customize the behavior. + + .. versionadded:: 0.5 + """ + options = dict(self.jinja_options) + if 'autoescape' not in options: + options['autoescape'] = self.select_jinja_autoescape + rv = Environment(self, **options) + rv.globals.update( + url_for=url_for, + get_flashed_messages=get_flashed_messages + ) + rv.filters['tojson'] = _tojson_filter + return rv + + def create_global_jinja_loader(self): + """Creates the loader for the Jinja2 environment. Can be used to + override just the loader and keeping the rest unchanged. It's + discouraged to override this function. Instead one should override + the :meth:`jinja_loader` function instead. + + The global loader dispatches between the loaders of the application + and the individual blueprints. + + .. versionadded:: 0.7 + """ + return DispatchingJinjaLoader(self) + + def init_jinja_globals(self): + """Deprecated. Used to initialize the Jinja2 globals. + + .. versionadded:: 0.5 + .. versionchanged:: 0.7 + This method is deprecated with 0.7. Override + :meth:`create_jinja_environment` instead. + """ + + def select_jinja_autoescape(self, filename): + """Returns `True` if autoescaping should be active for the given + template name. + + .. versionadded:: 0.5 + """ + if filename is None: + return False + return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + + def update_template_context(self, context): + """Update the template context with some commonly used variables. + This injects request, session, config and g into the template + context as well as everything template context processors want + to inject. Note that the as of Flask 0.6, the original values + in the context will not be overriden if a context processor + decides to return a value with the same key. + + :param context: the context as a dictionary that is updated in place + to add extra variables. + """ + funcs = self.template_context_processors[None] + bp = _request_ctx_stack.top.request.blueprint + if bp is not None and bp in self.template_context_processors: + funcs = chain(funcs, self.template_context_processors[bp]) + orig_ctx = context.copy() + for func in funcs: + context.update(func()) + # make sure the original values win. This makes it possible to + # easier add new variables in context processors without breaking + # existing views. + context.update(orig_ctx) + + def run(self, host='127.0.0.1', port=5000, debug=None, **options): + """Runs the application on a local development server. If the + :attr:`debug` flag is set the server will automatically reload + for code changes and show a debugger in case an exception happened. + + If you want to run the application in debug mode, but disable the + code execution on the interactive debugger, you can pass + ``use_evalex=False`` as parameter. This will keep the debugger's + traceback screen active, but disable code execution. + + .. admonition:: Keep in Mind + + Flask will suppress any server error with a generic error page + unless it is in debug mode. As such to enable just the + interactive debugger without the code reloading, you have to + invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. + Setting ``use_debugger`` to `True` without being in debug mode + won't catch any exceptions because there won't be any to + catch. + + :param host: the hostname to listen on. set this to ``'0.0.0.0'`` + to have the server available externally as well. + :param port: the port of the webserver + :param debug: if given, enable or disable debug mode. + See :attr:`debug`. + :param options: the options to be forwarded to the underlying + Werkzeug server. See + :func:`werkzeug.serving.run_simple` for more + information. + """ + from werkzeug.serving import run_simple + if debug is not None: + self.debug = bool(debug) + options.setdefault('use_reloader', self.debug) + options.setdefault('use_debugger', self.debug) + try: + run_simple(host, port, self, **options) + finally: + # reset the first request information if the development server + # resetted normally. This makes it possible to restart the server + # without reloader and that stuff from an interactive shell. + self._got_first_request = False + + def test_client(self, use_cookies=True): + """Creates a test client for this application. For information + about unit testing head over to :ref:`testing`. + + The test client can be used in a `with` block to defer the closing down + of the context until the end of the `with` block. This is useful if + you want to access the context locals for testing:: + + with app.test_client() as c: + rv = c.get('/?vodka=42') + assert request.args['vodka'] == '42' + + See :class:`~flask.testing.FlaskClient` for more information. + + .. versionchanged:: 0.4 + added support for `with` block usage for the client. + + .. versionadded:: 0.7 + The `use_cookies` parameter was added as well as the ability + to override the client to be used by setting the + :attr:`test_client_class` attribute. + """ + cls = self.test_client_class + if cls is None: + from flask.testing import FlaskClient as cls + return cls(self, self.response_class, use_cookies=use_cookies) + + def open_session(self, request): + """Creates or opens a new session. Default implementation stores all + session data in a signed cookie. This requires that the + :attr:`secret_key` is set. Instead of overriding this method + we recommend replacing the :class:`session_interface`. + + :param request: an instance of :attr:`request_class`. + """ + return self.session_interface.open_session(self, request) + + def save_session(self, session, response): + """Saves the session if it needs updates. For the default + implementation, check :meth:`open_session`. Instead of overriding this + method we recommend replacing the :class:`session_interface`. + + :param session: the session to be saved (a + :class:`~werkzeug.contrib.securecookie.SecureCookie` + object) + :param response: an instance of :attr:`response_class` + """ + return self.session_interface.save_session(self, session, response) + + def make_null_session(self): + """Creates a new instance of a missing session. Instead of overriding + this method we recommend replacing the :class:`session_interface`. + + .. versionadded:: 0.7 + """ + return self.session_interface.make_null_session(self) + + def register_module(self, module, **options): + """Registers a module with this application. The keyword argument + of this function are the same as the ones for the constructor of the + :class:`Module` class and will override the values of the module if + provided. + + .. versionchanged:: 0.7 + The module system was deprecated in favor for the blueprint + system. + """ + assert blueprint_is_module(module), 'register_module requires ' \ + 'actual module objects. Please upgrade to blueprints though.' + if not self.enable_modules: + raise RuntimeError('Module support was disabled but code ' + 'attempted to register a module named %r' % module) + else: + from warnings import warn + warn(DeprecationWarning('Modules are deprecated. Upgrade to ' + 'using blueprints. Have a look into the documentation for ' + 'more information. If this module was registered by a ' + 'Flask-Extension upgrade the extension or contact the author ' + 'of that extension instead. (Registered %r)' % module), + stacklevel=2) + + self.register_blueprint(module, **options) + + @setupmethod + def register_blueprint(self, blueprint, **options): + """Registers a blueprint on the application. + + .. versionadded:: 0.7 + """ + first_registration = False + if blueprint.name in self.blueprints: + assert self.blueprints[blueprint.name] is blueprint, \ + 'A blueprint\'s name collision ocurred between %r and ' \ + '%r. Both share the same name "%s". Blueprints that ' \ + 'are created on the fly need unique names.' % \ + (blueprint, self.blueprints[blueprint.name], blueprint.name) + else: + self.blueprints[blueprint.name] = blueprint + first_registration = True + blueprint.register(self, options, first_registration) + + @setupmethod + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + """Connects a URL rule. Works exactly like the :meth:`route` + decorator. If a view_func is provided it will be registered with the + endpoint. + + Basically this example:: + + @app.route('/') + def index(): + pass + + Is equivalent to the following:: + + def index(): + pass + app.add_url_rule('/', 'index', index) + + If the view_func is not provided you will need to connect the endpoint + to a view function like so:: + + app.view_functions['index'] = index + + Internally :meth:`route` invokes :meth:`add_url_rule` so if you want + to customize the behavior via subclassing you only need to change + this method. + + For more information refer to :ref:`url-route-registrations`. + + .. versionchanged:: 0.2 + `view_func` parameter added. + + .. versionchanged:: 0.6 + `OPTIONS` is added automatically as method. + + :param rule: the URL rule as string + :param endpoint: the endpoint for the registered URL rule. Flask + itself assumes the name of the view function as + endpoint + :param view_func: the function to call when serving a request to the + provided endpoint + :param options: the options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object. A change + to Werkzeug is handling of method options. methods + is a list of methods this rule should be limited + to (`GET`, `POST` etc.). By default a rule + just listens for `GET` (and implicitly `HEAD`). + Starting with Flask 0.6, `OPTIONS` is implicitly + added and handled by the standard request handling. + """ + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) + options['endpoint'] = endpoint + methods = options.pop('methods', None) + + # if the methods are not given and the view_func object knows its + # methods we can use that instead. If neither exists, we go with + # a tuple of only `GET` as default. + if methods is None: + methods = getattr(view_func, 'methods', None) or ('GET',) + + # starting with Flask 0.8 the view_func object can disable and + # force-enable the automatic options handling. + provide_automatic_options = getattr(view_func, + 'provide_automatic_options', None) + + if provide_automatic_options is None: + if 'OPTIONS' not in methods: + methods = tuple(methods) + ('OPTIONS',) + provide_automatic_options = True + else: + provide_automatic_options = False + + # due to a werkzeug bug we need to make sure that the defaults are + # None if they are an empty dictionary. This should not be necessary + # with Werkzeug 0.7 + options['defaults'] = options.get('defaults') or None + + rule = self.url_rule_class(rule, methods=methods, **options) + rule.provide_automatic_options = provide_automatic_options + self.url_map.add(rule) + if view_func is not None: + self.view_functions[endpoint] = view_func + + def route(self, rule, **options): + """A decorator that is used to register a view function for a + given URL rule. This does the same thing as :meth:`add_url_rule` + but is intended for decorator usage:: + + @app.route('/') + def index(): + return 'Hello World' + + For more information refer to :ref:`url-route-registrations`. + + :param rule: the URL rule as string + :param endpoint: the endpoint for the registered URL rule. Flask + itself assumes the name of the view function as + endpoint + :param view_func: the function to call when serving a request to the + provided endpoint + :param options: the options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object. A change + to Werkzeug is handling of method options. methods + is a list of methods this rule should be limited + to (`GET`, `POST` etc.). By default a rule + just listens for `GET` (and implicitly `HEAD`). + Starting with Flask 0.6, `OPTIONS` is implicitly + added and handled by the standard request handling. + """ + def decorator(f): + endpoint = options.pop('endpoint', None) + self.add_url_rule(rule, endpoint, f, **options) + return f + return decorator + + @setupmethod + def endpoint(self, endpoint): + """A decorator to register a function as an endpoint. + Example:: + + @app.endpoint('example.endpoint') + def example(): + return "example" + + :param endpoint: the name of the endpoint + """ + def decorator(f): + self.view_functions[endpoint] = f + return f + return decorator + + @setupmethod + def errorhandler(self, code_or_exception): + """A decorator that is used to register a function give a given + error code. Example:: + + @app.errorhandler(404) + def page_not_found(error): + return 'This page does not exist', 404 + + You can also register handlers for arbitrary exceptions:: + + @app.errorhandler(DatabaseError) + def special_exception_handler(error): + return 'Database connection failed', 500 + + You can also register a function as error handler without using + the :meth:`errorhandler` decorator. The following example is + equivalent to the one above:: + + def page_not_found(error): + return 'This page does not exist', 404 + app.error_handler_spec[None][404] = page_not_found + + Setting error handlers via assignments to :attr:`error_handler_spec` + however is discouraged as it requires fidling with nested dictionaries + and the special case for arbitrary exception types. + + The first `None` refers to the active blueprint. If the error + handler should be application wide `None` shall be used. + + .. versionadded:: 0.7 + One can now additionally also register custom exception types + that do not necessarily have to be a subclass of the + :class:`~werkzeug.exceptions.HTTPException` class. + + :param code: the code as integer for the handler + """ + def decorator(f): + self._register_error_handler(None, code_or_exception, f) + return f + return decorator + + def register_error_handler(self, code_or_exception, f): + """Alternative error attach function to the :meth:`errorhandler` + decorator that is more straightforward to use for non decorator + usage. + + .. versionadded:: 0.7 + """ + self._register_error_handler(None, code_or_exception, f) + + @setupmethod + def _register_error_handler(self, key, code_or_exception, f): + if isinstance(code_or_exception, HTTPException): + code_or_exception = code_or_exception.code + if isinstance(code_or_exception, (int, long)): + assert code_or_exception != 500 or key is None, \ + 'It is currently not possible to register a 500 internal ' \ + 'server error on a per-blueprint level.' + self.error_handler_spec.setdefault(key, {})[code_or_exception] = f + else: + self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \ + .append((code_or_exception, f)) + + @setupmethod + def template_filter(self, name=None): + """A decorator that is used to register custom template filter. + You can specify a name for the filter, otherwise the function + name will be used. Example:: + + @app.template_filter() + def reverse(s): + return s[::-1] + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + def decorator(f): + self.jinja_env.filters[name or f.__name__] = f + return f + return decorator + + @setupmethod + def before_request(self, f): + """Registers a function to run before each request.""" + self.before_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def before_first_request(self, f): + """Registers a function to be run before the first request to this + instance of the application. + + .. versionadded:: 0.8 + """ + self.before_first_request_funcs.append(f) + + @setupmethod + def after_request(self, f): + """Register a function to be run after each request. Your function + must take one parameter, a :attr:`response_class` object and return + a new response object or the same (see :meth:`process_response`). + + As of Flask 0.7 this function might not be executed at the end of the + request in case an unhandled exception ocurred. + """ + self.after_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def teardown_request(self, f): + """Register a function to be run at the end of each request, + regardless of whether there was an exception or not. These functions + are executed when the request context is popped, even if not an + actual request was performed. + + Example:: + + ctx = app.test_request_context() + ctx.push() + ... + ctx.pop() + + When ``ctx.pop()`` is executed in the above example, the teardown + functions are called just before the request context moves from the + stack of active contexts. This becomes relevant if you are using + such constructs in tests. + + Generally teardown functions must take every necesary step to avoid + that they will fail. If they do execute code that might fail they + will have to surround the execution of these code by try/except + statements and log ocurring errors. + """ + self.teardown_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def context_processor(self, f): + """Registers a template context processor function.""" + self.template_context_processors[None].append(f) + return f + + @setupmethod + def url_value_preprocessor(self, f): + """Registers a function as URL value preprocessor for all view + functions of the application. It's called before the view functions + are called and can modify the url values provided. + """ + self.url_value_preprocessors.setdefault(None, []).append(f) + return f + + @setupmethod + def url_defaults(self, f): + """Callback function for URL defaults for all view functions of the + application. It's called with the endpoint and values and should + update the values passed in place. + """ + self.url_default_functions.setdefault(None, []).append(f) + return f + + def handle_http_exception(self, e): + """Handles an HTTP exception. By default this will invoke the + registered error handlers and fall back to returning the + exception as response. + + .. versionadded: 0.3 + """ + handlers = self.error_handler_spec.get(request.blueprint) + if handlers and e.code in handlers: + handler = handlers[e.code] + else: + handler = self.error_handler_spec[None].get(e.code) + if handler is None: + return e + return handler(e) + + def trap_http_exception(self, e): + """Checks if an HTTP exception should be trapped or not. By default + this will return `False` for all exceptions except for a bad request + key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to `True`. It + also returns `True` if ``TRAP_HTTP_EXCEPTIONS`` is set to `True`. + + This is called for all HTTP exceptions raised by a view function. + If it returns `True` for any exception the error handler for this + exception is not called and it shows up as regular exception in the + traceback. This is helpful for debugging implicitly raised HTTP + exceptions. + + .. versionadded:: 0.8 + """ + if self.config['TRAP_HTTP_EXCEPTIONS']: + return True + if self.config['TRAP_BAD_REQUEST_ERRORS']: + return isinstance(e, BadRequest) + return False + + def handle_user_exception(self, e): + """This method is called whenever an exception occurs that should be + handled. A special case are + :class:`~werkzeug.exception.HTTPException`\s which are forwarded by + this function to the :meth:`handle_http_exception` method. This + function will either return a response value or reraise the + exception with the same traceback. + + .. versionadded:: 0.7 + """ + exc_type, exc_value, tb = sys.exc_info() + assert exc_value is e + + # ensure not to trash sys.exc_info() at that point in case someone + # wants the traceback preserved in handle_http_exception. Of course + # we cannot prevent users from trashing it themselves in a custom + # trap_http_exception method so that's their fault then. + if isinstance(e, HTTPException) and not self.trap_http_exception(e): + return self.handle_http_exception(e) + + blueprint_handlers = () + handlers = self.error_handler_spec.get(request.blueprint) + if handlers is not None: + blueprint_handlers = handlers.get(None, ()) + app_handlers = self.error_handler_spec[None].get(None, ()) + for typecheck, handler in chain(blueprint_handlers, app_handlers): + if isinstance(e, typecheck): + return handler(e) + + raise exc_type, exc_value, tb + + def handle_exception(self, e): + """Default exception handling that kicks in when an exception + occours that is not caught. In debug mode the exception will + be re-raised immediately, otherwise it is logged and the handler + for a 500 internal server error is used. If no such handler + exists, a default 500 internal server error message is displayed. + + .. versionadded: 0.3 + """ + exc_type, exc_value, tb = sys.exc_info() + + got_request_exception.send(self, exception=e) + handler = self.error_handler_spec[None].get(500) + + if self.propagate_exceptions: + # if we want to repropagate the exception, we can attempt to + # raise it with the whole traceback in case we can do that + # (the function was actually called from the except part) + # otherwise, we just raise the error again + if exc_value is e: + raise exc_type, exc_value, tb + else: + raise e + + self.log_exception((exc_type, exc_value, tb)) + if handler is None: + return InternalServerError() + return handler(e) + + def log_exception(self, exc_info): + """Logs an exception. This is called by :meth:`handle_exception` + if debugging is disabled and right before the handler is called. + The default implementation logs the exception as error on the + :attr:`logger`. + + .. versionadded:: 0.8 + """ + self.logger.error('Exception on %s [%s]' % ( + request.path, + request.method + ), exc_info=exc_info) + + def raise_routing_exception(self, request): + """Exceptions that are recording during routing are reraised with + this method. During debug we are not reraising redirect requests + for non ``GET``, ``HEAD``, or ``OPTIONS`` requests and we're raising + a different error instead to help debug situations. + + :internal: + """ + if not self.debug \ + or not isinstance(request.routing_exception, RequestRedirect) \ + or request.method in ('GET', 'HEAD', 'OPTIONS'): + raise request.routing_exception + + from .debughelpers import FormDataRoutingRedirect + raise FormDataRoutingRedirect(request) + + def dispatch_request(self): + """Does the request dispatching. Matches the URL and returns the + return value of the view or error handler. This does not have to + be a response object. In order to convert the return value to a + proper response object, call :func:`make_response`. + + .. versionchanged:: 0.7 + This no longer does the exception handling, this code was + moved to the new :meth:`full_dispatch_request`. + """ + req = _request_ctx_stack.top.request + if req.routing_exception is not None: + self.raise_routing_exception(req) + rule = req.url_rule + # if we provide automatic options for this URL and the + # request came with the OPTIONS method, reply automatically + if getattr(rule, 'provide_automatic_options', False) \ + and req.method == 'OPTIONS': + return self.make_default_options_response() + # otherwise dispatch to the handler for that endpoint + return self.view_functions[rule.endpoint](**req.view_args) + + def full_dispatch_request(self): + """Dispatches the request and on top of that performs request + pre and postprocessing as well as HTTP exception catching and + error handling. + + .. versionadded:: 0.7 + """ + self.try_trigger_before_first_request_functions() + try: + request_started.send(self) + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + except Exception, e: + rv = self.handle_user_exception(e) + response = self.make_response(rv) + response = self.process_response(response) + request_finished.send(self, response=response) + return response + + def try_trigger_before_first_request_functions(self): + """Called before each request and will ensure that it triggers + the :attr:`before_first_request_funcs` and only exactly once per + application instance (which means process usually). + + :internal: + """ + if self._got_first_request: + return + with self._before_request_lock: + if self._got_first_request: + return + self._got_first_request = True + for func in self.before_first_request_funcs: + func() + + def make_default_options_response(self): + """This method is called to create the default `OPTIONS` response. + This can be changed through subclassing to change the default + behaviour of `OPTIONS` responses. + + .. versionadded:: 0.7 + """ + adapter = _request_ctx_stack.top.url_adapter + if hasattr(adapter, 'allowed_methods'): + methods = adapter.allowed_methods() + else: + # fallback for Werkzeug < 0.7 + methods = [] + try: + adapter.match(method='--') + except MethodNotAllowed, e: + methods = e.valid_methods + except HTTPException, e: + pass + rv = self.response_class() + rv.allow.update(methods) + return rv + + def make_response(self, rv): + """Converts the return value from a view function to a real + response object that is an instance of :attr:`response_class`. + + The following types are allowed for `rv`: + + .. tabularcolumns:: |p{3.5cm}|p{9.5cm}| + + ======================= =========================================== + :attr:`response_class` the object is returned unchanged + :class:`str` a response object is created with the + string as body + :class:`unicode` a response object is created with the + string encoded to utf-8 as body + :class:`tuple` the response object is created with the + contents of the tuple as arguments + a WSGI function the function is called as WSGI application + and buffered as response object + ======================= =========================================== + + :param rv: the return value from the view function + """ + if rv is None: + raise ValueError('View function did not return a response') + if isinstance(rv, self.response_class): + return rv + if isinstance(rv, basestring): + return self.response_class(rv) + if isinstance(rv, tuple): + return self.response_class(*rv) + return self.response_class.force_type(rv, request.environ) + + def create_url_adapter(self, request): + """Creates a URL adapter for the given request. The URL adapter + is created at a point where the request context is not yet set up + so the request is passed explicitly. + + .. versionadded:: 0.6 + """ + return self.url_map.bind_to_environ(request.environ, + server_name=self.config['SERVER_NAME']) + + def inject_url_defaults(self, endpoint, values): + """Injects the URL defaults for the given endpoint directly into + the values dictionary passed. This is used internally and + automatically called on URL building. + + .. versionadded:: 0.7 + """ + funcs = self.url_default_functions.get(None, ()) + if '.' in endpoint: + bp = endpoint.split('.', 1)[0] + funcs = chain(funcs, self.url_default_functions.get(bp, ())) + for func in funcs: + func(endpoint, values) + + def preprocess_request(self): + """Called before the actual request dispatching and will + call every as :meth:`before_request` decorated function. + If any of these function returns a value it's handled as + if it was the return value from the view and further + request handling is stopped. + + This also triggers the :meth:`url_value_processor` functions before + the actualy :meth:`before_request` functions are called. + """ + bp = _request_ctx_stack.top.request.blueprint + + funcs = self.url_value_preprocessors.get(None, ()) + if bp is not None and bp in self.url_value_preprocessors: + funcs = chain(funcs, self.url_value_preprocessors[bp]) + for func in funcs: + func(request.endpoint, request.view_args) + + funcs = self.before_request_funcs.get(None, ()) + if bp is not None and bp in self.before_request_funcs: + funcs = chain(funcs, self.before_request_funcs[bp]) + for func in funcs: + rv = func() + if rv is not None: + return rv + + def process_response(self, response): + """Can be overridden in order to modify the response object + before it's sent to the WSGI server. By default this will + call all the :meth:`after_request` decorated functions. + + .. versionchanged:: 0.5 + As of Flask 0.5 the functions registered for after request + execution are called in reverse order of registration. + + :param response: a :attr:`response_class` object. + :return: a new response object or the same, has to be an + instance of :attr:`response_class`. + """ + ctx = _request_ctx_stack.top + bp = ctx.request.blueprint + if not self.session_interface.is_null_session(ctx.session): + self.save_session(ctx.session, response) + funcs = () + if bp is not None and bp in self.after_request_funcs: + funcs = reversed(self.after_request_funcs[bp]) + if None in self.after_request_funcs: + funcs = chain(funcs, reversed(self.after_request_funcs[None])) + for handler in funcs: + response = handler(response) + return response + + def do_teardown_request(self): + """Called after the actual request dispatching and will + call every as :meth:`teardown_request` decorated function. This is + not actually called by the :class:`Flask` object itself but is always + triggered when the request context is popped. That way we have a + tighter control over certain resources under testing environments. + """ + funcs = reversed(self.teardown_request_funcs.get(None, ())) + bp = _request_ctx_stack.top.request.blueprint + if bp is not None and bp in self.teardown_request_funcs: + funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) + exc = sys.exc_info()[1] + for func in funcs: + rv = func(exc) + if rv is not None: + return rv + request_tearing_down.send(self) + + def request_context(self, environ): + """Creates a :class:`~flask.ctx.RequestContext` from the given + environment and binds it to the current context. This must be used in + combination with the `with` statement because the request is only bound + to the current context for the duration of the `with` block. + + Example usage:: + + with app.request_context(environ): + do_something_with(request) + + The object returned can also be used without the `with` statement + which is useful for working in the shell. The example above is + doing exactly the same as this code:: + + ctx = app.request_context(environ) + ctx.push() + try: + do_something_with(request) + finally: + ctx.pop() + + .. versionchanged:: 0.3 + Added support for non-with statement usage and `with` statement + is now passed the ctx object. + + :param environ: a WSGI environment + """ + return RequestContext(self, environ) + + def test_request_context(self, *args, **kwargs): + """Creates a WSGI environment from the given values (see + :func:`werkzeug.test.EnvironBuilder` for more information, this + function accepts the same arguments). + """ + from flask.testing import make_test_environ_builder + builder = make_test_environ_builder(self, *args, **kwargs) + try: + return self.request_context(builder.get_environ()) + finally: + builder.close() + + def wsgi_app(self, environ, start_response): + """The actual WSGI application. This is not implemented in + `__call__` so that middlewares can be applied without losing a + reference to the class. So instead of doing this:: + + app = MyMiddleware(app) + + It's a better idea to do this instead:: + + app.wsgi_app = MyMiddleware(app.wsgi_app) + + Then you still have the original application object around and + can continue to call methods on it. + + .. versionchanged:: 0.7 + The behavior of the before and after request callbacks was changed + under error conditions and a new callback was added that will + always execute at the end of the request, independent on if an + error ocurred or not. See :ref:`callbacks-and-errors`. + + :param environ: a WSGI environment + :param start_response: a callable accepting a status code, + a list of headers and an optional + exception context to start the response + """ + with self.request_context(environ): + try: + response = self.full_dispatch_request() + except Exception, e: + response = self.make_response(self.handle_exception(e)) + return response(environ, start_response) + + @property + def modules(self): + from warnings import warn + warn(DeprecationWarning('Flask.modules is deprecated, use ' + 'Flask.blueprints instead'), stacklevel=2) + return self.blueprints + + def __call__(self, environ, start_response): + """Shortcut for :attr:`wsgi_app`.""" + return self.wsgi_app(environ, start_response) diff --git a/flask/blueprints.py b/flask/blueprints.py new file mode 100644 index 00000000..ccdda38d --- /dev/null +++ b/flask/blueprints.py @@ -0,0 +1,321 @@ +# -*- coding: utf-8 -*- +""" + flask.blueprints + ~~~~~~~~~~~~~~~~ + + Blueprints are the recommended way to implement larger or more + pluggable applications in Flask 0.7 and later. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from functools import update_wrapper + +from .helpers import _PackageBoundObject, _endpoint_from_view_func + + +class BlueprintSetupState(object): + """Temporary holder object for registering a blueprint with the + application. An instance of this class is created by the + :meth:`~flask.Blueprint.make_setup_state` method and later passed + to all register callback functions. + """ + + def __init__(self, blueprint, app, options, first_registration): + #: a reference to the current application + self.app = app + + #: a reference to the blurprint that created this setup state. + self.blueprint = blueprint + + #: a dictionary with all options that were passed to the + #: :meth:`~flask.Flask.register_blueprint` method. + self.options = options + + #: as blueprints can be registered multiple times with the + #: application and not everything wants to be registered + #: multiple times on it, this attribute can be used to figure + #: out if the blueprint was registered in the past already. + self.first_registration = first_registration + + subdomain = self.options.get('subdomain') + if subdomain is None: + subdomain = self.blueprint.subdomain + + #: The subdomain that the blueprint should be active for, `None` + #: otherwise. + self.subdomain = subdomain + + url_prefix = self.options.get('url_prefix') + if url_prefix is None: + url_prefix = self.blueprint.url_prefix + + #: The prefix that should be used for all URLs defined on the + #: blueprint. + self.url_prefix = url_prefix + + #: A dictionary with URL defaults that is added to each and every + #: URL that was defined with the blueprint. + self.url_defaults = dict(self.blueprint.url_values_defaults) + self.url_defaults.update(self.options.get('url_defaults', ())) + + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + """A helper method to register a rule (and optionally a view function) + to the application. The endpoint is automatically prefixed with the + blueprint's name. + """ + if self.url_prefix: + rule = self.url_prefix + rule + options.setdefault('subdomain', self.subdomain) + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) + defaults = self.url_defaults + if 'defaults' in options: + defaults = dict(defaults, **options.pop('defaults')) + self.app.add_url_rule(rule, '%s.%s' % (self.blueprint.name, endpoint), + view_func, defaults=defaults, **options) + + +class Blueprint(_PackageBoundObject): + """Represents a blueprint. A blueprint is an object that records + functions that will be called with the + :class:`~flask.blueprint.BlueprintSetupState` later to register functions + or other things on the main application. See :ref:`blueprints` for more + information. + + .. versionadded:: 0.7 + """ + + warn_on_modifications = False + _got_registered_once = False + + def __init__(self, name, import_name, static_folder=None, + static_url_path=None, template_folder=None, + url_prefix=None, subdomain=None, url_defaults=None): + _PackageBoundObject.__init__(self, import_name, template_folder) + self.name = name + self.url_prefix = url_prefix + self.subdomain = subdomain + self.static_folder = static_folder + self.static_url_path = static_url_path + self.deferred_functions = [] + self.view_functions = {} + if url_defaults is None: + url_defaults = {} + self.url_values_defaults = url_defaults + + def record(self, func): + """Registers a function that is called when the blueprint is + registered on the application. This function is called with the + state as argument as returned by the :meth:`make_setup_state` + method. + """ + if self._got_registered_once and self.warn_on_modifications: + from warnings import warn + warn(Warning('The blueprint was already registered once ' + 'but is getting modified now. These changes ' + 'will not show up.')) + self.deferred_functions.append(func) + + def record_once(self, func): + """Works like :meth:`record` but wraps the function in another + function that will ensure the function is only called once. If the + blueprint is registered a second time on the application, the + function passed is not called. + """ + def wrapper(state): + if state.first_registration: + func(state) + return self.record(update_wrapper(wrapper, func)) + + def make_setup_state(self, app, options, first_registration=False): + """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` + object that is later passed to the register callback functions. + Subclasses can override this to return a subclass of the setup state. + """ + return BlueprintSetupState(self, app, options, first_registration) + + def register(self, app, options, first_registration=False): + """Called by :meth:`Flask.register_blueprint` to register a blueprint + on the application. This can be overridden to customize the register + behavior. Keyword arguments from + :func:`~flask.Flask.register_blueprint` are directly forwarded to this + method in the `options` dictionary. + """ + self._got_registered_once = True + state = self.make_setup_state(app, options, first_registration) + if self.has_static_folder: + state.add_url_rule(self.static_url_path + '/', + view_func=self.send_static_file, + endpoint='static') + + for deferred in self.deferred_functions: + deferred(state) + + def route(self, rule, **options): + """Like :meth:`Flask.route` but for a blueprint. The endpoint for the + :func:`url_for` function is prefixed with the name of the blueprint. + """ + def decorator(f): + endpoint = options.pop("endpoint", f.__name__) + self.add_url_rule(rule, endpoint, f, **options) + return f + return decorator + + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for + the :func:`url_for` function is prefixed with the name of the blueprint. + """ + if endpoint: + assert '.' not in endpoint, "Blueprint endpoint's should not contain dot's" + self.record(lambda s: + s.add_url_rule(rule, endpoint, view_func, **options)) + + def endpoint(self, endpoint): + """Like :meth:`Flask.endpoint` but for a blueprint. This does not + prefix the endpoint with the blueprint name, this has to be done + explicitly by the user of this method. If the endpoint is prefixed + with a `.` it will be registered to the current blueprint, otherwise + it's an application independent endpoint. + """ + def decorator(f): + def register_endpoint(state): + state.app.view_functions[endpoint] = f + self.record_once(register_endpoint) + return f + return decorator + + def before_request(self, f): + """Like :meth:`Flask.before_request` but for a blueprint. This function + is only executed before each request that is handled by a function of + that blueprint. + """ + self.record_once(lambda s: s.app.before_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def before_app_request(self, f): + """Like :meth:`Flask.before_request`. Such a function is executed + before each request, even if outside of a blueprint. + """ + self.record_once(lambda s: s.app.before_request_funcs + .setdefault(None, []).append(f)) + return f + + def before_app_first_request(self, f): + """Like :meth:`Flask.before_first_request`. Such a function is + executed before the first request to the application. + """ + self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) + return f + + def after_request(self, f): + """Like :meth:`Flask.after_request` but for a blueprint. This function + is only executed after each request that is handled by a function of + that blueprint. + """ + self.record_once(lambda s: s.app.after_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def after_app_request(self, f): + """Like :meth:`Flask.after_request` but for a blueprint. Such a function + is executed after each request, even if outside of the blueprint. + """ + self.record_once(lambda s: s.app.after_request_funcs + .setdefault(None, []).append(f)) + return f + + def teardown_request(self, f): + """Like :meth:`Flask.teardown_request` but for a blueprint. This + function is only executed when tearing down requests handled by a + function of that blueprint. Teardown request functions are executed + when the request context is popped, even when no actual request was + performed. + """ + self.record_once(lambda s: s.app.teardown_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def teardown_app_request(self, f): + """Like :meth:`Flask.teardown_request` but for a blueprint. Such a + function is executed when tearing down each request, even if outside of + the blueprint. + """ + self.record_once(lambda s: s.app.teardown_request_funcs + .setdefault(None, []).append(f)) + return f + + def context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a blueprint. This + function is only executed for requests handled by a blueprint. + """ + self.record_once(lambda s: s.app.template_context_processors + .setdefault(self.name, []).append(f)) + return f + + def app_context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a blueprint. Such a + function is executed each request, even if outside of the blueprint. + """ + self.record_once(lambda s: s.app.template_context_processors + .setdefault(None, []).append(f)) + return f + + def app_errorhandler(self, code): + """Like :meth:`Flask.errorhandler` but for a blueprint. This + handler is used for all requests, even if outside of the blueprint. + """ + def decorator(f): + self.record_once(lambda s: s.app.errorhandler(code)(f)) + return f + return decorator + + def url_value_preprocessor(self, f): + """Registers a function as URL value preprocessor for this + blueprint. It's called before the view functions are called and + can modify the url values provided. + """ + self.record_once(lambda s: s.app.url_value_preprocessors + .setdefault(self.name, []).append(f)) + return f + + def url_defaults(self, f): + """Callback function for URL defaults for this blueprint. It's called + with the endpoint and values and should update the values passed + in place. + """ + self.record_once(lambda s: s.app.url_default_functions + .setdefault(self.name, []).append(f)) + return f + + def app_url_value_preprocessor(self, f): + """Same as :meth:`url_value_preprocessor` but application wide. + """ + self.record_once(lambda s: s.app.url_value_preprocessors + .setdefault(None, []).append(f)) + return f + + def app_url_defaults(self, f): + """Same as :meth:`url_defaults` but application wide. + """ + self.record_once(lambda s: s.app.url_default_functions + .setdefault(None, []).append(f)) + return f + + def errorhandler(self, code_or_exception): + """Registers an error handler that becomes active for this blueprint + only. Please be aware that routing does not happen local to a + blueprint so an error handler for 404 usually is not handled by + a blueprint unless it is caused inside a view function. Another + special case is the 500 internal server error which is always looked + up from the application. + + Otherwise works as the :meth:`~flask.Flask.errorhandler` decorator + of the :class:`~flask.Flask` object. + """ + def decorator(f): + self.record_once(lambda s: s.app._register_error_handler( + self.name, code_or_exception, f)) + return f + return decorator diff --git a/flask/config.py b/flask/config.py new file mode 100644 index 00000000..67dbf9b7 --- /dev/null +++ b/flask/config.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +""" + flask.config + ~~~~~~~~~~~~ + + Implements the configuration related objects. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import imp +import os +import errno + +from werkzeug.utils import import_string + + +class ConfigAttribute(object): + """Makes an attribute forward to the config""" + + def __init__(self, name, get_converter=None): + self.__name__ = name + self.get_converter = get_converter + + def __get__(self, obj, type=None): + if obj is None: + return self + rv = obj.config[self.__name__] + if self.get_converter is not None: + rv = self.get_converter(rv) + return rv + + def __set__(self, obj, value): + obj.config[self.__name__] = value + + +class Config(dict): + """Works exactly like a dict but provides ways to fill it from files + or special dictionaries. There are two common patterns to populate the + config. + + Either you can fill the config from a config file:: + + app.config.from_pyfile('yourconfig.cfg') + + Or alternatively you can define the configuration options in the + module that calls :meth:`from_object` or provide an import path to + a module that should be loaded. It is also possible to tell it to + use the same module and with that provide the configuration values + just before the call:: + + DEBUG = True + SECRET_KEY = 'development key' + app.config.from_object(__name__) + + In both cases (loading from any Python file or loading from modules), + only uppercase keys are added to the config. This makes it possible to use + lowercase values in the config file for temporary values that are not added + to the config or to define the config keys in the same file that implements + the application. + + Probably the most interesting way to load configurations is from an + environment variable pointing to a file:: + + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + + In this case before launching the application you have to set this + environment variable to the file you want to use. On Linux and OS X + use the export statement:: + + export YOURAPPLICATION_SETTINGS='/path/to/config/file' + + On windows use `set` instead. + + :param root_path: path to which files are read relative from. When the + config object is created by the application, this is + the application's :attr:`~flask.Flask.root_path`. + :param defaults: an optional dictionary of default values + """ + + def __init__(self, root_path, defaults=None): + dict.__init__(self, defaults or {}) + self.root_path = root_path + + def from_envvar(self, variable_name, silent=False): + """Loads a configuration from an environment variable pointing to + a configuration file. This is basically just a shortcut with nicer + error messages for this line of code:: + + app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) + + :param variable_name: name of the environment variable + :param silent: set to `True` if you want silent failure for missing + files. + :return: bool. `True` if able to load config, `False` otherwise. + """ + rv = os.environ.get(variable_name) + if not rv: + if silent: + return False + raise RuntimeError('The environment variable %r is not set ' + 'and as such configuration could not be ' + 'loaded. Set this variable and make it ' + 'point to a configuration file' % + variable_name) + self.from_pyfile(rv) + return True + + def from_pyfile(self, filename, silent=False): + """Updates the values in the config from a Python file. This function + behaves as if the file was imported as module with the + :meth:`from_object` function. + + :param filename: the filename of the config. This can either be an + absolute filename or a filename relative to the + root path. + :param silent: set to `True` if you want silent failure for missing + files. + + .. versionadded:: 0.7 + `silent` parameter. + """ + filename = os.path.join(self.root_path, filename) + d = imp.new_module('config') + d.__file__ = filename + try: + execfile(filename, d.__dict__) + except IOError, e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False + e.strerror = 'Unable to load configuration file (%s)' % e.strerror + raise + self.from_object(d) + return True + + def from_object(self, obj): + """Updates the values from the given object. An object can be of one + of the following two types: + + - a string: in this case the object with that name will be imported + - an actual object reference: that object is used directly + + Objects are usually either modules or classes. + + Just the uppercase variables in that object are stored in the config. + Example usage:: + + app.config.from_object('yourapplication.default_config') + from yourapplication import default_config + app.config.from_object(default_config) + + You should not use this function to load the actual configuration but + rather configuration defaults. The actual config should be loaded + with :meth:`from_pyfile` and ideally from a location not within the + package because the package might be installed system wide. + + :param obj: an import name or object + """ + if isinstance(obj, basestring): + obj = import_string(obj) + for key in dir(obj): + if key.isupper(): + self[key] = getattr(obj, key) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) diff --git a/flask/ctx.py b/flask/ctx.py new file mode 100644 index 00000000..26781dbd --- /dev/null +++ b/flask/ctx.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +""" + flask.ctx + ~~~~~~~~~ + + Implements the objects required to keep the context. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from werkzeug.exceptions import HTTPException + +from .globals import _request_ctx_stack +from .module import blueprint_is_module + + +class _RequestGlobals(object): + pass + + +def has_request_context(): + """If you have code that wants to test if a request context is there or + not this function can be used. For instance if you want to take advantage + of request information is it's available but fail silently if the request + object is unavailable. + + :: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and has_request_context(): + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + Alternatively you can also just test any of the context bound objects + (such as :class:`request` or :class:`g` for truthness):: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and request: + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + .. versionadded:: 0.7 + """ + return _request_ctx_stack.top is not None + + +class RequestContext(object): + """The request context contains all request relevant information. It is + created at the beginning of the request and pushed to the + `_request_ctx_stack` and removed at the end of it. It will create the + URL adapter and request object for the WSGI environment provided. + + Do not attempt to use this class directly, instead use + :meth:`~flask.Flask.test_request_context` and + :meth:`~flask.Flask.request_context` to create this object. + + When the request context is popped, it will evaluate all the + functions registered on the application for teardown execution + (:meth:`~flask.Flask.teardown_request`). + + The request context is automatically popped at the end of the request + for you. In debug mode the request context is kept around if + exceptions happen so that interactive debuggers have a chance to + introspect the data. With 0.4 this can also be forced for requests + that did not fail and outside of `DEBUG` mode. By setting + ``'flask._preserve_context'`` to `True` on the WSGI environment the + context will not pop itself at the end of the request. This is used by + the :meth:`~flask.Flask.test_client` for example to implement the + deferred cleanup functionality. + + You might find this helpful for unittests where you need the + information from the context local around for a little longer. Make + sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in + that situation, otherwise your unittests will leak memory. + """ + + def __init__(self, app, environ): + self.app = app + self.request = app.request_class(environ) + self.url_adapter = app.create_url_adapter(self.request) + self.g = _RequestGlobals() + self.flashes = None + self.session = None + + # indicator if the context was preserved. Next time another context + # is pushed the preserved context is popped. + self.preserved = False + + self.match_request() + + # XXX: Support for deprecated functionality. This is doing away with + # Flask 1.0 + blueprint = self.request.blueprint + if blueprint is not None: + # better safe than sorry, we don't want to break code that + # already worked + bp = app.blueprints.get(blueprint) + if bp is not None and blueprint_is_module(bp): + self.request._is_old_module = True + + def match_request(self): + """Can be overridden by a subclass to hook into the matching + of the request. + """ + try: + url_rule, self.request.view_args = \ + self.url_adapter.match(return_rule=True) + self.request.url_rule = url_rule + except HTTPException, e: + self.request.routing_exception = e + + def push(self): + """Binds the request context to the current context.""" + # If an exception ocurrs in debug mode or if context preservation is + # activated under exception situations exactly one context stays + # on the stack. The rationale is that you want to access that + # information under debug situations. However if someone forgets to + # pop that context again we want to make sure that on the next push + # it's invalidated otherwise we run at risk that something leaks + # memory. This is usually only a problem in testsuite since this + # functionality is not active in production environments. + top = _request_ctx_stack.top + if top is not None and top.preserved: + top.pop() + + _request_ctx_stack.push(self) + + # Open the session at the moment that the request context is + # available. This allows a custom open_session method to use the + # request context (e.g. flask-sqlalchemy). + self.session = self.app.open_session(self.request) + if self.session is None: + self.session = self.app.make_null_session() + + def pop(self): + """Pops the request context and unbinds it by doing that. This will + also trigger the execution of functions registered by the + :meth:`~flask.Flask.teardown_request` decorator. + """ + self.preserved = False + self.app.do_teardown_request() + rv = _request_ctx_stack.pop() + assert rv is self, 'Popped wrong request context. (%r instead of %r)' \ + % (rv, self) + + def __enter__(self): + self.push() + return self + + def __exit__(self, exc_type, exc_value, tb): + # do not pop the request stack if we are in debug mode and an + # exception happened. This will allow the debugger to still + # access the request object in the interactive shell. Furthermore + # the context can be force kept alive for the test client. + # See flask.testing for how this works. + if self.request.environ.get('flask._preserve_context') or \ + (tb is not None and self.app.preserve_context_on_exception): + self.preserved = True + else: + self.pop() + + def __repr__(self): + return '<%s \'%s\' [%s] of %s>' % ( + self.__class__.__name__, + self.request.url, + self.request.method, + self.app.name + ) diff --git a/flask/debughelpers.py b/flask/debughelpers.py new file mode 100644 index 00000000..edf8c111 --- /dev/null +++ b/flask/debughelpers.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +""" + flask.debughelpers + ~~~~~~~~~~~~~~~~~~ + + Various helpers to make the development experience better. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + + +class DebugFilesKeyError(KeyError, AssertionError): + """Raised from request.files during debugging. The idea is that it can + provide a better error message than just a generic KeyError/BadRequest. + """ + + def __init__(self, request, key): + form_matches = request.form.getlist(key) + buf = ['You tried to access the file "%s" in the request.files ' + 'dictionary but it does not exist. The mimetype for the request ' + 'is "%s" instead of "multipart/form-data" which means that no ' + 'file contents were transmitted. To fix this error you should ' + 'provide enctype="multipart/form-data" in your form.' % + (key, request.mimetype)] + if form_matches: + buf.append('\n\nThe browser instead transmitted some file names. ' + 'This was submitted: %s' % ', '.join('"%s"' % x + for x in form_matches)) + self.msg = ''.join(buf).encode('utf-8') + + def __str__(self): + return self.msg + + +class FormDataRoutingRedirect(AssertionError): + """This exception is raised by Flask in debug mode if it detects a + redirect caused by the routing system when the request method is not + GET, HEAD or OPTIONS. Reasoning: form data will be dropped. + """ + + def __init__(self, request): + exc = request.routing_exception + buf = ['A request was sent to this URL (%s) but a redirect was ' + 'issued automatically by the routing system to "%s".' + % (request.url, exc.new_url)] + + # In case just a slash was appended we can be extra helpful + if request.base_url + '/' == exc.new_url.split('?')[0]: + buf.append(' The URL was defined with a trailing slash so ' + 'Flask will automatically redirect to the URL ' + 'with the trailing slash if it was accessed ' + 'without one.') + + buf.append(' Make sure to directly send your %s-request to this URL ' + 'since we can\'t make browsers or HTTP clients redirect ' + 'with form data reliably or without user interaction.' % + request.method) + buf.append('\n\nNote: this exception is only raised in debug mode') + AssertionError.__init__(self, ''.join(buf).encode('utf-8')) + + +def attach_enctype_error_multidict(request): + """Since Flask 0.8 we're monkeypatching the files object in case a + request is detected that does not use multipart form data but the files + object is accessed. + """ + oldcls = request.files.__class__ + class newcls(oldcls): + def __getitem__(self, key): + try: + return oldcls.__getitem__(self, key) + except KeyError, e: + if key not in request.form: + raise + raise DebugFilesKeyError(request, key) + newcls.__name__ = oldcls.__name__ + newcls.__module__ = oldcls.__module__ + request.files.__class__ = newcls diff --git a/flask/ext/__init__.py b/flask/ext/__init__.py new file mode 100644 index 00000000..f29958a1 --- /dev/null +++ b/flask/ext/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" + flask.ext + ~~~~~~~~~ + + Redirect imports for extensions. This module basically makes it possible + for us to transition from flaskext.foo to flask_foo without having to + force all extensions to upgrade at the same time. + + When a user does ``from flask.ext.foo import bar`` it will attempt to + import ``from flask_foo import bar`` first and when that fails it will + try to import ``from flaskext.foo import bar``. + + We're switching from namespace packages because it was just too painful for + everybody involved. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + + +def setup(): + from ..exthook import ExtensionImporter + importer = ExtensionImporter(['flask_%s', 'flaskext.%s'], __name__) + importer.install() + + +setup() +del setup diff --git a/flask/exthook.py b/flask/exthook.py new file mode 100644 index 00000000..bb1deb29 --- /dev/null +++ b/flask/exthook.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +""" + flask.exthook + ~~~~~~~~~~~~~ + + Redirect imports for extensions. This module basically makes it possible + for us to transition from flaskext.foo to flask_foo without having to + force all extensions to upgrade at the same time. + + When a user does ``from flask.ext.foo import bar`` it will attempt to + import ``from flask_foo import bar`` first and when that fails it will + try to import ``from flaskext.foo import bar``. + + We're switching from namespace packages because it was just too painful for + everybody involved. + + This is used by `flask.ext`. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import sys +import os + + +class ExtensionImporter(object): + """This importer redirects imports from this submodule to other locations. + This makes it possible to transition from the old flaskext.name to the + newer flask_name without people having a hard time. + """ + + def __init__(self, module_choices, wrapper_module): + self.module_choices = module_choices + self.wrapper_module = wrapper_module + self.prefix = wrapper_module + '.' + self.prefix_cutoff = wrapper_module.count('.') + 1 + + def __eq__(self, other): + return self.__class__.__module__ == other.__class__.__module__ and \ + self.__class__.__name__ == other.__class__.__name__ and \ + self.wrapper_module == other.wrapper_module and \ + self.module_choices == other.module_choices + + def __ne__(self, other): + return not self.__eq__(other) + + def install(self): + sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] + + def find_module(self, fullname, path=None): + if fullname.startswith(self.prefix): + return self + + def load_module(self, fullname): + if fullname in sys.modules: + return sys.modules[fullname] + modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] + for path in self.module_choices: + realname = path % modname + try: + __import__(realname) + except ImportError: + exc_type, exc_value, tb = sys.exc_info() + # since we only establish the entry in sys.modules at the + # very this seems to be redundant, but if recursive imports + # happen we will call into the move import a second time. + # On the second invocation we still don't have an entry for + # fullname in sys.modules, but we will end up with the same + # fake module name and that import will succeed since this + # one already has a temporary entry in the modules dict. + # Since this one "succeeded" temporarily that second + # invocation now will have created a fullname entry in + # sys.modules which we have to kill. + sys.modules.pop(fullname, None) + + # If it's an important traceback we reraise it, otherwise + # we swallow it and try the next choice. The skipped frame + # is the one from __import__ above which we don't care about + if self.is_important_traceback(realname, tb): + raise exc_type, exc_value, tb.tb_next + continue + module = sys.modules[fullname] = sys.modules[realname] + if '.' not in modname: + setattr(sys.modules[self.wrapper_module], modname, module) + return module + raise ImportError('No module named %s' % fullname) + + def is_important_traceback(self, important_module, tb): + """Walks a traceback's frames and checks if any of the frames + originated in the given important module. If that is the case then we + were able to import the module itself but apparently something went + wrong when the module was imported. (Eg: import of an import failed). + """ + while tb is not None: + if self.is_important_frame(important_module, tb): + return True + tb = tb.tb_next + return False + + def is_important_frame(self, important_module, tb): + """Checks a single frame if it's important.""" + g = tb.tb_frame.f_globals + if '__name__' not in g: + return False + + module_name = g['__name__'] + + # Python 2.7 Behavior. Modules are cleaned up late so the + # name shows up properly here. Success! + if module_name == important_module: + return True + + # Some python verisons will will clean up modules so early that the + # module name at that point is no longer set. Try guessing from + # the filename then. + filename = os.path.abspath(tb.tb_frame.f_code.co_filename) + test_string = os.path.sep + important_module.replace('.', os.path.sep) + return test_string + '.py' in filename or \ + test_string + os.path.sep + '__init__.py' in filename diff --git a/flask/globals.py b/flask/globals.py new file mode 100644 index 00000000..16580d16 --- /dev/null +++ b/flask/globals.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" + flask.globals + ~~~~~~~~~~~~~ + + Defines all the global objects that are proxies to the current + active context. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from functools import partial +from werkzeug.local import LocalStack, LocalProxy + +def _lookup_object(name): + top = _request_ctx_stack.top + if top is None: + raise RuntimeError('working outside of request context') + return getattr(top, name) + + +# context locals +_request_ctx_stack = LocalStack() +current_app = LocalProxy(partial(_lookup_object, 'app')) +request = LocalProxy(partial(_lookup_object, 'request')) +session = LocalProxy(partial(_lookup_object, 'session')) +g = LocalProxy(partial(_lookup_object, 'g')) diff --git a/flask/helpers.py b/flask/helpers.py new file mode 100644 index 00000000..72c8f170 --- /dev/null +++ b/flask/helpers.py @@ -0,0 +1,649 @@ +# -*- coding: utf-8 -*- +""" + flask.helpers + ~~~~~~~~~~~~~ + + Implements various helpers. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import os +import sys +import posixpath +import mimetypes +from time import time +from zlib import adler32 +from threading import RLock + +# try to load the best simplejson implementation available. If JSON +# is not installed, we add a failing class. +json_available = True +json = None +try: + import simplejson as json +except ImportError: + try: + import json + except ImportError: + try: + # Google Appengine offers simplejson via django + from django.utils import simplejson as json + except ImportError: + json_available = False + + +from werkzeug.datastructures import Headers +from werkzeug.exceptions import NotFound + +# this was moved in 0.7 +try: + from werkzeug.wsgi import wrap_file +except ImportError: + from werkzeug.utils import wrap_file + +from jinja2 import FileSystemLoader + +from .globals import session, _request_ctx_stack, current_app, request + + +def _assert_have_json(): + """Helper function that fails if JSON is unavailable.""" + if not json_available: + raise RuntimeError('simplejson not installed') + +# figure out if simplejson escapes slashes. This behaviour was changed +# from one version to another without reason. +if not json_available or '\\/' not in json.dumps('/'): + + def _tojson_filter(*args, **kwargs): + if __debug__: + _assert_have_json() + return json.dumps(*args, **kwargs).replace('/', '\\/') +else: + _tojson_filter = json.dumps + + +# sentinel +_missing = object() + + +# what separators does this operating system provide that are not a slash? +# this is used by the send_from_directory function to ensure that nobody is +# able to access files from outside the filesystem. +_os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep] + if sep not in (None, '/')) + + +def _endpoint_from_view_func(view_func): + """Internal helper that returns the default endpoint for a given + function. This always is the function name. + """ + assert view_func is not None, 'expected view func if endpoint ' \ + 'is not provided.' + return view_func.__name__ + + +def jsonify(*args, **kwargs): + """Creates a :class:`~flask.Response` with the JSON representation of + the given arguments with an `application/json` mimetype. The arguments + to this function are the same as to the :class:`dict` constructor. + + Example usage:: + + @app.route('/_get_current_user') + def get_current_user(): + return jsonify(username=g.user.username, + email=g.user.email, + id=g.user.id) + + This will send a JSON response like this to the browser:: + + { + "username": "admin", + "email": "admin@localhost", + "id": 42 + } + + This requires Python 2.6 or an installed version of simplejson. For + security reasons only objects are supported toplevel. For more + information about this, have a look at :ref:`json-security`. + + .. versionadded:: 0.2 + """ + if __debug__: + _assert_have_json() + return current_app.response_class(json.dumps(dict(*args, **kwargs), + indent=None if request.is_xhr else 2), mimetype='application/json') + + +def make_response(*args): + """Sometimes it is necessary to set additional headers in a view. Because + views do not have to return response objects but can return a value that + is converted into a response object by Flask itself, it becomes tricky to + add headers to it. This function can be called instead of using a return + and you will get a response object which you can use to attach headers. + + If view looked like this and you want to add a new header:: + + def index(): + return render_template('index.html', foo=42) + + You can now do something like this:: + + def index(): + response = make_response(render_template('index.html', foo=42)) + response.headers['X-Parachutes'] = 'parachutes are cool' + return response + + This function accepts the very same arguments you can return from a + view function. This for example creates a response with a 404 error + code:: + + response = make_response(render_template('not_found.html'), 404) + + The other use case of this function is to force the return value of a + view function into a response which is helpful with view + decorators:: + + response = make_response(view_function()) + response.headers['X-Parachutes'] = 'parachutes are cool' + + Internally this function does the following things: + + - if no arguments are passed, it creates a new response argument + - if one argument is passed, :meth:`flask.Flask.make_response` + is invoked with it. + - if more than one argument is passed, the arguments are passed + to the :meth:`flask.Flask.make_response` function as tuple. + + .. versionadded:: 0.6 + """ + if not args: + return current_app.response_class() + if len(args) == 1: + args = args[0] + return current_app.make_response(args) + + +def url_for(endpoint, **values): + """Generates a URL to the given endpoint with the method provided. + + Variable arguments that are unknown to the target endpoint are appended + to the generated URL as query arguments. If the value of a query argument + is `None`, the whole pair is skipped. In case blueprints are active + you can shortcut references to the same blueprint by prefixing the + local endpoint with a dot (``.``). + + This will reference the index function local to the current blueprint:: + + url_for('.index') + + For more information, head over to the :ref:`Quickstart `. + + :param endpoint: the endpoint of the URL (name of the function) + :param values: the variable arguments of the URL rule + :param _external: if set to `True`, an absolute URL is generated. + """ + ctx = _request_ctx_stack.top + blueprint_name = request.blueprint + if not ctx.request._is_old_module: + if endpoint[:1] == '.': + if blueprint_name is not None: + endpoint = blueprint_name + endpoint + else: + endpoint = endpoint[1:] + else: + # TODO: get rid of this deprecated functionality in 1.0 + if '.' not in endpoint: + if blueprint_name is not None: + endpoint = blueprint_name + '.' + endpoint + elif endpoint.startswith('.'): + endpoint = endpoint[1:] + external = values.pop('_external', False) + ctx.app.inject_url_defaults(endpoint, values) + return ctx.url_adapter.build(endpoint, values, force_external=external) + + +def get_template_attribute(template_name, attribute): + """Loads a macro (or variable) a template exports. This can be used to + invoke a macro from within Python code. If you for example have a + template named `_cider.html` with the following contents: + + .. sourcecode:: html+jinja + + {% macro hello(name) %}Hello {{ name }}!{% endmacro %} + + You can access this from Python code like this:: + + hello = get_template_attribute('_cider.html', 'hello') + return hello('World') + + .. versionadded:: 0.2 + + :param template_name: the name of the template + :param attribute: the name of the variable of macro to acccess + """ + return getattr(current_app.jinja_env.get_template(template_name).module, + attribute) + + +def flash(message, category='message'): + """Flashes a message to the next request. In order to remove the + flashed message from the session and to display it to the user, + the template has to call :func:`get_flashed_messages`. + + .. versionchanged: 0.3 + `category` parameter added. + + :param message: the message to be flashed. + :param category: the category for the message. The following values + are recommended: ``'message'`` for any kind of message, + ``'error'`` for errors, ``'info'`` for information + messages and ``'warning'`` for warnings. However any + kind of string can be used as category. + """ + session.setdefault('_flashes', []).append((category, message)) + + +def get_flashed_messages(with_categories=False): + """Pulls all flashed messages from the session and returns them. + Further calls in the same request to the function will return + the same messages. By default just the messages are returned, + but when `with_categories` is set to `True`, the return value will + be a list of tuples in the form ``(category, message)`` instead. + + Example usage: + + .. sourcecode:: html+jinja + + {% for category, msg in get_flashed_messages(with_categories=true) %} +

{{ msg }} + {% endfor %} + + .. versionchanged:: 0.3 + `with_categories` parameter added. + + :param with_categories: set to `True` to also receive categories. + """ + flashes = _request_ctx_stack.top.flashes + if flashes is None: + _request_ctx_stack.top.flashes = flashes = session.pop('_flashes') \ + if '_flashes' in session else [] + if not with_categories: + return [x[1] for x in flashes] + return flashes + + +def send_file(filename_or_fp, mimetype=None, as_attachment=False, + attachment_filename=None, add_etags=True, + cache_timeout=60 * 60 * 12, conditional=False): + """Sends the contents of a file to the client. This will use the + most efficient method available and configured. By default it will + try to use the WSGI server's file_wrapper support. Alternatively + you can set the application's :attr:`~Flask.use_x_sendfile` attribute + to ``True`` to directly emit an `X-Sendfile` header. This however + requires support of the underlying webserver for `X-Sendfile`. + + By default it will try to guess the mimetype for you, but you can + also explicitly provide one. For extra security you probably want + to send certain files as attachment (HTML for instance). The mimetype + guessing requires a `filename` or an `attachment_filename` to be + provided. + + Please never pass filenames to this function from user sources without + checking them first. Something like this is usually sufficient to + avoid security problems:: + + if '..' in filename or filename.startswith('/'): + abort(404) + + .. versionadded:: 0.2 + + .. versionadded:: 0.5 + The `add_etags`, `cache_timeout` and `conditional` parameters were + added. The default behaviour is now to attach etags. + + .. versionchanged:: 0.7 + mimetype guessing and etag support for file objects was + deprecated because it was unreliable. Pass a filename if you are + able to, otherwise attach an etag yourself. This functionality + will be removed in Flask 1.0 + + :param filename_or_fp: the filename of the file to send. This is + relative to the :attr:`~Flask.root_path` if a + relative path is specified. + Alternatively a file object might be provided + in which case `X-Sendfile` might not work and + fall back to the traditional method. Make sure + that the file pointer is positioned at the start + of data to send before calling :func:`send_file`. + :param mimetype: the mimetype of the file if provided, otherwise + auto detection happens. + :param as_attachment: set to `True` if you want to send this file with + a ``Content-Disposition: attachment`` header. + :param attachment_filename: the filename for the attachment if it + differs from the file's filename. + :param add_etags: set to `False` to disable attaching of etags. + :param conditional: set to `True` to enable conditional responses. + :param cache_timeout: the timeout in seconds for the headers. + """ + mtime = None + if isinstance(filename_or_fp, basestring): + filename = filename_or_fp + file = None + else: + from warnings import warn + file = filename_or_fp + filename = getattr(file, 'name', None) + + # XXX: this behaviour is now deprecated because it was unreliable. + # removed in Flask 1.0 + if not attachment_filename and not mimetype \ + and isinstance(filename, basestring): + warn(DeprecationWarning('The filename support for file objects ' + 'passed to send_file is now deprecated. Pass an ' + 'attach_filename if you want mimetypes to be guessed.'), + stacklevel=2) + if add_etags: + warn(DeprecationWarning('In future flask releases etags will no ' + 'longer be generated for file objects passed to the send_file ' + 'function because this behaviour was unreliable. Pass ' + 'filenames instead if possible, otherwise attach an etag ' + 'yourself based on another value'), stacklevel=2) + + if filename is not None: + if not os.path.isabs(filename): + filename = os.path.join(current_app.root_path, filename) + if mimetype is None and (filename or attachment_filename): + mimetype = mimetypes.guess_type(filename or attachment_filename)[0] + if mimetype is None: + mimetype = 'application/octet-stream' + + headers = Headers() + if as_attachment: + if attachment_filename is None: + if filename is None: + raise TypeError('filename unavailable, required for ' + 'sending as attachment') + attachment_filename = os.path.basename(filename) + headers.add('Content-Disposition', 'attachment', + filename=attachment_filename) + + if current_app.use_x_sendfile and filename: + if file is not None: + file.close() + headers['X-Sendfile'] = filename + data = None + else: + if file is None: + file = open(filename, 'rb') + mtime = os.path.getmtime(filename) + data = wrap_file(request.environ, file) + + rv = current_app.response_class(data, mimetype=mimetype, headers=headers, + direct_passthrough=True) + + # if we know the file modification date, we can store it as the + # the time of the last modification. + if mtime is not None: + rv.last_modified = int(mtime) + + rv.cache_control.public = True + if cache_timeout: + rv.cache_control.max_age = cache_timeout + rv.expires = int(time() + cache_timeout) + + if add_etags and filename is not None: + rv.set_etag('flask-%s-%s-%s' % ( + os.path.getmtime(filename), + os.path.getsize(filename), + adler32( + filename.encode('utf8') if isinstance(filename, unicode) + else filename + ) & 0xffffffff + )) + if conditional: + rv = rv.make_conditional(request) + # make sure we don't send x-sendfile for servers that + # ignore the 304 status code for x-sendfile. + if rv.status_code == 304: + rv.headers.pop('x-sendfile', None) + return rv + + +def safe_join(directory, filename): + """Safely join `directory` and `filename`. + + Example usage:: + + @app.route('/wiki/') + def wiki_page(filename): + filename = safe_join(app.config['WIKI_FOLDER'], filename) + with open(filename, 'rb') as fd: + content = fd.read() # Read and process the file content... + + :param directory: the base directory. + :param filename: the untrusted filename relative to that directory. + :raises: :class:`~werkzeug.exceptions.NotFound` if the resulting path + would fall out of `directory`. + """ + filename = posixpath.normpath(filename) + for sep in _os_alt_seps: + if sep in filename: + raise NotFound() + if os.path.isabs(filename) or filename.startswith('../'): + raise NotFound() + return os.path.join(directory, filename) + + +def send_from_directory(directory, filename, **options): + """Send a file from a given directory with :func:`send_file`. This + is a secure way to quickly expose static files from an upload folder + or something similar. + + Example usage:: + + @app.route('/uploads/') + def download_file(filename): + return send_from_directory(app.config['UPLOAD_FOLDER'], + filename, as_attachment=True) + + .. admonition:: Sending files and Performance + + It is strongly recommended to activate either `X-Sendfile` support in + your webserver or (if no authentication happens) to tell the webserver + to serve files for the given path on its own without calling into the + web application for improved performance. + + .. versionadded:: 0.5 + + :param directory: the directory where all the files are stored. + :param filename: the filename relative to that directory to + download. + :param options: optional keyword arguments that are directly + forwarded to :func:`send_file`. + """ + filename = safe_join(directory, filename) + if not os.path.isfile(filename): + raise NotFound() + return send_file(filename, conditional=True, **options) + + +def get_root_path(import_name): + """Returns the path to a package or cwd if that cannot be found. This + returns the path of a package or the folder that contains a module. + + Not to be confused with the package path returned by :func:`find_package`. + """ + __import__(import_name) + try: + directory = os.path.dirname(sys.modules[import_name].__file__) + return os.path.abspath(directory) + except AttributeError: + # this is necessary in case we are running from the interactive + # python shell. It will never be used for production code however + return os.getcwd() + + +def find_package(import_name): + """Finds a package and returns the prefix (or None if the package is + not installed) as well as the folder that contains the package or + module as a tuple. The package path returned is the module that would + have to be added to the pythonpath in order to make it possible to + import the module. The prefix is the path below which a UNIX like + folder structure exists (lib, share etc.). + """ + __import__(import_name) + root_mod = sys.modules[import_name.split('.')[0]] + package_path = getattr(root_mod, '__file__', None) + if package_path is None: + # support for the interactive python shell + package_path = os.getcwd() + else: + package_path = os.path.abspath(os.path.dirname(package_path)) + if hasattr(root_mod, '__path__'): + package_path = os.path.dirname(package_path) + + # leave the egg wrapper folder or the actual .egg on the filesystem + test_package_path = package_path + if os.path.basename(test_package_path).endswith('.egg'): + test_package_path = os.path.dirname(test_package_path) + + site_parent, site_folder = os.path.split(test_package_path) + py_prefix = os.path.abspath(sys.prefix) + if test_package_path.startswith(py_prefix): + return py_prefix, package_path + elif site_folder.lower() == 'site-packages': + parent, folder = os.path.split(site_parent) + # Windows like installations + if folder.lower() == 'lib': + base_dir = parent + # UNIX like installations + elif os.path.basename(parent).lower() == 'lib': + base_dir = os.path.dirname(parent) + else: + base_dir = site_parent + return base_dir, package_path + return None, package_path + + +class locked_cached_property(object): + """A decorator that converts a function into a lazy property. The + function wrapped is called the first time to retrieve the result + and then that calculated result is used the next time you access + the value. Works like the one in Werkzeug but has a lock for + thread safety. + """ + + def __init__(self, func, name=None, doc=None): + self.__name__ = name or func.__name__ + self.__module__ = func.__module__ + self.__doc__ = doc or func.__doc__ + self.func = func + self.lock = RLock() + + def __get__(self, obj, type=None): + if obj is None: + return self + with self.lock: + value = obj.__dict__.get(self.__name__, _missing) + if value is _missing: + value = self.func(obj) + obj.__dict__[self.__name__] = value + return value + + +class _PackageBoundObject(object): + + def __init__(self, import_name, template_folder=None): + #: The name of the package or module. Do not change this once + #: it was set by the constructor. + self.import_name = import_name + + #: location of the templates. `None` if templates should not be + #: exposed. + self.template_folder = template_folder + + #: Where is the app root located? + self.root_path = get_root_path(self.import_name) + + self._static_folder = None + self._static_url_path = None + + def _get_static_folder(self): + if self._static_folder is not None: + return os.path.join(self.root_path, self._static_folder) + def _set_static_folder(self, value): + self._static_folder = value + static_folder = property(_get_static_folder, _set_static_folder) + del _get_static_folder, _set_static_folder + + def _get_static_url_path(self): + if self._static_url_path is None: + if self.static_folder is None: + return None + return '/' + os.path.basename(self.static_folder) + return self._static_url_path + def _set_static_url_path(self, value): + self._static_url_path = value + static_url_path = property(_get_static_url_path, _set_static_url_path) + del _get_static_url_path, _set_static_url_path + + @property + def has_static_folder(self): + """This is `True` if the package bound object's container has a + folder named ``'static'``. + + .. versionadded:: 0.5 + """ + return self.static_folder is not None + + @locked_cached_property + def jinja_loader(self): + """The Jinja loader for this package bound object. + + .. versionadded:: 0.5 + """ + if self.template_folder is not None: + return FileSystemLoader(os.path.join(self.root_path, + self.template_folder)) + + def send_static_file(self, filename): + """Function used internally to send static files from the static + folder to the browser. + + .. versionadded:: 0.5 + """ + if not self.has_static_folder: + raise RuntimeError('No static folder for this object') + return send_from_directory(self.static_folder, filename) + + def open_resource(self, resource, mode='rb'): + """Opens a resource from the application's resource folder. To see + how this works, consider the following folder structure:: + + /myapplication.py + /schema.sql + /static + /style.css + /templates + /layout.html + /index.html + + If you want to open the `schema.sql` file you would do the + following:: + + with app.open_resource('schema.sql') as f: + contents = f.read() + do_something_with(contents) + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. + """ + if mode not in ('r', 'rb'): + raise ValueError('Resources can only be opened for reading') + return open(os.path.join(self.root_path, resource), mode) diff --git a/flask/logging.py b/flask/logging.py new file mode 100644 index 00000000..b992aef8 --- /dev/null +++ b/flask/logging.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" + flask.logging + ~~~~~~~~~~~~~ + + Implements the logging support for Flask. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import absolute_import + +from logging import getLogger, StreamHandler, Formatter, getLoggerClass, DEBUG + + +def create_logger(app): + """Creates a logger for the given application. This logger works + similar to a regular Python logger but changes the effective logging + level based on the application's debug flag. Furthermore this + function also removes all attached handlers in case there was a + logger with the log name before. + """ + Logger = getLoggerClass() + + class DebugLogger(Logger): + def getEffectiveLevel(x): + return DEBUG if app.debug else Logger.getEffectiveLevel(x) + + class DebugHandler(StreamHandler): + def emit(x, record): + StreamHandler.emit(x, record) if app.debug else None + + handler = DebugHandler() + handler.setLevel(DEBUG) + handler.setFormatter(Formatter(app.debug_log_format)) + logger = getLogger(app.logger_name) + # just in case that was not a new logger, get rid of all the handlers + # already attached to it. + del logger.handlers[:] + logger.__class__ = DebugLogger + logger.addHandler(handler) + return logger diff --git a/flask/module.py b/flask/module.py new file mode 100644 index 00000000..1c4f466c --- /dev/null +++ b/flask/module.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" + flask.module + ~~~~~~~~~~~~ + + Implements a class that represents module blueprints. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +import os + +from .blueprints import Blueprint + + +def blueprint_is_module(bp): + """Used to figure out if something is actually a module""" + return isinstance(bp, Module) + + +class Module(Blueprint): + """Deprecated module support. Until Flask 0.6 modules were a different + name of the concept now available as blueprints in Flask. They are + essentially doing the same but have some bad semantics for templates and + static files that were fixed with blueprints. + + .. versionchanged:: 0.7 + Modules were deprecated in favor for blueprints. + """ + + def __init__(self, import_name, name=None, url_prefix=None, + static_path=None, subdomain=None): + if name is None: + assert '.' in import_name, 'name required if package name ' \ + 'does not point to a submodule' + name = import_name.rsplit('.', 1)[1] + Blueprint.__init__(self, name, import_name, url_prefix=url_prefix, + subdomain=subdomain, template_folder='templates') + + if os.path.isdir(os.path.join(self.root_path, 'static')): + self._static_folder = 'static' diff --git a/flask/session.py b/flask/session.py new file mode 100644 index 00000000..1a43fdc1 --- /dev/null +++ b/flask/session.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +""" + flask.session + ~~~~~~~~~~~~~ + + This module used to flask with the session global so we moved it + over to flask.sessions + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from warnings import warn +warn(DeprecationWarning('please use flask.sessions instead')) + +from .sessions import SecureCookieSession, NullSession + +Session = SecureCookieSession +_NullSession = NullSession diff --git a/flask/sessions.py b/flask/sessions.py new file mode 100644 index 00000000..2795bb1f --- /dev/null +++ b/flask/sessions.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +""" + flask.sessions + ~~~~~~~~~~~~~~ + + Implements cookie based sessions based on Werkzeug's secure cookie + system. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from datetime import datetime +from werkzeug.contrib.securecookie import SecureCookie + + +class SessionMixin(object): + """Expands a basic dictionary with an accessors that are expected + by Flask extensions and users for the session. + """ + + def _get_permanent(self): + return self.get('_permanent', False) + + def _set_permanent(self, value): + self['_permanent'] = bool(value) + + #: this reflects the ``'_permanent'`` key in the dict. + permanent = property(_get_permanent, _set_permanent) + del _get_permanent, _set_permanent + + #: some session backends can tell you if a session is new, but that is + #: not necessarily guaranteed. Use with caution. The default mixin + #: implementation just hardcodes `False` in. + new = False + + #: for some backends this will always be `True`, but some backends will + #: default this to false and detect changes in the dictionary for as + #: long as changes do not happen on mutable structures in the session. + #: The default mixin implementation just hardcodes `True` in. + modified = True + + +class SecureCookieSession(SecureCookie, SessionMixin): + """Expands the session with support for switching between permanent + and non-permanent sessions. + """ + + +class NullSession(SecureCookieSession): + """Class used to generate nicer error messages if sessions are not + available. Will still allow read-only access to the empty session + but fail on setting. + """ + + def _fail(self, *args, **kwargs): + raise RuntimeError('the session is unavailable because no secret ' + 'key was set. Set the secret_key on the ' + 'application to something unique and secret.') + __setitem__ = __delitem__ = clear = pop = popitem = \ + update = setdefault = _fail + del _fail + + +class SessionInterface(object): + """The basic interface you have to implement in order to replace the + default session interface which uses werkzeug's securecookie + implementation. The only methods you have to implement are + :meth:`open_session` and :meth:`save_session`, the others have + useful defaults which you don't need to change. + + The session object returned by the :meth:`open_session` method has to + provide a dictionary like interface plus the properties and methods + from the :class:`SessionMixin`. We recommend just subclassing a dict + and adding that mixin:: + + class Session(dict, SessionMixin): + pass + + If :meth:`open_session` returns `None` Flask will call into + :meth:`make_null_session` to create a session that acts as replacement + if the session support cannot work because some requirement is not + fulfilled. The default :class:`NullSession` class that is created + will complain that the secret key was not set. + + To replace the session interface on an application all you have to do + is to assign :attr:`flask.Flask.session_interface`:: + + app = Flask(__name__) + app.session_interface = MySessionInterface() + + .. versionadded:: 0.8 + """ + + #: :meth:`make_null_session` will look here for the class that should + #: be created when a null session is requested. Likewise the + #: :meth:`is_null_session` method will perform a typecheck against + #: this type. + null_session_class = NullSession + + def make_null_session(self, app): + """Creates a null session which acts as a replacement object if the + real session support could not be loaded due to a configuration + error. This mainly aids the user experience because the job of the + null session is to still support lookup without complaining but + modifications are answered with a helpful error message of what + failed. + + This creates an instance of :attr:`null_session_class` by default. + """ + return self.null_session_class() + + def is_null_session(self, obj): + """Checks if a given object is a null session. Null sessions are + not asked to be saved. + + This checks if the object is an instance of :attr:`null_session_class` + by default. + """ + return isinstance(obj, self.null_session_class) + + def get_cookie_domain(self, app): + """Helpful helper method that returns the cookie domain that should + be used for the session cookie if session cookies are used. + """ + if app.config['SESSION_COOKIE_DOMAIN'] is not None: + return app.config['SESSION_COOKIE_DOMAIN'] + if app.config['SERVER_NAME'] is not None: + # chop of the port which is usually not supported by browsers + return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] + + def get_cookie_path(self, app): + """Returns the path for which the cookie should be valid. The + default implementation uses the value from the SESSION_COOKIE_PATH`` + config var if it's set, and falls back to ``APPLICATION_ROOT`` or + uses ``/`` if it's `None`. + """ + return app.config['SESSION_COOKIE_PATH'] or \ + app.config['APPLICATION_ROOT'] or '/' + + def get_cookie_httponly(self, app): + """Returns True if the session cookie should be httponly. This + currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` + config var. + """ + return app.config['SESSION_COOKIE_HTTPONLY'] + + def get_cookie_secure(self, app): + """Returns True if the cookie should be secure. This currently + just returns the value of the ``SESSION_COOKIE_SECURE`` setting. + """ + return app.config['SESSION_COOKIE_SECURE'] + + def get_expiration_time(self, app, session): + """A helper method that returns an expiration date for the session + or `None` if the session is linked to the browser session. The + default implementation returns now + the permanent session + lifetime configured on the application. + """ + if session.permanent: + return datetime.utcnow() + app.permanent_session_lifetime + + def open_session(self, app, request): + """This method has to be implemented and must either return `None` + in case the loading failed because of a configuration error or an + instance of a session object which implements a dictionary like + interface + the methods and attributes on :class:`SessionMixin`. + """ + raise NotImplementedError() + + def save_session(self, app, session, response): + """This is called for actual sessions returned by :meth:`open_session` + at the end of the request. This is still called during a request + context so if you absolutely need access to the request you can do + that. + """ + raise NotImplementedError() + + +class SecureCookieSessionInterface(SessionInterface): + """The cookie session interface that uses the Werkzeug securecookie + as client side session backend. + """ + session_class = SecureCookieSession + + def open_session(self, app, request): + key = app.secret_key + if key is not None: + return self.session_class.load_cookie(request, + app.session_cookie_name, + secret_key=key) + + def save_session(self, app, session, response): + expires = self.get_expiration_time(app, session) + domain = self.get_cookie_domain(app) + path = self.get_cookie_path(app) + httponly = self.get_cookie_httponly(app) + secure = self.get_cookie_secure(app) + if session.modified and not session: + response.delete_cookie(app.session_cookie_name, path=path, + domain=domain) + else: + session.save_cookie(response, app.session_cookie_name, path=path, + expires=expires, httponly=httponly, + secure=secure, domain=domain) diff --git a/flask/signals.py b/flask/signals.py new file mode 100644 index 00000000..eeb763d4 --- /dev/null +++ b/flask/signals.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +""" + flask.signals + ~~~~~~~~~~~~~ + + Implements signals based on blinker if available, otherwise + falls silently back to a noop + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +signals_available = False +try: + from blinker import Namespace + signals_available = True +except ImportError: + class Namespace(object): + def signal(self, name, doc=None): + return _FakeSignal(name, doc) + + class _FakeSignal(object): + """If blinker is unavailable, create a fake class with the same + interface that allows sending of signals but will fail with an + error on anything else. Instead of doing anything on send, it + will just ignore the arguments and do nothing instead. + """ + + def __init__(self, name, doc=None): + self.name = name + self.__doc__ = doc + def _fail(self, *args, **kwargs): + raise RuntimeError('signalling support is unavailable ' + 'because the blinker library is ' + 'not installed.') + send = lambda *a, **kw: None + connect = disconnect = has_receivers_for = receivers_for = \ + temporarily_connected_to = connected_to = _fail + del _fail + +# the namespace for code signals. If you are not flask code, do +# not put signals in here. Create your own namespace instead. +_signals = Namespace() + + +# core signals. For usage examples grep the sourcecode or consult +# the API documentation in docs/api.rst as well as docs/signals.rst +template_rendered = _signals.signal('template-rendered') +request_started = _signals.signal('request-started') +request_finished = _signals.signal('request-finished') +request_tearing_down = _signals.signal('request-tearing-down') +got_request_exception = _signals.signal('got-request-exception') diff --git a/flask/templating.py b/flask/templating.py new file mode 100644 index 00000000..90e8772a --- /dev/null +++ b/flask/templating.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +""" + flask.templating + ~~~~~~~~~~~~~~~~ + + Implements the bridge to Jinja2. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import posixpath +from jinja2 import BaseLoader, Environment as BaseEnvironment, \ + TemplateNotFound + +from .globals import _request_ctx_stack +from .signals import template_rendered +from .module import blueprint_is_module + + +def _default_template_ctx_processor(): + """Default template context processor. Injects `request`, + `session` and `g`. + """ + reqctx = _request_ctx_stack.top + return dict( + config=reqctx.app.config, + request=reqctx.request, + session=reqctx.session, + g=reqctx.g + ) + + +class Environment(BaseEnvironment): + """Works like a regular Jinja2 environment but has some additional + knowledge of how Flask's blueprint works so that it can prepend the + name of the blueprint to referenced templates if necessary. + """ + + def __init__(self, app, **options): + if 'loader' not in options: + options['loader'] = app.create_global_jinja_loader() + BaseEnvironment.__init__(self, **options) + self.app = app + + +class DispatchingJinjaLoader(BaseLoader): + """A loader that looks for templates in the application and all + the blueprint folders. + """ + + def __init__(self, app): + self.app = app + + def get_source(self, environment, template): + for loader, local_name in self._iter_loaders(template): + try: + return loader.get_source(environment, local_name) + except TemplateNotFound: + pass + + raise TemplateNotFound(template) + + def _iter_loaders(self, template): + loader = self.app.jinja_loader + if loader is not None: + yield loader, template + + # old style module based loaders in case we are dealing with a + # blueprint that is an old style module + try: + module, local_name = posixpath.normpath(template).split('/', 1) + blueprint = self.app.blueprints[module] + if blueprint_is_module(blueprint): + loader = blueprint.jinja_loader + if loader is not None: + yield loader, local_name + except (ValueError, KeyError): + pass + + for blueprint in self.app.blueprints.itervalues(): + if blueprint_is_module(blueprint): + continue + loader = blueprint.jinja_loader + if loader is not None: + yield loader, template + + def list_templates(self): + result = set() + loader = self.app.jinja_loader + if loader is not None: + result.update(loader.list_templates()) + + for name, blueprint in self.app.blueprints.iteritems(): + loader = blueprint.jinja_loader + if loader is not None: + for template in loader.list_templates(): + prefix = '' + if blueprint_is_module(blueprint): + prefix = name + '/' + result.add(prefix + template) + + return list(result) + + +def _render(template, context, app): + """Renders the template and fires the signal""" + rv = template.render(context) + template_rendered.send(app, template=template, context=context) + return rv + + +def render_template(template_name, **context): + """Renders a template from the template folder with the given + context. + + :param template_name: the name of the template to be rendered + :param context: the variables that should be available in the + context of the template. + """ + ctx = _request_ctx_stack.top + ctx.app.update_template_context(context) + return _render(ctx.app.jinja_env.get_template(template_name), + context, ctx.app) + + +def render_template_string(source, **context): + """Renders a template from the given template source string + with the given context. + + :param template_name: the sourcecode of the template to be + rendered + :param context: the variables that should be available in the + context of the template. + """ + ctx = _request_ctx_stack.top + ctx.app.update_template_context(context) + return _render(ctx.app.jinja_env.from_string(source), + context, ctx.app) diff --git a/flask/testing.py b/flask/testing.py new file mode 100644 index 00000000..782b40f6 --- /dev/null +++ b/flask/testing.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +""" + flask.testing + ~~~~~~~~~~~~~ + + Implements test support helpers. This module is lazily imported + and usually not used in production environments. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +from contextlib import contextmanager +from werkzeug.test import Client, EnvironBuilder +from flask import _request_ctx_stack + + +def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): + """Creates a new test builder with some application defaults thrown in.""" + http_host = app.config.get('SERVER_NAME') + app_root = app.config.get('APPLICATION_ROOT') + if base_url is None: + base_url = 'http://%s/' % (http_host or 'localhost') + if app_root: + base_url += app_root.lstrip('/') + return EnvironBuilder(path, base_url, *args, **kwargs) + + +class FlaskClient(Client): + """Works like a regular Werkzeug test client but has some knowledge about + how Flask works to defer the cleanup of the request context stack to the + end of a with body when used in a with statement. For general information + about how to use this class refer to :class:`werkzeug.test.Client`. + + Basic usage is outlined in the :ref:`testing` chapter. + """ + + preserve_context = False + + @contextmanager + def session_transaction(self, *args, **kwargs): + """When used in combination with a with statement this opens a + session transaction. This can be used to modify the session that + the test client uses. Once the with block is left the session is + stored back. + + with client.session_transaction() as session: + session['value'] = 42 + + Internally this is implemented by going through a temporary test + request context and since session handling could depend on + request variables this function accepts the same arguments as + :meth:`~flask.Flask.test_request_context` which are directly + passed through. + """ + if self.cookie_jar is None: + raise RuntimeError('Session transactions only make sense ' + 'with cookies enabled.') + app = self.application + environ_overrides = kwargs.setdefault('environ_overrides', {}) + self.cookie_jar.inject_wsgi(environ_overrides) + outer_reqctx = _request_ctx_stack.top + with app.test_request_context(*args, **kwargs) as c: + sess = app.open_session(c.request) + if sess is None: + raise RuntimeError('Session backend did not open a session. ' + 'Check the configuration') + + # Since we have to open a new request context for the session + # handling we want to make sure that we hide out own context + # from the caller. By pushing the original request context + # (or None) on top of this and popping it we get exactly that + # behavior. It's important to not use the push and pop + # methods of the actual request context object since that would + # mean that cleanup handlers are called + _request_ctx_stack.push(outer_reqctx) + try: + yield sess + finally: + _request_ctx_stack.pop() + + resp = app.response_class() + if not app.session_interface.is_null_session(sess): + app.save_session(sess, resp) + headers = resp.get_wsgi_headers(c.request.environ) + self.cookie_jar.extract_wsgi(c.request.environ, headers) + + def open(self, *args, **kwargs): + kwargs.setdefault('environ_overrides', {}) \ + ['flask._preserve_context'] = self.preserve_context + + as_tuple = kwargs.pop('as_tuple', False) + buffered = kwargs.pop('buffered', False) + follow_redirects = kwargs.pop('follow_redirects', False) + builder = make_test_environ_builder(self.application, *args, **kwargs) + + return Client.open(self, builder, + as_tuple=as_tuple, + buffered=buffered, + follow_redirects=follow_redirects) + + def __enter__(self): + if self.preserve_context: + raise RuntimeError('Cannot nest client invocations') + self.preserve_context = True + return self + + def __exit__(self, exc_type, exc_value, tb): + self.preserve_context = False + + # on exit we want to clean up earlier. Normally the request context + # stays preserved until the next request in the same thread comes + # in. See RequestGlobals.push() for the general behavior. + top = _request_ctx_stack.top + if top is not None and top.preserved: + top.pop() diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py new file mode 100644 index 00000000..76a4d724 --- /dev/null +++ b/flask/testsuite/__init__.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite + ~~~~~~~~~~~~~~~ + + Tests Flask itself. The majority of Flask is already tested + as part of Werkzeug. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import os +import sys +import flask +import warnings +import unittest +from StringIO import StringIO +from functools import update_wrapper +from contextlib import contextmanager +from werkzeug.utils import import_string, find_modules + + +def add_to_path(path): + """Adds an entry to sys.path if it's not already there. This does + not append it but moves it to the front so that we can be sure it + is loaded. + """ + if not os.path.isdir(path): + raise RuntimeError('Tried to add nonexisting path') + + def _samefile(x, y): + try: + return os.path.samefile(x, y) + except (IOError, OSError): + return False + sys.path[:] = [x for x in sys.path if not _samefile(path, x)] + sys.path.insert(0, path) + + +def iter_suites(): + """Yields all testsuites.""" + for module in find_modules(__name__): + mod = import_string(module) + if hasattr(mod, 'suite'): + yield mod.suite() + + +def find_all_tests(suite): + """Yields all the tests and their names from a given suite.""" + suites = [suite] + while suites: + s = suites.pop() + try: + suites.extend(s) + except TypeError: + yield s, '%s.%s.%s' % ( + s.__class__.__module__, + s.__class__.__name__, + s._testMethodName + ) + + +@contextmanager +def catch_warnings(): + """Catch warnings in a with block in a list""" + # make sure deprecation warnings are active in tests + warnings.simplefilter('default', category=DeprecationWarning) + + filters = warnings.filters + warnings.filters = filters[:] + old_showwarning = warnings.showwarning + log = [] + def showwarning(message, category, filename, lineno, file=None, line=None): + log.append(locals()) + try: + warnings.showwarning = showwarning + yield log + finally: + warnings.filters = filters + warnings.showwarning = old_showwarning + + +@contextmanager +def catch_stderr(): + """Catch stderr in a StringIO""" + old_stderr = sys.stderr + sys.stderr = rv = StringIO() + try: + yield rv + finally: + sys.stderr = old_stderr + + +def emits_module_deprecation_warning(f): + def new_f(self, *args, **kwargs): + with catch_warnings() as log: + f(self, *args, **kwargs) + self.assert_(log, 'expected deprecation warning') + for entry in log: + self.assert_('Modules are deprecated' in str(entry['message'])) + return update_wrapper(new_f, f) + + +class FlaskTestCase(unittest.TestCase): + """Baseclass for all the tests that Flask uses. Use these methods + for testing instead of the camelcased ones in the baseclass for + consistency. + """ + + def ensure_clean_request_context(self): + # make sure we're not leaking a request context since we are + # testing flask internally in debug mode in a few cases + self.assert_equal(flask._request_ctx_stack.top, None) + + def setup(self): + pass + + def teardown(self): + pass + + def setUp(self): + self.setup() + + def tearDown(self): + unittest.TestCase.tearDown(self) + self.ensure_clean_request_context() + self.teardown() + + def assert_equal(self, x, y): + return self.assertEqual(x, y) + + def assert_raises(self, exc_type, callable=None, *args, **kwargs): + catcher = _ExceptionCatcher(self, exc_type) + if callable is None: + return catcher + with catcher: + callable(*args, **kwargs) + + +class _ExceptionCatcher(object): + + def __init__(self, test_case, exc_type): + self.test_case = test_case + self.exc_type = exc_type + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + exception_name = self.exc_type.__name__ + if exc_type is None: + self.test_case.fail('Expected exception of type %r' % + exception_name) + elif not issubclass(exc_type, self.exc_type): + raise exc_type, exc_value, tb + return True + + +class BetterLoader(unittest.TestLoader): + """A nicer loader that solves two problems. First of all we are setting + up tests from different sources and we're doing this programmatically + which breaks the default loading logic so this is required anyways. + Secondly this loader has a nicer interpolation for test names than the + default one so you can just do ``run-tests.py ViewTestCase`` and it + will work. + """ + + def getRootSuite(self): + return suite() + + def loadTestsFromName(self, name, module=None): + root = self.getRootSuite() + if name == 'suite': + return root + + all_tests = [] + for testcase, testname in find_all_tests(root): + if testname == name or \ + testname.endswith('.' + name) or \ + ('.' + name + '.') in testname or \ + testname.startswith(name + '.'): + all_tests.append(testcase) + + if not all_tests: + raise LookupError('could not find test case for "%s"' % name) + + if len(all_tests) == 1: + return all_tests[0] + rv = unittest.TestSuite() + for test in all_tests: + rv.addTest(test) + return rv + + +def setup_path(): + add_to_path(os.path.abspath(os.path.join( + os.path.dirname(__file__), 'test_apps'))) + + +def suite(): + """A testsuite that has all the Flask tests. You can use this + function to integrate the Flask tests into your own testsuite + in case you want to test that monkeypatches to Flask do not + break it. + """ + setup_path() + suite = unittest.TestSuite() + for other_suite in iter_suites(): + suite.addTest(other_suite) + return suite + + +def main(): + """Runs the testsuite as command line application.""" + try: + unittest.main(testLoader=BetterLoader(), defaultTest='suite') + except Exception, e: + print 'Error: %s' % e diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py new file mode 100644 index 00000000..1733f0a3 --- /dev/null +++ b/flask/testsuite/basic.py @@ -0,0 +1,1051 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.basic + ~~~~~~~~~~~~~~~~~~~~~ + + The basic functionality. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import re +import flask +import unittest +from datetime import datetime +from threading import Thread +from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning +from werkzeug.exceptions import BadRequest, NotFound +from werkzeug.http import parse_date + + +class BasicFunctionalityTestCase(FlaskTestCase): + + def test_options_work(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + rv = app.test_client().open('/', method='OPTIONS') + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) + self.assert_equal(rv.data, '') + + def test_options_on_multiple_rules(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + @app.route('/', methods=['PUT']) + def index_put(): + return 'Aha!' + rv = app.test_client().open('/', method='OPTIONS') + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']) + + def test_options_handling_disabled(self): + app = flask.Flask(__name__) + def index(): + return 'Hello World!' + index.provide_automatic_options = False + app.route('/')(index) + rv = app.test_client().open('/', method='OPTIONS') + self.assert_equal(rv.status_code, 405) + + app = flask.Flask(__name__) + def index2(): + return 'Hello World!' + index2.provide_automatic_options = True + app.route('/', methods=['OPTIONS'])(index2) + rv = app.test_client().open('/', method='OPTIONS') + self.assert_equal(sorted(rv.allow), ['OPTIONS']) + + def test_request_dispatching(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.request.method + @app.route('/more', methods=['GET', 'POST']) + def more(): + return flask.request.method + + c = app.test_client() + self.assert_equal(c.get('/').data, 'GET') + rv = c.post('/') + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) + rv = c.head('/') + self.assert_equal(rv.status_code, 200) + self.assert_(not rv.data) # head truncates + self.assert_equal(c.post('/more').data, 'POST') + self.assert_equal(c.get('/more').data, 'GET') + rv = c.delete('/more') + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_url_mapping(self): + app = flask.Flask(__name__) + def index(): + return flask.request.method + def more(): + return flask.request.method + + app.add_url_rule('/', 'index', index) + app.add_url_rule('/more', 'more', more, methods=['GET', 'POST']) + + c = app.test_client() + self.assert_equal(c.get('/').data, 'GET') + rv = c.post('/') + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) + rv = c.head('/') + self.assert_equal(rv.status_code, 200) + self.assert_(not rv.data) # head truncates + self.assert_equal(c.post('/more').data, 'POST') + self.assert_equal(c.get('/more').data, 'GET') + rv = c.delete('/more') + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_werkzeug_routing(self): + from werkzeug.routing import Submount, Rule + app = flask.Flask(__name__) + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + def bar(): + return 'bar' + def index(): + return 'index' + app.view_functions['bar'] = bar + app.view_functions['index'] = index + + c = app.test_client() + self.assert_equal(c.get('/foo/').data, 'index') + self.assert_equal(c.get('/foo/bar').data, 'bar') + + def test_endpoint_decorator(self): + from werkzeug.routing import Submount, Rule + app = flask.Flask(__name__) + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + + @app.endpoint('bar') + def bar(): + return 'bar' + + @app.endpoint('index') + def index(): + return 'index' + + c = app.test_client() + self.assert_equal(c.get('/foo/').data, 'index') + self.assert_equal(c.get('/foo/bar').data, 'bar') + + def test_session(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + @app.route('/set', methods=['POST']) + def set(): + flask.session['value'] = flask.request.form['value'] + return 'value set' + @app.route('/get') + def get(): + return flask.session['value'] + + c = app.test_client() + self.assert_equal(c.post('/set', data={'value': '42'}).data, 'value set') + self.assert_equal(c.get('/get').data, '42') + + def test_session_using_server_name(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='example.com' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com/') + self.assert_('domain=.example.com' in rv.headers['set-cookie'].lower()) + self.assert_('httponly' in rv.headers['set-cookie'].lower()) + + def test_session_using_server_name_and_port(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='example.com:8080' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com:8080/') + self.assert_('domain=.example.com' in rv.headers['set-cookie'].lower()) + self.assert_('httponly' in rv.headers['set-cookie'].lower()) + + def test_session_using_application_root(self): + class PrefixPathMiddleware(object): + def __init__(self, app, prefix): + self.app = app + self.prefix = prefix + def __call__(self, environ, start_response): + environ['SCRIPT_NAME'] = self.prefix + return self.app(environ, start_response) + + app = flask.Flask(__name__) + app.wsgi_app = PrefixPathMiddleware(app.wsgi_app, '/bar') + app.config.update( + SECRET_KEY='foo', + APPLICATION_ROOT='/bar' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com:8080/') + self.assert_('path=/bar' in rv.headers['set-cookie'].lower()) + + def test_session_using_session_settings(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='www.example.com:8080', + APPLICATION_ROOT='/test', + SESSION_COOKIE_DOMAIN='.example.com', + SESSION_COOKIE_HTTPONLY=False, + SESSION_COOKIE_SECURE=True, + SESSION_COOKIE_PATH='/' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://www.example.com:8080/test/') + cookie = rv.headers['set-cookie'].lower() + self.assert_('domain=.example.com' in cookie) + self.assert_('path=/;' in cookie) + self.assert_('secure' in cookie) + self.assert_('httponly' not in cookie) + + def test_missing_session(self): + app = flask.Flask(__name__) + def expect_exception(f, *args, **kwargs): + try: + f(*args, **kwargs) + except RuntimeError, e: + self.assert_(e.args and 'session is unavailable' in e.args[0]) + else: + self.assert_(False, 'expected exception') + with app.test_request_context(): + self.assert_(flask.session.get('missing_key') is None) + expect_exception(flask.session.__setitem__, 'foo', 42) + expect_exception(flask.session.pop, 'foo') + + def test_session_expiration(self): + permanent = True + app = flask.Flask(__name__) + app.secret_key = 'testkey' + @app.route('/') + def index(): + flask.session['test'] = 42 + flask.session.permanent = permanent + return '' + + @app.route('/test') + def test(): + return unicode(flask.session.permanent) + + client = app.test_client() + rv = client.get('/') + self.assert_('set-cookie' in rv.headers) + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + expires = parse_date(match.group()) + expected = datetime.utcnow() + app.permanent_session_lifetime + self.assert_equal(expires.year, expected.year) + self.assert_equal(expires.month, expected.month) + self.assert_equal(expires.day, expected.day) + + rv = client.get('/test') + self.assert_equal(rv.data, 'True') + + permanent = False + rv = app.test_client().get('/') + self.assert_('set-cookie' in rv.headers) + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + self.assert_(match is None) + + def test_flashes(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + + with app.test_request_context(): + self.assert_(not flask.session.modified) + flask.flash('Zap') + flask.session.modified = False + flask.flash('Zip') + self.assert_(flask.session.modified) + self.assert_equal(list(flask.get_flashed_messages()), ['Zap', 'Zip']) + + def test_extended_flashing(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + + @app.route('/') + def index(): + flask.flash(u'Hello World') + flask.flash(u'Hello World', 'error') + flask.flash(flask.Markup(u'Testing'), 'warning') + return '' + + @app.route('/test') + def test(): + messages = flask.get_flashed_messages(with_categories=True) + self.assert_equal(len(messages), 3) + self.assert_equal(messages[0], ('message', u'Hello World')) + self.assert_equal(messages[1], ('error', u'Hello World')) + self.assert_equal(messages[2], ('warning', flask.Markup(u'Testing'))) + return '' + messages = flask.get_flashed_messages() + self.assert_equal(len(messages), 3) + self.assert_equal(messages[0], u'Hello World') + self.assert_equal(messages[1], u'Hello World') + self.assert_equal(messages[2], flask.Markup(u'Testing')) + + c = app.test_client() + c.get('/') + c.get('/test') + + def test_request_processing(self): + app = flask.Flask(__name__) + evts = [] + @app.before_request + def before_request(): + evts.append('before') + @app.after_request + def after_request(response): + response.data += '|after' + evts.append('after') + return response + @app.route('/') + def index(): + self.assert_('before' in evts) + self.assert_('after' not in evts) + return 'request' + self.assert_('after' not in evts) + rv = app.test_client().get('/').data + self.assert_('after' in evts) + self.assert_equal(rv, 'request|after') + + def test_teardown_request_handler(self): + called = [] + app = flask.Flask(__name__) + @app.teardown_request + def teardown_request(exc): + called.append(True) + return "Ignored" + @app.route('/') + def root(): + return "Response" + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 200) + self.assert_('Response' in rv.data) + self.assert_equal(len(called), 1) + + def test_teardown_request_handler_debug_mode(self): + called = [] + app = flask.Flask(__name__) + app.testing = True + @app.teardown_request + def teardown_request(exc): + called.append(True) + return "Ignored" + @app.route('/') + def root(): + return "Response" + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 200) + self.assert_('Response' in rv.data) + self.assert_equal(len(called), 1) + + def test_teardown_request_handler_error(self): + called = [] + app = flask.Flask(__name__) + @app.teardown_request + def teardown_request1(exc): + self.assert_equal(type(exc), ZeroDivisionError) + called.append(True) + # This raises a new error and blows away sys.exc_info(), so we can + # test that all teardown_requests get passed the same original + # exception. + try: + raise TypeError + except: + pass + @app.teardown_request + def teardown_request2(exc): + self.assert_equal(type(exc), ZeroDivisionError) + called.append(True) + # This raises a new error and blows away sys.exc_info(), so we can + # test that all teardown_requests get passed the same original + # exception. + try: + raise TypeError + except: + pass + @app.route('/') + def fails(): + 1/0 + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 500) + self.assert_('Internal Server Error' in rv.data) + self.assert_equal(len(called), 2) + + def test_before_after_request_order(self): + called = [] + app = flask.Flask(__name__) + @app.before_request + def before1(): + called.append(1) + @app.before_request + def before2(): + called.append(2) + @app.after_request + def after1(response): + called.append(4) + return response + @app.after_request + def after2(response): + called.append(3) + return response + @app.teardown_request + def finish1(exc): + called.append(6) + @app.teardown_request + def finish2(exc): + called.append(5) + @app.route('/') + def index(): + return '42' + rv = app.test_client().get('/') + self.assert_equal(rv.data, '42') + self.assert_equal(called, [1, 2, 3, 4, 5, 6]) + + def test_error_handling(self): + app = flask.Flask(__name__) + @app.errorhandler(404) + def not_found(e): + return 'not found', 404 + @app.errorhandler(500) + def internal_server_error(e): + return 'internal server error', 500 + @app.route('/') + def index(): + flask.abort(404) + @app.route('/error') + def error(): + 1 // 0 + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'not found') + rv = c.get('/error') + self.assert_equal(rv.status_code, 500) + self.assert_equal('internal server error', rv.data) + + def test_before_request_and_routing_errors(self): + app = flask.Flask(__name__) + @app.before_request + def attach_something(): + flask.g.something = 'value' + @app.errorhandler(404) + def return_something(error): + return flask.g.something, 404 + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'value') + + def test_user_error_handling(self): + class MyException(Exception): + pass + + app = flask.Flask(__name__) + @app.errorhandler(MyException) + def handle_my_exception(e): + self.assert_(isinstance(e, MyException)) + return '42' + @app.route('/') + def index(): + raise MyException() + + c = app.test_client() + self.assert_equal(c.get('/').data, '42') + + def test_trapping_of_bad_request_key_errors(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/fail') + def fail(): + flask.request.form['missing_key'] + c = app.test_client() + self.assert_equal(c.get('/fail').status_code, 400) + + app.config['TRAP_BAD_REQUEST_ERRORS'] = True + c = app.test_client() + try: + c.get('/fail') + except KeyError, e: + self.assert_(isinstance(e, BadRequest)) + else: + self.fail('Expected exception') + + def test_trapping_of_all_http_exceptions(self): + app = flask.Flask(__name__) + app.testing = True + app.config['TRAP_HTTP_EXCEPTIONS'] = True + @app.route('/fail') + def fail(): + flask.abort(404) + + c = app.test_client() + try: + c.get('/fail') + except NotFound, e: + pass + else: + self.fail('Expected exception') + + def test_enctype_debug_helper(self): + from flask.debughelpers import DebugFilesKeyError + app = flask.Flask(__name__) + app.debug = True + @app.route('/fail', methods=['POST']) + def index(): + return flask.request.files['foo'].filename + + # with statement is important because we leave an exception on the + # stack otherwise and we want to ensure that this is not the case + # to not negatively affect other tests. + with app.test_client() as c: + try: + c.post('/fail', data={'foo': 'index.txt'}) + except DebugFilesKeyError, e: + self.assert_('no file contents were transmitted' in str(e)) + self.assert_('This was submitted: "index.txt"' in str(e)) + else: + self.fail('Expected exception') + + def test_teardown_on_pop(self): + buffer = [] + app = flask.Flask(__name__) + @app.teardown_request + def end_of_request(exception): + buffer.append(exception) + + ctx = app.test_request_context() + ctx.push() + self.assert_equal(buffer, []) + ctx.pop() + self.assert_equal(buffer, [None]) + + def test_response_creation(self): + app = flask.Flask(__name__) + @app.route('/unicode') + def from_unicode(): + return u'Hällo Wörld' + @app.route('/string') + def from_string(): + return u'Hällo Wörld'.encode('utf-8') + @app.route('/args') + def from_tuple(): + return 'Meh', 400, {'X-Foo': 'Testing'}, 'text/plain' + c = app.test_client() + self.assert_equal(c.get('/unicode').data, u'Hällo Wörld'.encode('utf-8')) + self.assert_equal(c.get('/string').data, u'Hällo Wörld'.encode('utf-8')) + rv = c.get('/args') + self.assert_equal(rv.data, 'Meh') + self.assert_equal(rv.headers['X-Foo'], 'Testing') + self.assert_equal(rv.status_code, 400) + self.assert_equal(rv.mimetype, 'text/plain') + + def test_make_response(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.make_response() + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data, '') + self.assert_equal(rv.mimetype, 'text/html') + + rv = flask.make_response('Awesome') + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data, 'Awesome') + self.assert_equal(rv.mimetype, 'text/html') + + rv = flask.make_response('W00t', 404) + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'W00t') + self.assert_equal(rv.mimetype, 'text/html') + + def test_url_generation(self): + app = flask.Flask(__name__) + @app.route('/hello/', methods=['POST']) + def hello(): + pass + with app.test_request_context(): + self.assert_equal(flask.url_for('hello', name='test x'), '/hello/test%20x') + self.assert_equal(flask.url_for('hello', name='test x', _external=True), + 'http://localhost/hello/test%20x') + + def test_custom_converters(self): + from werkzeug.routing import BaseConverter + class ListConverter(BaseConverter): + def to_python(self, value): + return value.split(',') + def to_url(self, value): + base_to_url = super(ListConverter, self).to_url + return ','.join(base_to_url(x) for x in value) + app = flask.Flask(__name__) + app.url_map.converters['list'] = ListConverter + @app.route('/') + def index(args): + return '|'.join(args) + c = app.test_client() + self.assert_equal(c.get('/1,2,3').data, '1|2|3') + + def test_static_files(self): + app = flask.Flask(__name__) + rv = app.test_client().get('/static/index.html') + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data.strip(), '

Hello World!

') + with app.test_request_context(): + self.assert_equal(flask.url_for('static', filename='index.html'), + '/static/index.html') + + def test_none_response(self): + app = flask.Flask(__name__) + @app.route('/') + def test(): + return None + try: + app.test_client().get('/') + except ValueError, e: + self.assert_equal(str(e), 'View function did not return a response') + pass + else: + self.assert_("Expected ValueError") + + def test_request_locals(self): + self.assert_equal(repr(flask.g), '') + self.assertFalse(flask.g) + + def test_proper_test_request_context(self): + app = flask.Flask(__name__) + app.config.update( + SERVER_NAME='localhost.localdomain:5000' + ) + + @app.route('/') + def index(): + return None + + @app.route('/', subdomain='foo') + def sub(): + return None + + with app.test_request_context('/'): + self.assert_equal(flask.url_for('index', _external=True), 'http://localhost.localdomain:5000/') + + with app.test_request_context('/'): + self.assert_equal(flask.url_for('sub', _external=True), 'http://foo.localhost.localdomain:5000/') + + try: + with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): + pass + except Exception, e: + self.assert_(isinstance(e, ValueError)) + self.assert_equal(str(e), "the server name provided " + + "('localhost.localdomain:5000') does not match the " + \ + "server name from the WSGI environment ('localhost')") + + try: + app.config.update(SERVER_NAME='localhost') + with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost'}): + pass + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + app.config.update(SERVER_NAME='localhost:80') + with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost:80'}): + pass + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + def test_test_app_proper_environ(self): + app = flask.Flask(__name__) + app.config.update( + SERVER_NAME='localhost.localdomain:5000' + ) + @app.route('/') + def index(): + return 'Foo' + + @app.route('/', subdomain='foo') + def subdomain(): + return 'Foo SubDomain' + + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'Foo') + + rv = app.test_client().get('/', 'http://localhost.localdomain:5000') + self.assert_equal(rv.data, 'Foo') + + rv = app.test_client().get('/', 'https://localhost.localdomain:5000') + self.assert_equal(rv.data, 'Foo') + + app.config.update(SERVER_NAME='localhost.localdomain') + rv = app.test_client().get('/', 'https://localhost.localdomain') + self.assert_equal(rv.data, 'Foo') + + try: + app.config.update(SERVER_NAME='localhost.localdomain:443') + rv = app.test_client().get('/', 'https://localhost.localdomain') + # Werkzeug 0.8 + self.assert_equal(rv.status_code, 404) + except ValueError, e: + # Werkzeug 0.7 + self.assert_equal(str(e), "the server name provided " + + "('localhost.localdomain:443') does not match the " + \ + "server name from the WSGI environment ('localhost.localdomain')") + + try: + app.config.update(SERVER_NAME='localhost.localdomain') + rv = app.test_client().get('/', 'http://foo.localhost') + # Werkzeug 0.8 + self.assert_equal(rv.status_code, 404) + except ValueError, e: + # Werkzeug 0.7 + self.assert_equal(str(e), "the server name provided " + \ + "('localhost.localdomain') does not match the " + \ + "server name from the WSGI environment ('foo.localhost')") + + rv = app.test_client().get('/', 'http://foo.localhost.localdomain') + self.assert_equal(rv.data, 'Foo SubDomain') + + def test_exception_propagation(self): + def apprunner(configkey): + app = flask.Flask(__name__) + @app.route('/') + def index(): + 1/0 + c = app.test_client() + if config_key is not None: + app.config[config_key] = True + try: + resp = c.get('/') + except Exception: + pass + else: + self.fail('expected exception') + else: + self.assert_equal(c.get('/').status_code, 500) + + # we have to run this test in an isolated thread because if the + # debug flag is set to true and an exception happens the context is + # not torn down. This causes other tests that run after this fail + # when they expect no exception on the stack. + for config_key in 'TESTING', 'PROPAGATE_EXCEPTIONS', 'DEBUG', None: + t = Thread(target=apprunner, args=(config_key,)) + t.start() + t.join() + + def test_max_content_length(self): + app = flask.Flask(__name__) + app.config['MAX_CONTENT_LENGTH'] = 64 + @app.before_request + def always_first(): + flask.request.form['myfile'] + self.assert_(False) + @app.route('/accept', methods=['POST']) + def accept_file(): + flask.request.form['myfile'] + self.assert_(False) + @app.errorhandler(413) + def catcher(error): + return '42' + + c = app.test_client() + rv = c.post('/accept', data={'myfile': 'foo' * 100}) + self.assert_equal(rv.data, '42') + + def test_url_processors(self): + app = flask.Flask(__name__) + + @app.url_defaults + def add_language_code(endpoint, values): + if flask.g.lang_code is not None and \ + app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): + values.setdefault('lang_code', flask.g.lang_code) + + @app.url_value_preprocessor + def pull_lang_code(endpoint, values): + flask.g.lang_code = values.pop('lang_code', None) + + @app.route('//') + def index(): + return flask.url_for('about') + + @app.route('//about') + def about(): + return flask.url_for('something_else') + + @app.route('/foo') + def something_else(): + return flask.url_for('about', lang_code='en') + + c = app.test_client() + + self.assert_equal(c.get('/de/').data, '/de/about') + self.assert_equal(c.get('/de/about').data, '/foo') + self.assert_equal(c.get('/foo').data, '/en/about') + + def test_debug_mode_complains_after_first_request(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/') + def index(): + return 'Awesome' + self.assert_(not app.got_first_request) + self.assert_equal(app.test_client().get('/').data, 'Awesome') + try: + @app.route('/foo') + def broken(): + return 'Meh' + except AssertionError, e: + self.assert_('A setup function was called' in str(e)) + else: + self.fail('Expected exception') + + app.debug = False + @app.route('/foo') + def working(): + return 'Meh' + self.assert_equal(app.test_client().get('/foo').data, 'Meh') + self.assert_(app.got_first_request) + + def test_before_first_request_functions(self): + got = [] + app = flask.Flask(__name__) + @app.before_first_request + def foo(): + got.append(42) + c = app.test_client() + c.get('/') + self.assert_equal(got, [42]) + c.get('/') + self.assert_equal(got, [42]) + self.assert_(app.got_first_request) + + def test_routing_redirect_debugging(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/foo/', methods=['GET', 'POST']) + def foo(): + return 'success' + with app.test_client() as c: + try: + c.post('/foo', data={}) + except AssertionError, e: + self.assert_('http://localhost/foo/' in str(e)) + self.assert_('Make sure to directly send your POST-request ' + 'to this URL' in str(e)) + else: + self.fail('Expected exception') + + rv = c.get('/foo', data={}, follow_redirects=True) + self.assert_equal(rv.data, 'success') + + app.debug = False + with app.test_client() as c: + rv = c.post('/foo', data={}, follow_redirects=True) + self.assert_equal(rv.data, 'success') + + def test_route_decorator_custom_endpoint(self): + app = flask.Flask(__name__) + app.debug = True + + @app.route('/foo/') + def foo(): + return flask.request.endpoint + + @app.route('/bar/', endpoint='bar') + def for_bar(): + return flask.request.endpoint + + @app.route('/bar/123', endpoint='123') + def for_bar_foo(): + return flask.request.endpoint + + with app.test_request_context(): + assert flask.url_for('foo') == '/foo/' + assert flask.url_for('bar') == '/bar/' + assert flask.url_for('123') == '/bar/123' + + c = app.test_client() + self.assertEqual(c.get('/foo/').data, 'foo') + self.assertEqual(c.get('/bar/').data, 'bar') + self.assertEqual(c.get('/bar/123').data, '123') + + def test_preserve_only_once(self): + app = flask.Flask(__name__) + app.debug = True + + @app.route('/fail') + def fail_func(): + 1/0 + + c = app.test_client() + for x in xrange(3): + with self.assert_raises(ZeroDivisionError): + c.get('/fail') + + self.assert_(flask._request_ctx_stack.top is not None) + flask._request_ctx_stack.pop() + self.assert_(flask._request_ctx_stack.top is None) + + +class ContextTestCase(FlaskTestCase): + + def test_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return 'Hello %s!' % flask.request.args['name'] + @app.route('/meh') + def meh(): + return flask.request.url + + with app.test_request_context('/?name=World'): + self.assert_equal(index(), 'Hello World!') + with app.test_request_context('/meh'): + self.assert_equal(meh(), 'http://localhost/meh') + self.assert_(flask._request_ctx_stack.top is None) + + def test_context_test(self): + app = flask.Flask(__name__) + self.assert_(not flask.request) + self.assert_(not flask.has_request_context()) + ctx = app.test_request_context() + ctx.push() + try: + self.assert_(flask.request) + self.assert_(flask.has_request_context()) + finally: + ctx.pop() + + def test_manual_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return 'Hello %s!' % flask.request.args['name'] + + ctx = app.test_request_context('/?name=World') + ctx.push() + self.assert_equal(index(), 'Hello World!') + ctx.pop() + try: + index() + except RuntimeError: + pass + else: + self.assert_(0, 'expected runtime error') + + +class SubdomainTestCase(FlaskTestCase): + + def test_basic_support(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost' + @app.route('/') + def normal_index(): + return 'normal index' + @app.route('/', subdomain='test') + def test_index(): + return 'test index' + + c = app.test_client() + rv = c.get('/', 'http://localhost/') + self.assert_equal(rv.data, 'normal index') + + rv = c.get('/', 'http://test.localhost/') + self.assert_equal(rv.data, 'test index') + + @emits_module_deprecation_warning + def test_module_static_path_subdomain(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'example.com' + from subdomaintestmodule import mod + app.register_module(mod) + c = app.test_client() + rv = c.get('/static/hello.txt', 'http://foo.example.com/') + self.assert_equal(rv.data.strip(), 'Hello Subdomain') + + def test_subdomain_matching(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost' + @app.route('/', subdomain='') + def index(user): + return 'index for %s' % user + + c = app.test_client() + rv = c.get('/', 'http://mitsuhiko.localhost/') + self.assert_equal(rv.data, 'index for mitsuhiko') + + def test_subdomain_matching_with_ports(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost:3000' + @app.route('/', subdomain='') + def index(user): + return 'index for %s' % user + + c = app.test_client() + rv = c.get('/', 'http://mitsuhiko.localhost:3000/') + self.assert_equal(rv.data, 'index for mitsuhiko') + + @emits_module_deprecation_warning + def test_module_subdomain_support(self): + app = flask.Flask(__name__) + mod = flask.Module(__name__, 'test', subdomain='testing') + app.config['SERVER_NAME'] = 'localhost' + + @mod.route('/test') + def test(): + return 'Test' + + @mod.route('/outside', subdomain='xtesting') + def bar(): + return 'Outside' + + app.register_module(mod) + + c = app.test_client() + rv = c.get('/test', 'http://testing.localhost/') + self.assert_equal(rv.data, 'Test') + rv = c.get('/outside', 'http://xtesting.localhost/') + self.assert_equal(rv.data, 'Outside') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase)) + suite.addTest(unittest.makeSuite(ContextTestCase)) + suite.addTest(unittest.makeSuite(SubdomainTestCase)) + return suite diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py new file mode 100644 index 00000000..3f65dd48 --- /dev/null +++ b/flask/testsuite/blueprints.py @@ -0,0 +1,512 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.blueprints + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Blueprints (and currently modules) + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import flask +import unittest +import warnings +from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning +from werkzeug.exceptions import NotFound +from jinja2 import TemplateNotFound + + +# import moduleapp here because it uses deprecated features and we don't +# want to see the warnings +warnings.simplefilter('ignore', DeprecationWarning) +from moduleapp import app as moduleapp +warnings.simplefilter('default', DeprecationWarning) + + +class ModuleTestCase(FlaskTestCase): + + @emits_module_deprecation_warning + def test_basic_module(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin', url_prefix='/admin') + @admin.route('/') + def admin_index(): + return 'admin index' + @admin.route('/login') + def admin_login(): + return 'admin login' + @admin.route('/logout') + def admin_logout(): + return 'admin logout' + @app.route('/') + def index(): + return 'the index' + app.register_module(admin) + c = app.test_client() + self.assert_equal(c.get('/').data, 'the index') + self.assert_equal(c.get('/admin/').data, 'admin index') + self.assert_equal(c.get('/admin/login').data, 'admin login') + self.assert_equal(c.get('/admin/logout').data, 'admin logout') + + @emits_module_deprecation_warning + def test_default_endpoint_name(self): + app = flask.Flask(__name__) + mod = flask.Module(__name__, 'frontend') + def index(): + return 'Awesome' + mod.add_url_rule('/', view_func=index) + app.register_module(mod) + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'Awesome') + with app.test_request_context(): + self.assert_equal(flask.url_for('frontend.index'), '/') + + @emits_module_deprecation_warning + def test_request_processing(self): + catched = [] + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin', url_prefix='/admin') + @admin.before_request + def before_admin_request(): + catched.append('before-admin') + @admin.after_request + def after_admin_request(response): + catched.append('after-admin') + return response + @admin.route('/') + def admin_index(): + return 'the admin' + @app.before_request + def before_request(): + catched.append('before-app') + @app.after_request + def after_request(response): + catched.append('after-app') + return response + @app.route('/') + def index(): + return 'the index' + app.register_module(admin) + c = app.test_client() + + self.assert_equal(c.get('/').data, 'the index') + self.assert_equal(catched, ['before-app', 'after-app']) + del catched[:] + + self.assert_equal(c.get('/admin/').data, 'the admin') + self.assert_equal(catched, ['before-app', 'before-admin', + 'after-admin', 'after-app']) + + @emits_module_deprecation_warning + def test_context_processors(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin', url_prefix='/admin') + @app.context_processor + def inject_all_regualr(): + return {'a': 1} + @admin.context_processor + def inject_admin(): + return {'b': 2} + @admin.app_context_processor + def inject_all_module(): + return {'c': 3} + @app.route('/') + def index(): + return flask.render_template_string('{{ a }}{{ b }}{{ c }}') + @admin.route('/') + def admin_index(): + return flask.render_template_string('{{ a }}{{ b }}{{ c }}') + app.register_module(admin) + c = app.test_client() + self.assert_equal(c.get('/').data, '13') + self.assert_equal(c.get('/admin/').data, '123') + + @emits_module_deprecation_warning + def test_late_binding(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin') + @admin.route('/') + def index(): + return '42' + app.register_module(admin, url_prefix='/admin') + self.assert_equal(app.test_client().get('/admin/').data, '42') + + @emits_module_deprecation_warning + def test_error_handling(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin') + @admin.app_errorhandler(404) + def not_found(e): + return 'not found', 404 + @admin.app_errorhandler(500) + def internal_server_error(e): + return 'internal server error', 500 + @admin.route('/') + def index(): + flask.abort(404) + @admin.route('/error') + def error(): + 1 // 0 + app.register_module(admin) + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'not found') + rv = c.get('/error') + self.assert_equal(rv.status_code, 500) + self.assert_equal('internal server error', rv.data) + + def test_templates_and_static(self): + app = moduleapp + app.testing = True + c = app.test_client() + + rv = c.get('/') + self.assert_equal(rv.data, 'Hello from the Frontend') + rv = c.get('/admin/') + self.assert_equal(rv.data, 'Hello from the Admin') + rv = c.get('/admin/index2') + self.assert_equal(rv.data, 'Hello from the Admin') + rv = c.get('/admin/static/test.txt') + self.assert_equal(rv.data.strip(), 'Admin File') + rv = c.get('/admin/static/css/test.css') + self.assert_equal(rv.data.strip(), '/* nested file */') + + with app.test_request_context(): + self.assert_equal(flask.url_for('admin.static', filename='test.txt'), + '/admin/static/test.txt') + + with app.test_request_context(): + try: + flask.render_template('missing.html') + except TemplateNotFound, e: + self.assert_equal(e.name, 'missing.html') + else: + self.assert_(0, 'expected exception') + + with flask.Flask(__name__).test_request_context(): + self.assert_equal(flask.render_template('nested/nested.txt'), 'I\'m nested') + + def test_safe_access(self): + app = moduleapp + + with app.test_request_context(): + f = app.view_functions['admin.static'] + + try: + f('/etc/passwd') + except NotFound: + pass + else: + self.assert_(0, 'expected exception') + try: + f('../__init__.py') + except NotFound: + pass + else: + self.assert_(0, 'expected exception') + + # testcase for a security issue that may exist on windows systems + import os + import ntpath + old_path = os.path + os.path = ntpath + try: + try: + f('..\\__init__.py') + except NotFound: + pass + else: + self.assert_(0, 'expected exception') + finally: + os.path = old_path + + @emits_module_deprecation_warning + def test_endpoint_decorator(self): + from werkzeug.routing import Submount, Rule + from flask import Module + + app = flask.Flask(__name__) + app.testing = True + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + module = Module(__name__, __name__) + + @module.endpoint('bar') + def bar(): + return 'bar' + + @module.endpoint('index') + def index(): + return 'index' + + app.register_module(module) + + c = app.test_client() + self.assert_equal(c.get('/foo/').data, 'index') + self.assert_equal(c.get('/foo/bar').data, 'bar') + + +class BlueprintTestCase(FlaskTestCase): + + def test_blueprint_specific_error_handling(self): + frontend = flask.Blueprint('frontend', __name__) + backend = flask.Blueprint('backend', __name__) + sideend = flask.Blueprint('sideend', __name__) + + @frontend.errorhandler(403) + def frontend_forbidden(e): + return 'frontend says no', 403 + + @frontend.route('/frontend-no') + def frontend_no(): + flask.abort(403) + + @backend.errorhandler(403) + def backend_forbidden(e): + return 'backend says no', 403 + + @backend.route('/backend-no') + def backend_no(): + flask.abort(403) + + @sideend.route('/what-is-a-sideend') + def sideend_no(): + flask.abort(403) + + app = flask.Flask(__name__) + app.register_blueprint(frontend) + app.register_blueprint(backend) + app.register_blueprint(sideend) + + @app.errorhandler(403) + def app_forbidden(e): + return 'application itself says no', 403 + + c = app.test_client() + + self.assert_equal(c.get('/frontend-no').data, 'frontend says no') + self.assert_equal(c.get('/backend-no').data, 'backend says no') + self.assert_equal(c.get('/what-is-a-sideend').data, 'application itself says no') + + def test_blueprint_url_definitions(self): + bp = flask.Blueprint('test', __name__) + + @bp.route('/foo', defaults={'baz': 42}) + def foo(bar, baz): + return '%s/%d' % (bar, baz) + + @bp.route('/bar') + def bar(bar): + return unicode(bar) + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/1', url_defaults={'bar': 23}) + app.register_blueprint(bp, url_prefix='/2', url_defaults={'bar': 19}) + + c = app.test_client() + self.assert_equal(c.get('/1/foo').data, u'23/42') + self.assert_equal(c.get('/2/foo').data, u'19/42') + self.assert_equal(c.get('/1/bar').data, u'23') + self.assert_equal(c.get('/2/bar').data, u'19') + + def test_blueprint_url_processors(self): + bp = flask.Blueprint('frontend', __name__, url_prefix='/') + + @bp.url_defaults + def add_language_code(endpoint, values): + values.setdefault('lang_code', flask.g.lang_code) + + @bp.url_value_preprocessor + def pull_lang_code(endpoint, values): + flask.g.lang_code = values.pop('lang_code') + + @bp.route('/') + def index(): + return flask.url_for('.about') + + @bp.route('/about') + def about(): + return flask.url_for('.index') + + app = flask.Flask(__name__) + app.register_blueprint(bp) + + c = app.test_client() + + self.assert_equal(c.get('/de/').data, '/de/about') + self.assert_equal(c.get('/de/about').data, '/de/') + + def test_templates_and_static(self): + from blueprintapp import app + c = app.test_client() + + rv = c.get('/') + self.assert_equal(rv.data, 'Hello from the Frontend') + rv = c.get('/admin/') + self.assert_equal(rv.data, 'Hello from the Admin') + rv = c.get('/admin/index2') + self.assert_equal(rv.data, 'Hello from the Admin') + rv = c.get('/admin/static/test.txt') + self.assert_equal(rv.data.strip(), 'Admin File') + rv = c.get('/admin/static/css/test.css') + self.assert_equal(rv.data.strip(), '/* nested file */') + + with app.test_request_context(): + self.assert_equal(flask.url_for('admin.static', filename='test.txt'), + '/admin/static/test.txt') + + with app.test_request_context(): + try: + flask.render_template('missing.html') + except TemplateNotFound, e: + self.assert_equal(e.name, 'missing.html') + else: + self.assert_(0, 'expected exception') + + with flask.Flask(__name__).test_request_context(): + self.assert_equal(flask.render_template('nested/nested.txt'), 'I\'m nested') + + def test_templates_list(self): + from blueprintapp import app + templates = sorted(app.jinja_env.list_templates()) + self.assert_equal(templates, ['admin/index.html', + 'frontend/index.html']) + + def test_dotted_names(self): + frontend = flask.Blueprint('myapp.frontend', __name__) + backend = flask.Blueprint('myapp.backend', __name__) + + @frontend.route('/fe') + def frontend_index(): + return flask.url_for('myapp.backend.backend_index') + + @frontend.route('/fe2') + def frontend_page2(): + return flask.url_for('.frontend_index') + + @backend.route('/be') + def backend_index(): + return flask.url_for('myapp.frontend.frontend_index') + + app = flask.Flask(__name__) + app.register_blueprint(frontend) + app.register_blueprint(backend) + + c = app.test_client() + self.assert_equal(c.get('/fe').data.strip(), '/be') + self.assert_equal(c.get('/fe2').data.strip(), '/fe') + self.assert_equal(c.get('/be').data.strip(), '/fe') + + def test_empty_url_defaults(self): + bp = flask.Blueprint('bp', __name__) + + @bp.route('/', defaults={'page': 1}) + @bp.route('/page/') + def something(page): + return str(page) + + app = flask.Flask(__name__) + app.register_blueprint(bp) + + c = app.test_client() + self.assert_equal(c.get('/').data, '1') + self.assert_equal(c.get('/page/2').data, '2') + + def test_route_decorator_custom_endpoint(self): + + bp = flask.Blueprint('bp', __name__) + + @bp.route('/foo') + def foo(): + return flask.request.endpoint + + @bp.route('/bar', endpoint='bar') + def foo_bar(): + return flask.request.endpoint + + @bp.route('/bar/123', endpoint='123') + def foo_bar_foo(): + return flask.request.endpoint + + @bp.route('/bar/foo') + def bar_foo(): + return flask.request.endpoint + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + + @app.route('/') + def index(): + return flask.request.endpoint + + c = app.test_client() + self.assertEqual(c.get('/').data, 'index') + self.assertEqual(c.get('/py/foo').data, 'bp.foo') + self.assertEqual(c.get('/py/bar').data, 'bp.bar') + self.assertEqual(c.get('/py/bar/123').data, 'bp.123') + self.assertEqual(c.get('/py/bar/foo').data, 'bp.bar_foo') + + def test_route_decorator_custom_endpoint_with_dots(self): + bp = flask.Blueprint('bp', __name__) + + @bp.route('/foo') + def foo(): + return flask.request.endpoint + + try: + @bp.route('/bar', endpoint='bar.bar') + def foo_bar(): + return flask.request.endpoint + except AssertionError: + pass + else: + raise AssertionError('expected AssertionError not raised') + + try: + @bp.route('/bar/123', endpoint='bar.123') + def foo_bar_foo(): + return flask.request.endpoint + except AssertionError: + pass + else: + raise AssertionError('expected AssertionError not raised') + + def foo_foo_foo(): + pass + + self.assertRaises( + AssertionError, + lambda: bp.add_url_rule( + '/bar/123', endpoint='bar.123', view_func=foo_foo_foo + ) + ) + + self.assertRaises( + AssertionError, + bp.route('/bar/123', endpoint='bar.123'), + lambda: None + ) + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + + c = app.test_client() + self.assertEqual(c.get('/py/foo').data, 'bp.foo') + # The rule's din't actually made it through + rv = c.get('/py/bar') + assert rv.status_code == 404 + rv = c.get('/py/bar/123') + assert rv.status_code == 404 + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BlueprintTestCase)) + suite.addTest(unittest.makeSuite(ModuleTestCase)) + return suite diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py new file mode 100644 index 00000000..ad1721fd --- /dev/null +++ b/flask/testsuite/config.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.config + ~~~~~~~~~~~~~~~~~~~~~~ + + Configuration and instances. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import sys +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +# config keys used for the ConfigTestCase +TEST_KEY = 'foo' +SECRET_KEY = 'devkey' + + +class ConfigTestCase(FlaskTestCase): + + def common_object_test(self, app): + self.assert_equal(app.secret_key, 'devkey') + self.assert_equal(app.config['TEST_KEY'], 'foo') + self.assert_('ConfigTestCase' not in app.config) + + def test_config_from_file(self): + app = flask.Flask(__name__) + app.config.from_pyfile(__file__.rsplit('.', 1)[0] + '.py') + self.common_object_test(app) + + def test_config_from_object(self): + app = flask.Flask(__name__) + app.config.from_object(__name__) + self.common_object_test(app) + + def test_config_from_class(self): + class Base(object): + TEST_KEY = 'foo' + class Test(Base): + SECRET_KEY = 'devkey' + app = flask.Flask(__name__) + app.config.from_object(Test) + self.common_object_test(app) + + def test_config_from_envvar(self): + env = os.environ + try: + os.environ = {} + app = flask.Flask(__name__) + try: + app.config.from_envvar('FOO_SETTINGS') + except RuntimeError, e: + self.assert_("'FOO_SETTINGS' is not set" in str(e)) + else: + self.assert_(0, 'expected exception') + self.assert_(not app.config.from_envvar('FOO_SETTINGS', silent=True)) + + os.environ = {'FOO_SETTINGS': __file__.rsplit('.', 1)[0] + '.py'} + self.assert_(app.config.from_envvar('FOO_SETTINGS')) + self.common_object_test(app) + finally: + os.environ = env + + def test_config_missing(self): + app = flask.Flask(__name__) + try: + app.config.from_pyfile('missing.cfg') + except IOError, e: + msg = str(e) + self.assert_(msg.startswith('[Errno 2] Unable to load configuration ' + 'file (No such file or directory):')) + self.assert_(msg.endswith("missing.cfg'")) + else: + self.assert_(0, 'expected config') + self.assert_(not app.config.from_pyfile('missing.cfg', silent=True)) + + def test_session_lifetime(self): + app = flask.Flask(__name__) + app.config['PERMANENT_SESSION_LIFETIME'] = 42 + self.assert_equal(app.permanent_session_lifetime.seconds, 42) + + +class InstanceTestCase(FlaskTestCase): + + def test_explicit_instance_paths(self): + here = os.path.abspath(os.path.dirname(__file__)) + try: + flask.Flask(__name__, instance_path='instance') + except ValueError, e: + self.assert_('must be absolute' in str(e)) + else: + self.fail('Expected value error') + + app = flask.Flask(__name__, instance_path=here) + self.assert_equal(app.instance_path, here) + + def test_uninstalled_module_paths(self): + from config_module_app import app + here = os.path.abspath(os.path.dirname(__file__)) + self.assert_equal(app.instance_path, os.path.join(here, 'test_apps', 'instance')) + + def test_uninstalled_package_paths(self): + from config_package_app import app + here = os.path.abspath(os.path.dirname(__file__)) + self.assert_equal(app.instance_path, os.path.join(here, 'test_apps', 'instance')) + + def test_installed_module_paths(self): + import types + expected_prefix = os.path.abspath('foo') + mod = types.ModuleType('myapp') + mod.__file__ = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'myapp.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_installed_package_paths(self): + import types + expected_prefix = os.path.abspath('foo') + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_prefix_installed_paths(self): + import types + expected_prefix = os.path.abspath(sys.prefix) + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_egg_installed_paths(self): + import types + expected_prefix = os.path.abspath(sys.prefix) + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'MyApp.egg', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ConfigTestCase)) + suite.addTest(unittest.makeSuite(InstanceTestCase)) + return suite diff --git a/flask/testsuite/deprecations.py b/flask/testsuite/deprecations.py new file mode 100644 index 00000000..795a5d3d --- /dev/null +++ b/flask/testsuite/deprecations.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.deprecations + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Tests deprecation support. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import flask +import unittest +from flask.testsuite import FlaskTestCase, catch_warnings + + +class DeprecationsTestCase(FlaskTestCase): + + def test_init_jinja_globals(self): + class MyFlask(flask.Flask): + def init_jinja_globals(self): + self.jinja_env.globals['foo'] = '42' + + with catch_warnings() as log: + app = MyFlask(__name__) + @app.route('/') + def foo(): + return app.jinja_env.globals['foo'] + + c = app.test_client() + self.assert_equal(c.get('/').data, '42') + self.assert_equal(len(log), 1) + self.assert_('init_jinja_globals' in str(log[0]['message'])) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(DeprecationsTestCase)) + return suite diff --git a/flask/testsuite/examples.py b/flask/testsuite/examples.py new file mode 100644 index 00000000..2d30958f --- /dev/null +++ b/flask/testsuite/examples.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.examples + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Tests the examples. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import unittest +from flask.testsuite import add_to_path + + +def setup_path(): + example_path = os.path.join(os.path.dirname(__file__), + os.pardir, os.pardir, 'examples') + add_to_path(os.path.join(example_path, 'flaskr')) + add_to_path(os.path.join(example_path, 'minitwit')) + + +def suite(): + setup_path() + suite = unittest.TestSuite() + try: + from minitwit_tests import MiniTwitTestCase + except ImportError: + pass + else: + suite.addTest(unittest.makeSuite(MiniTwitTestCase)) + try: + from flaskr_tests import FlaskrTestCase + except ImportError: + pass + else: + suite.addTest(unittest.makeSuite(FlaskrTestCase)) + return suite diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py new file mode 100644 index 00000000..034ab5be --- /dev/null +++ b/flask/testsuite/ext.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.ext + ~~~~~~~~~~~~~~~~~~~ + + Tests the extension import thing. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from __future__ import with_statement + +import sys +import unittest +from flask.testsuite import FlaskTestCase + + +class ExtImportHookTestCase(FlaskTestCase): + + def setup(self): + # we clear this out for various reasons. The most important one is + # that a real flaskext could be in there which would disable our + # fake package. Secondly we want to make sure that the flaskext + # import hook does not break on reloading. + for entry, value in sys.modules.items(): + if (entry.startswith('flask.ext.') or + entry.startswith('flask_') or + entry.startswith('flaskext.') or + entry == 'flaskext') and value is not None: + sys.modules.pop(entry, None) + from flask import ext + reload(ext) + + # reloading must not add more hooks + import_hooks = 0 + for item in sys.meta_path: + cls = type(item) + if cls.__module__ == 'flask.exthook' and \ + cls.__name__ == 'ExtensionImporter': + import_hooks += 1 + self.assert_equal(import_hooks, 1) + + def teardown(self): + from flask import ext + for key in ext.__dict__: + self.assert_('.' not in key) + + def test_flaskext_new_simple_import_normal(self): + from flask.ext.newext_simple import ext_id + self.assert_equal(ext_id, 'newext_simple') + + def test_flaskext_new_simple_import_module(self): + from flask.ext import newext_simple + self.assert_equal(newext_simple.ext_id, 'newext_simple') + self.assert_equal(newext_simple.__name__, 'flask_newext_simple') + + def test_flaskext_new_package_import_normal(self): + from flask.ext.newext_package import ext_id + self.assert_equal(ext_id, 'newext_package') + + def test_flaskext_new_package_import_module(self): + from flask.ext import newext_package + self.assert_equal(newext_package.ext_id, 'newext_package') + self.assert_equal(newext_package.__name__, 'flask_newext_package') + + def test_flaskext_new_package_import_submodule_function(self): + from flask.ext.newext_package.submodule import test_function + self.assert_equal(test_function(), 42) + + def test_flaskext_new_package_import_submodule(self): + from flask.ext.newext_package import submodule + self.assert_equal(submodule.__name__, 'flask_newext_package.submodule') + self.assert_equal(submodule.test_function(), 42) + + def test_flaskext_old_simple_import_normal(self): + from flask.ext.oldext_simple import ext_id + self.assert_equal(ext_id, 'oldext_simple') + + def test_flaskext_old_simple_import_module(self): + from flask.ext import oldext_simple + self.assert_equal(oldext_simple.ext_id, 'oldext_simple') + self.assert_equal(oldext_simple.__name__, 'flaskext.oldext_simple') + + def test_flaskext_old_package_import_normal(self): + from flask.ext.oldext_package import ext_id + self.assert_equal(ext_id, 'oldext_package') + + def test_flaskext_old_package_import_module(self): + from flask.ext import oldext_package + self.assert_equal(oldext_package.ext_id, 'oldext_package') + self.assert_equal(oldext_package.__name__, 'flaskext.oldext_package') + + def test_flaskext_old_package_import_submodule(self): + from flask.ext.oldext_package import submodule + self.assert_equal(submodule.__name__, 'flaskext.oldext_package.submodule') + self.assert_equal(submodule.test_function(), 42) + + def test_flaskext_old_package_import_submodule_function(self): + from flask.ext.oldext_package.submodule import test_function + self.assert_equal(test_function(), 42) + + def test_flaskext_broken_package_no_module_caching(self): + for x in xrange(2): + with self.assert_raises(ImportError): + import flask.ext.broken + + def test_no_error_swallowing(self): + try: + import flask.ext.broken + except ImportError: + exc_type, exc_value, tb = sys.exc_info() + self.assert_(exc_type is ImportError) + self.assert_equal(str(exc_value), 'No module named missing_module') + self.assert_(tb.tb_frame.f_globals is globals()) + + next = tb.tb_next + self.assert_('flask_broken/__init__.py' in next.tb_frame.f_code.co_filename) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ExtImportHookTestCase)) + return suite diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py new file mode 100644 index 00000000..052d36e1 --- /dev/null +++ b/flask/testsuite/helpers.py @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.helpers + ~~~~~~~~~~~~~~~~~~~~~~~ + + Various helpers. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import os +import flask +import unittest +from logging import StreamHandler +from StringIO import StringIO +from flask.testsuite import FlaskTestCase, catch_warnings, catch_stderr +from werkzeug.http import parse_options_header + + +def has_encoding(name): + try: + import codecs + codecs.lookup(name) + return True + except LookupError: + return False + + +class JSONTestCase(FlaskTestCase): + + def test_json_bad_requests(self): + app = flask.Flask(__name__) + @app.route('/json', methods=['POST']) + def return_json(): + return unicode(flask.request.json) + c = app.test_client() + rv = c.post('/json', data='malformed', content_type='application/json') + self.assert_equal(rv.status_code, 400) + + def test_json_body_encoding(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + return flask.request.json + + c = app.test_client() + resp = c.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'), + content_type='application/json; charset=iso-8859-15') + self.assert_equal(resp.data, u'Hällo Wörld'.encode('utf-8')) + + def test_jsonify(self): + d = dict(a=23, b=42, c=[1, 2, 3]) + app = flask.Flask(__name__) + @app.route('/kw') + def return_kwargs(): + return flask.jsonify(**d) + @app.route('/dict') + def return_dict(): + return flask.jsonify(d) + c = app.test_client() + for url in '/kw', '/dict': + rv = c.get(url) + self.assert_equal(rv.mimetype, 'application/json') + self.assert_equal(flask.json.loads(rv.data), d) + + def test_json_attr(self): + app = flask.Flask(__name__) + @app.route('/add', methods=['POST']) + def add(): + return unicode(flask.request.json['a'] + flask.request.json['b']) + c = app.test_client() + rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), + content_type='application/json') + self.assert_equal(rv.data, '3') + + def test_template_escaping(self): + app = flask.Flask(__name__) + render = flask.render_template_string + with app.test_request_context(): + rv = render('{{ ""|tojson|safe }}') + self.assert_equal(rv, '"<\\/script>"') + rv = render('{{ "<\0/script>"|tojson|safe }}') + self.assert_equal(rv, '"<\\u0000\\/script>"') + + def test_modified_url_encoding(self): + class ModifiedRequest(flask.Request): + url_charset = 'euc-kr' + app = flask.Flask(__name__) + app.request_class = ModifiedRequest + app.url_map.charset = 'euc-kr' + + @app.route('/') + def index(): + return flask.request.args['foo'] + + rv = app.test_client().get(u'/?foo=정상처리'.encode('euc-kr')) + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data, u'정상처리'.encode('utf-8')) + + if not has_encoding('euc-kr'): + test_modified_url_encoding = None + + +class SendfileTestCase(FlaskTestCase): + + def test_send_file_regular(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.send_file('static/index.html') + self.assert_(rv.direct_passthrough) + self.assert_equal(rv.mimetype, 'text/html') + with app.open_resource('static/index.html') as f: + self.assert_equal(rv.data, f.read()) + + def test_send_file_xsendfile(self): + app = flask.Flask(__name__) + app.use_x_sendfile = True + with app.test_request_context(): + rv = flask.send_file('static/index.html') + self.assert_(rv.direct_passthrough) + self.assert_('x-sendfile' in rv.headers) + self.assert_equal(rv.headers['x-sendfile'], + os.path.join(app.root_path, 'static/index.html')) + self.assert_equal(rv.mimetype, 'text/html') + + def test_send_file_object(self): + app = flask.Flask(__name__) + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f) + with app.open_resource('static/index.html') as f: + self.assert_equal(rv.data, f.read()) + self.assert_equal(rv.mimetype, 'text/html') + # mimetypes + etag + self.assert_equal(len(captured), 2) + + app.use_x_sendfile = True + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f) + self.assert_equal(rv.mimetype, 'text/html') + self.assert_('x-sendfile' in rv.headers) + self.assert_equal(rv.headers['x-sendfile'], + os.path.join(app.root_path, 'static/index.html')) + # mimetypes + etag + self.assert_equal(len(captured), 2) + + app.use_x_sendfile = False + with app.test_request_context(): + with catch_warnings() as captured: + f = StringIO('Test') + rv = flask.send_file(f) + self.assert_equal(rv.data, 'Test') + self.assert_equal(rv.mimetype, 'application/octet-stream') + # etags + self.assert_equal(len(captured), 1) + with catch_warnings() as captured: + f = StringIO('Test') + rv = flask.send_file(f, mimetype='text/plain') + self.assert_equal(rv.data, 'Test') + self.assert_equal(rv.mimetype, 'text/plain') + # etags + self.assert_equal(len(captured), 1) + + app.use_x_sendfile = True + with catch_warnings() as captured: + with app.test_request_context(): + f = StringIO('Test') + rv = flask.send_file(f) + self.assert_('x-sendfile' not in rv.headers) + # etags + self.assert_equal(len(captured), 1) + + def test_attachment(self): + app = flask.Flask(__name__) + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f, as_attachment=True) + value, options = parse_options_header(rv.headers['Content-Disposition']) + self.assert_equal(value, 'attachment') + # mimetypes + etag + self.assert_equal(len(captured), 2) + + with app.test_request_context(): + self.assert_equal(options['filename'], 'index.html') + rv = flask.send_file('static/index.html', as_attachment=True) + value, options = parse_options_header(rv.headers['Content-Disposition']) + self.assert_equal(value, 'attachment') + self.assert_equal(options['filename'], 'index.html') + + with app.test_request_context(): + rv = flask.send_file(StringIO('Test'), as_attachment=True, + attachment_filename='index.txt', + add_etags=False) + self.assert_equal(rv.mimetype, 'text/plain') + value, options = parse_options_header(rv.headers['Content-Disposition']) + self.assert_equal(value, 'attachment') + self.assert_equal(options['filename'], 'index.txt') + + +class LoggingTestCase(FlaskTestCase): + + def test_logger_cache(self): + app = flask.Flask(__name__) + logger1 = app.logger + self.assert_(app.logger is logger1) + self.assert_equal(logger1.name, __name__) + app.logger_name = __name__ + '/test_logger_cache' + self.assert_(app.logger is not logger1) + + def test_debug_log(self): + app = flask.Flask(__name__) + app.debug = True + + @app.route('/') + def index(): + app.logger.warning('the standard library is dead') + app.logger.debug('this is a debug statement') + return '' + + @app.route('/exc') + def exc(): + 1/0 + + with app.test_client() as c: + with catch_stderr() as err: + c.get('/') + out = err.getvalue() + self.assert_('WARNING in helpers [' in out) + self.assert_(os.path.basename(__file__.rsplit('.', 1)[0] + '.py') in out) + self.assert_('the standard library is dead' in out) + self.assert_('this is a debug statement' in out) + + with catch_stderr() as err: + try: + c.get('/exc') + except ZeroDivisionError: + pass + else: + self.assert_(False, 'debug log ate the exception') + + def test_exception_logging(self): + out = StringIO() + app = flask.Flask(__name__) + app.logger_name = 'flask_tests/test_exception_logging' + app.logger.addHandler(StreamHandler(out)) + + @app.route('/') + def index(): + 1/0 + + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 500) + self.assert_('Internal Server Error' in rv.data) + + err = out.getvalue() + self.assert_('Exception on / [GET]' in err) + self.assert_('Traceback (most recent call last):' in err) + self.assert_('1/0' in err) + self.assert_('ZeroDivisionError:' in err) + + def test_processor_exceptions(self): + app = flask.Flask(__name__) + @app.before_request + def before_request(): + if trigger == 'before': + 1/0 + @app.after_request + def after_request(response): + if trigger == 'after': + 1/0 + return response + @app.route('/') + def index(): + return 'Foo' + @app.errorhandler(500) + def internal_server_error(e): + return 'Hello Server Error', 500 + for trigger in 'before', 'after': + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 500) + self.assert_equal(rv.data, 'Hello Server Error') + + +def suite(): + suite = unittest.TestSuite() + if flask.json_available: + suite.addTest(unittest.makeSuite(JSONTestCase)) + suite.addTest(unittest.makeSuite(SendfileTestCase)) + suite.addTest(unittest.makeSuite(LoggingTestCase)) + return suite diff --git a/flask/testsuite/signals.py b/flask/testsuite/signals.py new file mode 100644 index 00000000..da1a68ca --- /dev/null +++ b/flask/testsuite/signals.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.signals + ~~~~~~~~~~~~~~~~~~~~~~~ + + Signalling. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +class SignalsTestCase(FlaskTestCase): + + def test_template_rendered(self): + app = flask.Flask(__name__) + + @app.route('/') + def index(): + return flask.render_template('simple_template.html', whiskey=42) + + recorded = [] + def record(sender, template, context): + recorded.append((template, context)) + + flask.template_rendered.connect(record, app) + try: + app.test_client().get('/') + self.assert_equal(len(recorded), 1) + template, context = recorded[0] + self.assert_equal(template.name, 'simple_template.html') + self.assert_equal(context['whiskey'], 42) + finally: + flask.template_rendered.disconnect(record, app) + + def test_request_signals(self): + app = flask.Flask(__name__) + calls = [] + + def before_request_signal(sender): + calls.append('before-signal') + + def after_request_signal(sender, response): + self.assert_equal(response.data, 'stuff') + calls.append('after-signal') + + @app.before_request + def before_request_handler(): + calls.append('before-handler') + + @app.after_request + def after_request_handler(response): + calls.append('after-handler') + response.data = 'stuff' + return response + + @app.route('/') + def index(): + calls.append('handler') + return 'ignored anyway' + + flask.request_started.connect(before_request_signal, app) + flask.request_finished.connect(after_request_signal, app) + + try: + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'stuff') + + self.assert_equal(calls, ['before-signal', 'before-handler', + 'handler', 'after-handler', + 'after-signal']) + finally: + flask.request_started.disconnect(before_request_signal, app) + flask.request_finished.disconnect(after_request_signal, app) + + def test_request_exception_signal(self): + app = flask.Flask(__name__) + recorded = [] + + @app.route('/') + def index(): + 1/0 + + def record(sender, exception): + recorded.append(exception) + + flask.got_request_exception.connect(record, app) + try: + self.assert_equal(app.test_client().get('/').status_code, 500) + self.assert_equal(len(recorded), 1) + self.assert_(isinstance(recorded[0], ZeroDivisionError)) + finally: + flask.got_request_exception.disconnect(record, app) + + +def suite(): + suite = unittest.TestSuite() + if flask.signals_available: + suite.addTest(unittest.makeSuite(SignalsTestCase)) + return suite diff --git a/tests/static/index.html b/flask/testsuite/static/index.html similarity index 100% rename from tests/static/index.html rename to flask/testsuite/static/index.html diff --git a/flask/testsuite/subclassing.py b/flask/testsuite/subclassing.py new file mode 100644 index 00000000..e56ad563 --- /dev/null +++ b/flask/testsuite/subclassing.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.subclassing + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Test that certain behavior of flask can be customized by + subclasses. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +from StringIO import StringIO +from logging import StreamHandler +from flask.testsuite import FlaskTestCase + + +class FlaskSubclassingTestCase(FlaskTestCase): + + def test_supressed_exception_logging(self): + class SupressedFlask(flask.Flask): + def log_exception(self, exc_info): + pass + + out = StringIO() + app = SupressedFlask(__name__) + app.logger_name = 'flask_tests/test_supressed_exception_logging' + app.logger.addHandler(StreamHandler(out)) + + @app.route('/') + def index(): + 1/0 + + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 500) + self.assert_('Internal Server Error' in rv.data) + + err = out.getvalue() + self.assert_equal(err, '') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(FlaskSubclassingTestCase)) + return suite diff --git a/tests/templates/_macro.html b/flask/testsuite/templates/_macro.html similarity index 100% rename from tests/templates/_macro.html rename to flask/testsuite/templates/_macro.html diff --git a/tests/templates/context_template.html b/flask/testsuite/templates/context_template.html similarity index 100% rename from tests/templates/context_template.html rename to flask/testsuite/templates/context_template.html diff --git a/tests/templates/escaping_template.html b/flask/testsuite/templates/escaping_template.html similarity index 100% rename from tests/templates/escaping_template.html rename to flask/testsuite/templates/escaping_template.html diff --git a/tests/templates/mail.txt b/flask/testsuite/templates/mail.txt similarity index 100% rename from tests/templates/mail.txt rename to flask/testsuite/templates/mail.txt diff --git a/tests/templates/nested/nested.txt b/flask/testsuite/templates/nested/nested.txt similarity index 100% rename from tests/templates/nested/nested.txt rename to flask/testsuite/templates/nested/nested.txt diff --git a/tests/templates/simple_template.html b/flask/testsuite/templates/simple_template.html similarity index 100% rename from tests/templates/simple_template.html rename to flask/testsuite/templates/simple_template.html diff --git a/flask/testsuite/templates/template_filter.html b/flask/testsuite/templates/template_filter.html new file mode 100644 index 00000000..af46cd94 --- /dev/null +++ b/flask/testsuite/templates/template_filter.html @@ -0,0 +1 @@ +{{ value|super_reverse }} \ No newline at end of file diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py new file mode 100644 index 00000000..453bfb65 --- /dev/null +++ b/flask/testsuite/templating.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.templating + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Template functionality + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +class TemplatingTestCase(FlaskTestCase): + + def test_context_processing(self): + app = flask.Flask(__name__) + @app.context_processor + def context_processor(): + return {'injected_value': 42} + @app.route('/') + def index(): + return flask.render_template('context_template.html', value=23) + rv = app.test_client().get('/') + self.assert_equal(rv.data, '

23|42') + + def test_original_win(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template_string('{{ config }}', config=42) + rv = app.test_client().get('/') + self.assert_equal(rv.data, '42') + + def test_standard_context(self): + app = flask.Flask(__name__) + app.secret_key = 'development key' + @app.route('/') + def index(): + flask.g.foo = 23 + flask.session['test'] = 'aha' + return flask.render_template_string(''' + {{ request.args.foo }} + {{ g.foo }} + {{ config.DEBUG }} + {{ session.test }} + ''') + rv = app.test_client().get('/?foo=42') + self.assert_equal(rv.data.split(), ['42', '23', 'False', 'aha']) + + def test_escaping(self): + text = '

Hello World!' + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template('escaping_template.html', text=text, + html=flask.Markup(text)) + lines = app.test_client().get('/').data.splitlines() + self.assert_equal(lines, [ + '<p>Hello World!', + '

Hello World!', + '

Hello World!', + '

Hello World!', + '<p>Hello World!', + '

Hello World!' + ]) + + def test_no_escaping(self): + app = flask.Flask(__name__) + with app.test_request_context(): + self.assert_equal(flask.render_template_string('{{ foo }}', + foo=''), '') + self.assert_equal(flask.render_template('mail.txt', foo=''), + ' Mail') + + def test_macros(self): + app = flask.Flask(__name__) + with app.test_request_context(): + macro = flask.get_template_attribute('_macro.html', 'hello') + self.assert_equal(macro('World'), 'Hello World!') + + def test_template_filter(self): + app = flask.Flask(__name__) + @app.template_filter() + def my_reverse(s): + return s[::-1] + self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) + self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') + + def test_template_filter_with_name(self): + app = flask.Flask(__name__) + @app.template_filter('strrev') + def my_reverse(s): + return s[::-1] + self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) + self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') + + def test_template_filter_with_template(self): + app = flask.Flask(__name__) + @app.template_filter() + def super_reverse(s): + return s[::-1] + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'dcba') + + def test_template_filter_with_name_and_template(self): + app = flask.Flask(__name__) + @app.template_filter('super_reverse') + def my_reverse(s): + return s[::-1] + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'dcba') + + def test_custom_template_loader(self): + class MyFlask(flask.Flask): + def create_global_jinja_loader(self): + from jinja2 import DictLoader + return DictLoader({'index.html': 'Hello Custom World!'}) + app = MyFlask(__name__) + @app.route('/') + def index(): + return flask.render_template('index.html') + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.data, 'Hello Custom World!') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TemplatingTestCase)) + return suite diff --git a/flask/testsuite/test_apps/blueprintapp/__init__.py b/flask/testsuite/test_apps/blueprintapp/__init__.py new file mode 100644 index 00000000..2b8ef75d --- /dev/null +++ b/flask/testsuite/test_apps/blueprintapp/__init__.py @@ -0,0 +1,7 @@ +from flask import Flask + +app = Flask(__name__) +from blueprintapp.apps.admin import admin +from blueprintapp.apps.frontend import frontend +app.register_blueprint(admin) +app.register_blueprint(frontend) diff --git a/tests/test_apps/blueprintapp/apps/__init__.py b/flask/testsuite/test_apps/blueprintapp/apps/__init__.py similarity index 100% rename from tests/test_apps/blueprintapp/apps/__init__.py rename to flask/testsuite/test_apps/blueprintapp/apps/__init__.py diff --git a/flask/testsuite/test_apps/blueprintapp/apps/admin/__init__.py b/flask/testsuite/test_apps/blueprintapp/apps/admin/__init__.py new file mode 100644 index 00000000..3f714d95 --- /dev/null +++ b/flask/testsuite/test_apps/blueprintapp/apps/admin/__init__.py @@ -0,0 +1,15 @@ +from flask import Blueprint, render_template + +admin = Blueprint('admin', __name__, url_prefix='/admin', + template_folder='templates', + static_folder='static') + + +@admin.route('/') +def index(): + return render_template('admin/index.html') + + +@admin.route('/index2') +def index2(): + return render_template('./admin/index.html') diff --git a/tests/test_apps/blueprintapp/apps/admin/static/css/test.css b/flask/testsuite/test_apps/blueprintapp/apps/admin/static/css/test.css similarity index 100% rename from tests/test_apps/blueprintapp/apps/admin/static/css/test.css rename to flask/testsuite/test_apps/blueprintapp/apps/admin/static/css/test.css diff --git a/tests/test_apps/blueprintapp/apps/admin/static/test.txt b/flask/testsuite/test_apps/blueprintapp/apps/admin/static/test.txt similarity index 100% rename from tests/test_apps/blueprintapp/apps/admin/static/test.txt rename to flask/testsuite/test_apps/blueprintapp/apps/admin/static/test.txt diff --git a/tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html b/flask/testsuite/test_apps/blueprintapp/apps/admin/templates/admin/index.html similarity index 100% rename from tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html rename to flask/testsuite/test_apps/blueprintapp/apps/admin/templates/admin/index.html diff --git a/flask/testsuite/test_apps/blueprintapp/apps/frontend/__init__.py b/flask/testsuite/test_apps/blueprintapp/apps/frontend/__init__.py new file mode 100644 index 00000000..69c8666a --- /dev/null +++ b/flask/testsuite/test_apps/blueprintapp/apps/frontend/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint, render_template + +frontend = Blueprint('frontend', __name__, template_folder='templates') + + +@frontend.route('/') +def index(): + return render_template('frontend/index.html') diff --git a/tests/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html b/flask/testsuite/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html similarity index 100% rename from tests/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html rename to flask/testsuite/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html diff --git a/flask/testsuite/test_apps/config_module_app.py b/flask/testsuite/test_apps/config_module_app.py new file mode 100644 index 00000000..380d46bf --- /dev/null +++ b/flask/testsuite/test_apps/config_module_app.py @@ -0,0 +1,4 @@ +import os +import flask +here = os.path.abspath(os.path.dirname(__file__)) +app = flask.Flask(__name__) diff --git a/flask/testsuite/test_apps/config_package_app/__init__.py b/flask/testsuite/test_apps/config_package_app/__init__.py new file mode 100644 index 00000000..380d46bf --- /dev/null +++ b/flask/testsuite/test_apps/config_package_app/__init__.py @@ -0,0 +1,4 @@ +import os +import flask +here = os.path.abspath(os.path.dirname(__file__)) +app = flask.Flask(__name__) diff --git a/flask/testsuite/test_apps/flask_broken/__init__.py b/flask/testsuite/test_apps/flask_broken/__init__.py new file mode 100644 index 00000000..c194c04f --- /dev/null +++ b/flask/testsuite/test_apps/flask_broken/__init__.py @@ -0,0 +1,2 @@ +import flask.ext.broken.b +import missing_module diff --git a/src/flask/py.typed b/flask/testsuite/test_apps/flask_broken/b.py similarity index 100% rename from src/flask/py.typed rename to flask/testsuite/test_apps/flask_broken/b.py diff --git a/flask/testsuite/test_apps/flask_newext_package/__init__.py b/flask/testsuite/test_apps/flask_newext_package/__init__.py new file mode 100644 index 00000000..3fd13e17 --- /dev/null +++ b/flask/testsuite/test_apps/flask_newext_package/__init__.py @@ -0,0 +1 @@ +ext_id = 'newext_package' diff --git a/flask/testsuite/test_apps/flask_newext_package/submodule.py b/flask/testsuite/test_apps/flask_newext_package/submodule.py new file mode 100644 index 00000000..26ad56b7 --- /dev/null +++ b/flask/testsuite/test_apps/flask_newext_package/submodule.py @@ -0,0 +1,2 @@ +def test_function(): + return 42 diff --git a/flask/testsuite/test_apps/flask_newext_simple.py b/flask/testsuite/test_apps/flask_newext_simple.py new file mode 100644 index 00000000..dc4a3628 --- /dev/null +++ b/flask/testsuite/test_apps/flask_newext_simple.py @@ -0,0 +1 @@ +ext_id = 'newext_simple' diff --git a/tests/test_apps/cliapp/__init__.py b/flask/testsuite/test_apps/flaskext/__init__.py similarity index 100% rename from tests/test_apps/cliapp/__init__.py rename to flask/testsuite/test_apps/flaskext/__init__.py diff --git a/flask/testsuite/test_apps/flaskext/oldext_package/__init__.py b/flask/testsuite/test_apps/flaskext/oldext_package/__init__.py new file mode 100644 index 00000000..7c462065 --- /dev/null +++ b/flask/testsuite/test_apps/flaskext/oldext_package/__init__.py @@ -0,0 +1 @@ +ext_id = 'oldext_package' diff --git a/flask/testsuite/test_apps/flaskext/oldext_package/submodule.py b/flask/testsuite/test_apps/flaskext/oldext_package/submodule.py new file mode 100644 index 00000000..26ad56b7 --- /dev/null +++ b/flask/testsuite/test_apps/flaskext/oldext_package/submodule.py @@ -0,0 +1,2 @@ +def test_function(): + return 42 diff --git a/flask/testsuite/test_apps/flaskext/oldext_simple.py b/flask/testsuite/test_apps/flaskext/oldext_simple.py new file mode 100644 index 00000000..c6664a78 --- /dev/null +++ b/flask/testsuite/test_apps/flaskext/oldext_simple.py @@ -0,0 +1 @@ +ext_id = 'oldext_simple' diff --git a/flask/testsuite/test_apps/moduleapp/__init__.py b/flask/testsuite/test_apps/moduleapp/__init__.py new file mode 100644 index 00000000..35e82d4e --- /dev/null +++ b/flask/testsuite/test_apps/moduleapp/__init__.py @@ -0,0 +1,7 @@ +from flask import Flask + +app = Flask(__name__) +from moduleapp.apps.admin import admin +from moduleapp.apps.frontend import frontend +app.register_module(admin) +app.register_module(frontend) diff --git a/tests/test_apps/cliapp/inner1/inner2/__init__.py b/flask/testsuite/test_apps/moduleapp/apps/__init__.py similarity index 100% rename from tests/test_apps/cliapp/inner1/inner2/__init__.py rename to flask/testsuite/test_apps/moduleapp/apps/__init__.py diff --git a/flask/testsuite/test_apps/moduleapp/apps/admin/__init__.py b/flask/testsuite/test_apps/moduleapp/apps/admin/__init__.py new file mode 100644 index 00000000..b85b8024 --- /dev/null +++ b/flask/testsuite/test_apps/moduleapp/apps/admin/__init__.py @@ -0,0 +1,14 @@ +from flask import Module, render_template + + +admin = Module(__name__, url_prefix='/admin') + + +@admin.route('/') +def index(): + return render_template('admin/index.html') + + +@admin.route('/index2') +def index2(): + return render_template('./admin/index.html') diff --git a/flask/testsuite/test_apps/moduleapp/apps/admin/static/css/test.css b/flask/testsuite/test_apps/moduleapp/apps/admin/static/css/test.css new file mode 100644 index 00000000..b9f564de --- /dev/null +++ b/flask/testsuite/test_apps/moduleapp/apps/admin/static/css/test.css @@ -0,0 +1 @@ +/* nested file */ diff --git a/flask/testsuite/test_apps/moduleapp/apps/admin/static/test.txt b/flask/testsuite/test_apps/moduleapp/apps/admin/static/test.txt new file mode 100644 index 00000000..f220d22f --- /dev/null +++ b/flask/testsuite/test_apps/moduleapp/apps/admin/static/test.txt @@ -0,0 +1 @@ +Admin File diff --git a/flask/testsuite/test_apps/moduleapp/apps/admin/templates/index.html b/flask/testsuite/test_apps/moduleapp/apps/admin/templates/index.html new file mode 100644 index 00000000..eeec199a --- /dev/null +++ b/flask/testsuite/test_apps/moduleapp/apps/admin/templates/index.html @@ -0,0 +1 @@ +Hello from the Admin diff --git a/flask/testsuite/test_apps/moduleapp/apps/frontend/__init__.py b/flask/testsuite/test_apps/moduleapp/apps/frontend/__init__.py new file mode 100644 index 00000000..f83581e7 --- /dev/null +++ b/flask/testsuite/test_apps/moduleapp/apps/frontend/__init__.py @@ -0,0 +1,9 @@ +from flask import Module, render_template + + +frontend = Module(__name__) + + +@frontend.route('/') +def index(): + return render_template('frontend/index.html') diff --git a/flask/testsuite/test_apps/moduleapp/apps/frontend/templates/index.html b/flask/testsuite/test_apps/moduleapp/apps/frontend/templates/index.html new file mode 100644 index 00000000..a062d713 --- /dev/null +++ b/flask/testsuite/test_apps/moduleapp/apps/frontend/templates/index.html @@ -0,0 +1 @@ +Hello from the Frontend diff --git a/flask/testsuite/test_apps/subdomaintestmodule/__init__.py b/flask/testsuite/test_apps/subdomaintestmodule/__init__.py new file mode 100644 index 00000000..3c5e3583 --- /dev/null +++ b/flask/testsuite/test_apps/subdomaintestmodule/__init__.py @@ -0,0 +1,4 @@ +from flask import Module + + +mod = Module(__name__, 'foo', subdomain='foo') diff --git a/tests/test_apps/subdomaintestmodule/static/hello.txt b/flask/testsuite/test_apps/subdomaintestmodule/static/hello.txt similarity index 100% rename from tests/test_apps/subdomaintestmodule/static/hello.txt rename to flask/testsuite/test_apps/subdomaintestmodule/static/hello.txt diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py new file mode 100644 index 00000000..6574e77d --- /dev/null +++ b/flask/testsuite/testing.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.testing + ~~~~~~~~~~~~~~~~~~~~~~~ + + Test client and more. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +class TestToolsTestCase(FlaskTestCase): + + def test_environ_defaults_from_config(self): + app = flask.Flask(__name__) + app.testing = True + app.config['SERVER_NAME'] = 'example.com:1234' + app.config['APPLICATION_ROOT'] = '/foo' + @app.route('/') + def index(): + return flask.request.url + + ctx = app.test_request_context() + self.assert_equal(ctx.request.url, 'http://example.com:1234/foo/') + with app.test_client() as c: + rv = c.get('/') + self.assert_equal(rv.data, 'http://example.com:1234/foo/') + + def test_environ_defaults(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + return flask.request.url + + ctx = app.test_request_context() + self.assert_equal(ctx.request.url, 'http://localhost/') + with app.test_client() as c: + rv = c.get('/') + self.assert_equal(rv.data, 'http://localhost/') + + def test_session_transactions(self): + app = flask.Flask(__name__) + app.testing = True + app.secret_key = 'testing' + + @app.route('/') + def index(): + return unicode(flask.session['foo']) + + with app.test_client() as c: + with c.session_transaction() as sess: + self.assert_equal(len(sess), 0) + sess['foo'] = [42] + self.assert_equal(len(sess), 1) + rv = c.get('/') + self.assert_equal(rv.data, '[42]') + with c.session_transaction() as sess: + self.assert_equal(len(sess), 1) + self.assert_equal(sess['foo'], [42]) + + def test_session_transactions_no_null_sessions(self): + app = flask.Flask(__name__) + app.testing = True + + with app.test_client() as c: + try: + with c.session_transaction() as sess: + pass + except RuntimeError, e: + self.assert_('Session backend did not open a session' in str(e)) + else: + self.fail('Expected runtime error') + + def test_session_transactions_keep_context(self): + app = flask.Flask(__name__) + app.testing = True + app.secret_key = 'testing' + + with app.test_client() as c: + rv = c.get('/') + req = flask.request._get_current_object() + with c.session_transaction(): + self.assert_(req is flask.request._get_current_object()) + + def test_session_transaction_needs_cookies(self): + app = flask.Flask(__name__) + app.testing = True + c = app.test_client(use_cookies=False) + try: + with c.session_transaction() as s: + pass + except RuntimeError, e: + self.assert_('cookies' in str(e)) + else: + self.fail('Expected runtime error') + + def test_test_client_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + flask.g.value = 42 + return 'Hello World!' + + @app.route('/other') + def other(): + 1/0 + + with app.test_client() as c: + resp = c.get('/') + self.assert_equal(flask.g.value, 42) + self.assert_equal(resp.data, 'Hello World!') + self.assert_equal(resp.status_code, 200) + + resp = c.get('/other') + self.assert_(not hasattr(flask.g, 'value')) + self.assert_('Internal Server Error' in resp.data) + self.assert_equal(resp.status_code, 500) + flask.g.value = 23 + + try: + flask.g.value + except (AttributeError, RuntimeError): + pass + else: + raise AssertionError('some kind of exception expected') + + def test_reuse_client(self): + app = flask.Flask(__name__) + c = app.test_client() + + with c: + self.assert_equal(c.get('/').status_code, 404) + + with c: + self.assert_equal(c.get('/').status_code, 404) + + def test_test_client_calls_teardown_handlers(self): + app = flask.Flask(__name__) + called = [] + @app.teardown_request + def remember(error): + called.append(error) + + with app.test_client() as c: + self.assert_equal(called, []) + c.get('/') + self.assert_equal(called, []) + self.assert_equal(called, [None]) + + del called[:] + with app.test_client() as c: + self.assert_equal(called, []) + c.get('/') + self.assert_equal(called, []) + c.get('/') + self.assert_equal(called, [None]) + self.assert_equal(called, [None, None]) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestToolsTestCase)) + return suite diff --git a/flask/testsuite/views.py b/flask/testsuite/views.py new file mode 100644 index 00000000..c7cb0a8a --- /dev/null +++ b/flask/testsuite/views.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.views + ~~~~~~~~~~~~~~~~~~~~~ + + Pluggable views. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import flask.views +import unittest +from flask.testsuite import FlaskTestCase +from werkzeug.http import parse_set_header + + +class ViewTestCase(FlaskTestCase): + + def common_test(self, app): + c = app.test_client() + + self.assert_equal(c.get('/').data, 'GET') + self.assert_equal(c.post('/').data, 'POST') + self.assert_equal(c.put('/').status_code, 405) + meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) + self.assert_equal(sorted(meths), ['GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_basic_view(self): + app = flask.Flask(__name__) + + class Index(flask.views.View): + methods = ['GET', 'POST'] + def dispatch_request(self): + return flask.request.method + + app.add_url_rule('/', view_func=Index.as_view('index')) + self.common_test(app) + + def test_method_based_view(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def post(self): + return 'POST' + + app.add_url_rule('/', view_func=Index.as_view('index')) + + self.common_test(app) + + def test_view_patching(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + 1/0 + def post(self): + 1/0 + + class Other(Index): + def get(self): + return 'GET' + def post(self): + return 'POST' + + view = Index.as_view('index') + view.view_class = Other + app.add_url_rule('/', view_func=view) + self.common_test(app) + + def test_view_inheritance(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def post(self): + return 'POST' + + class BetterIndex(Index): + def delete(self): + return 'DELETE' + + app.add_url_rule('/', view_func=BetterIndex.as_view('index')) + c = app.test_client() + + meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) + self.assert_equal(sorted(meths), ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_view_decorators(self): + app = flask.Flask(__name__) + + def add_x_parachute(f): + def new_function(*args, **kwargs): + resp = flask.make_response(f(*args, **kwargs)) + resp.headers['X-Parachute'] = 'awesome' + return resp + return new_function + + class Index(flask.views.View): + decorators = [add_x_parachute] + def dispatch_request(self): + return 'Awesome' + + app.add_url_rule('/', view_func=Index.as_view('index')) + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.headers['X-Parachute'], 'awesome') + self.assert_equal(rv.data, 'Awesome') + + def test_implicit_head(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return flask.Response('Blub', headers={ + 'X-Method': flask.request.method + }) + + app.add_url_rule('/', view_func=Index.as_view('index')) + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.data, 'Blub') + self.assert_equal(rv.headers['X-Method'], 'GET') + rv = c.head('/') + self.assert_equal(rv.data, '') + self.assert_equal(rv.headers['X-Method'], 'HEAD') + + def test_explicit_head(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def head(self): + return flask.Response('', headers={'X-Method': 'HEAD'}) + + app.add_url_rule('/', view_func=Index.as_view('index')) + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.data, 'GET') + rv = c.head('/') + self.assert_equal(rv.data, '') + self.assert_equal(rv.headers['X-Method'], 'HEAD') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ViewTestCase)) + return suite diff --git a/flask/views.py b/flask/views.py new file mode 100644 index 00000000..f11c3ddd --- /dev/null +++ b/flask/views.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +""" + flask.views + ~~~~~~~~~~~ + + This module provides class based views inspired by the ones in Django. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from .globals import request + + +http_method_funcs = frozenset(['get', 'post', 'head', 'options', + 'delete', 'put', 'trace']) + + +class View(object): + """Alternative way to use view functions. A subclass has to implement + :meth:`dispatch_request` which is called with the view arguments from + the URL routing system. If :attr:`methods` is provided the methods + do not have to be passed to the :meth:`~flask.Flask.add_url_rule` + method explicitly:: + + class MyView(View): + methods = ['GET'] + + def dispatch_request(self, name): + return 'Hello %s!' % name + + app.add_url_rule('/hello/', view_func=MyView.as_view('myview')) + + When you want to decorate a pluggable view you will have to either do that + when the view function is created (by wrapping the return value of + :meth:`as_view`) or you can use the :attr:`decorators` attribute:: + + class SecretView(View): + methods = ['GET'] + decorators = [superuser_required] + + def dispatch_request(self): + ... + + The decorators stored in the decorators list are applied one after another + when the view function is created. Note that you can *not* use the class + based decorators since those would decorate the view class and not the + generated view function! + """ + + #: A for which methods this pluggable view can handle. + methods = None + + #: The canonical way to decorate class based views is to decorate the + #: return value of as_view(). However since this moves parts of the + #: logic from the class declaration to the place where it's hooked + #: into the routing system. + #: + #: You can place one or more decorators in this list and whenever the + #: view function is created the result is automatically decorated. + #: + #: .. versionadded:: 0.8 + decorators = [] + + def dispatch_request(self): + """Subclasses have to override this method to implement the + actual view function code. This method is called with all + the arguments from the URL rule. + """ + raise NotImplementedError() + + @classmethod + def as_view(cls, name, *class_args, **class_kwargs): + """Converts the class into an actual view function that can be + used with the routing system. What it does internally is generating + a function on the fly that will instanciate the :class:`View` + on each request and call the :meth:`dispatch_request` method on it. + + The arguments passed to :meth:`as_view` are forwarded to the + constructor of the class. + """ + def view(*args, **kwargs): + self = view.view_class(*class_args, **class_kwargs) + return self.dispatch_request(*args, **kwargs) + + if cls.decorators: + view.__name__ = name + view.__module__ = cls.__module__ + for decorator in cls.decorators: + view = decorator(view) + + # we attach the view class to the view function for two reasons: + # first of all it allows us to easily figure out what class based + # view this thing came from, secondly it's also used for instanciating + # the view class so you can actually replace it with something else + # for testing purposes and debugging. + view.view_class = cls + view.__name__ = name + view.__doc__ = cls.__doc__ + view.__module__ = cls.__module__ + view.methods = cls.methods + return view + + +class MethodViewType(type): + + def __new__(cls, name, bases, d): + rv = type.__new__(cls, name, bases, d) + if 'methods' not in d: + methods = set(rv.methods or []) + for key, value in d.iteritems(): + if key in http_method_funcs: + methods.add(key.upper()) + # if we have no method at all in there we don't want to + # add a method list. (This is for instance the case for + # the baseclass or another subclass of a base method view + # that does not introduce new methods). + if methods: + rv.methods = sorted(methods) + return rv + + +class MethodView(View): + """Like a regular class based view but that dispatches requests to + particular methods. For instance if you implement a method called + :meth:`get` it means you will response to ``'GET'`` requests and + the :meth:`dispatch_request` implementation will automatically + forward your request to that. Also :attr:`options` is set for you + automatically:: + + class CounterAPI(MethodView): + + def get(self): + return session.get('counter', 0) + + def post(self): + session['counter'] = session.get('counter', 0) + 1 + return 'OK' + + app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) + """ + __metaclass__ = MethodViewType + + def dispatch_request(self, *args, **kwargs): + meth = getattr(self, request.method.lower(), None) + # if the request method is HEAD and we don't have a handler for it + # retry with GET + if meth is None and request.method == 'HEAD': + meth = getattr(self, 'get', None) + assert meth is not None, 'Not implemented method %r' % request.method + return meth(*args, **kwargs) diff --git a/flask/wrappers.py b/flask/wrappers.py new file mode 100644 index 00000000..f6ec2788 --- /dev/null +++ b/flask/wrappers.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +""" + flask.wrappers + ~~~~~~~~~~~~~~ + + Implements the WSGI wrappers (request and response). + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase +from werkzeug.exceptions import BadRequest +from werkzeug.utils import cached_property + +from .debughelpers import attach_enctype_error_multidict +from .helpers import json, _assert_have_json +from .globals import _request_ctx_stack + + +class Request(RequestBase): + """The request object used by default in Flask. Remembers the + matched endpoint and view arguments. + + It is what ends up as :class:`~flask.request`. If you want to replace + the request object used you can subclass this and set + :attr:`~flask.Flask.request_class` to your subclass. + + The request object is a :class:`~werkzeug.wrappers.Request` subclass and + provides all of the attributes Werkzeug defines plus a few Flask + specific ones. + """ + + #: the internal URL rule that matched the request. This can be + #: useful to inspect which methods are allowed for the URL from + #: a before/after handler (``request.url_rule.methods``) etc. + #: + #: .. versionadded:: 0.6 + url_rule = None + + #: a dict of view arguments that matched the request. If an exception + #: happened when matching, this will be `None`. + view_args = None + + #: if matching the URL failed, this is the exception that will be + #: raised / was raised as part of the request handling. This is + #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or + #: something similar. + routing_exception = None + + # switched by the request context until 1.0 to opt in deprecated + # module functionality + _is_old_module = False + + @property + def max_content_length(self): + """Read-only view of the `MAX_CONTENT_LENGTH` config key.""" + ctx = _request_ctx_stack.top + if ctx is not None: + return ctx.app.config['MAX_CONTENT_LENGTH'] + + @property + def endpoint(self): + """The endpoint that matched the request. This in combination with + :attr:`view_args` can be used to reconstruct the same or a + modified URL. If an exception happened when matching, this will + be `None`. + """ + if self.url_rule is not None: + return self.url_rule.endpoint + + @property + def module(self): + """The name of the current module if the request was dispatched + to an actual module. This is deprecated functionality, use blueprints + instead. + """ + from warnings import warn + warn(DeprecationWarning('modules were deprecated in favor of ' + 'blueprints. Use request.blueprint ' + 'instead.'), stacklevel=2) + if self._is_old_module: + return self.blueprint + + @property + def blueprint(self): + """The name of the current blueprint""" + if self.url_rule and '.' in self.url_rule.endpoint: + return self.url_rule.endpoint.rsplit('.', 1)[0] + + @cached_property + def json(self): + """If the mimetype is `application/json` this will contain the + parsed JSON data. Otherwise this will be `None`. + + This requires Python 2.6 or an installed version of simplejson. + """ + if __debug__: + _assert_have_json() + if self.mimetype == 'application/json': + request_charset = self.mimetype_params.get('charset') + try: + if request_charset is not None: + return json.loads(self.data, encoding=request_charset) + return json.loads(self.data) + except ValueError, e: + return self.on_json_loading_failed(e) + + def on_json_loading_failed(self, e): + """Called if decoding of the JSON data failed. The return value of + this method is used by :attr:`json` when an error ocurred. The + default implementation raises a :class:`~werkzeug.exceptions.BadRequest`. + + .. versionadded:: 0.8 + """ + raise BadRequest() + + def _load_form_data(self): + RequestBase._load_form_data(self) + + # in debug mode we're replacing the files multidict with an ad-hoc + # subclass that raises a different error for key errors. + ctx = _request_ctx_stack.top + if ctx is not None and ctx.app.debug and \ + self.mimetype != 'multipart/form-data' and not self.files: + attach_enctype_error_multidict(self) + + +class Response(ResponseBase): + """The response object that is used by default in Flask. Works like the + response object from Werkzeug but is set to have an HTML mimetype by + default. Quite often you don't have to create this object yourself because + :meth:`~flask.Flask.make_response` will take care of that for you. + + If you want to replace the response object used you can subclass this and + set :attr:`~flask.Flask.response_class` to your subclass. + """ + default_mimetype = 'text/html' diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 1bc3e0e1..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,277 +0,0 @@ -[project] -name = "Flask" -version = "3.2.0.dev" -description = "A simple framework for building complex web applications." -readme = "README.md" -license = "BSD-3-Clause" -license-files = ["LICENSE.txt"] -maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Framework :: Flask", - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Topic :: Internet :: WWW/HTTP :: Dynamic Content", - "Topic :: Internet :: WWW/HTTP :: WSGI", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - "Topic :: Software Development :: Libraries :: Application Frameworks", - "Typing :: Typed", -] -requires-python = ">=3.10" -dependencies = [ - "blinker>=1.9.0", - "click>=8.1.3", - "itsdangerous>=2.2.0", - "jinja2>=3.1.2", - "markupsafe>=2.1.1", - "werkzeug>=3.1.0", -] - -[project.optional-dependencies] -async = ["asgiref>=3.2"] -dotenv = ["python-dotenv"] - -[dependency-groups] -dev = [ - "ruff", - "tox", - "tox-uv", -] -docs = [ - "pallets-sphinx-themes", - "sphinx<9", - "sphinx-tabs", - "sphinxcontrib-log-cabinet", -] -docs-auto = [ - "sphinx-autobuild", -] -gha-update = [ - "gha-update ; python_full_version >= '3.12'", -] -pre-commit = [ - "pre-commit", - "pre-commit-uv", -] -tests = [ - "asgiref", - "pytest", - "python-dotenv", -] -typing = [ - "asgiref", - "cryptography", - "mypy", - "pyright", - "pytest", - "python-dotenv", - "types-contextvars", - "types-dataclasses", -] - -[project.urls] -Donate = "https://palletsprojects.com/donate" -Documentation = "https://flask.palletsprojects.com/" -Changes = "https://flask.palletsprojects.com/page/changes/" -Source = "https://github.com/pallets/flask/" -Chat = "https://discord.gg/pallets" - -[project.scripts] -flask = "flask.cli:main" - -[build-system] -requires = ["flit_core>=3.11,<4"] -build-backend = "flit_core.buildapi" - -[tool.flit.module] -name = "flask" - -[tool.flit.sdist] -include = [ - "docs/", - "examples/", - "tests/", - "CHANGES.rst", - "uv.lock" -] -exclude = [ - "docs/_build/", -] - -[tool.uv] -default-groups = ["dev", "pre-commit", "tests", "typing"] - -[tool.pytest.ini_options] -testpaths = ["tests"] -filterwarnings = [ - "error", -] - -[tool.coverage.run] -branch = true -source = ["flask", "tests"] - -[tool.coverage.paths] -source = ["src", "*/site-packages"] - -[tool.coverage.report] -exclude_also = [ - "if t.TYPE_CHECKING", - "raise NotImplementedError", - ": \\.{3}", -] - -[tool.mypy] -python_version = "3.10" -files = ["src", "tests/type_check"] -show_error_codes = true -pretty = true -strict = true - -[[tool.mypy.overrides]] -module = [ - "asgiref.*", - "dotenv.*", - "cryptography.*", - "importlib_metadata", -] -ignore_missing_imports = true - -[tool.pyright] -pythonVersion = "3.10" -include = ["src", "tests/type_check"] -typeCheckingMode = "basic" - -[tool.ruff] -src = ["src"] -fix = true -show-fixes = true -output-format = "full" - -[tool.ruff.lint] -select = [ - "B", # flake8-bugbear - "E", # pycodestyle error - "F", # pyflakes - "I", # isort - "UP", # pyupgrade - "W", # pycodestyle warning -] - -[tool.ruff.lint.isort] -force-single-line = true -order-by-type = false - -[tool.codespell] -ignore-words-list = "te" - -[tool.tox] -env_list = [ - "py3.14", "py3.14t", - "py3.13", "py3.12", "py3.11", "py3.10", - "pypy3.11", - "tests-min", "tests-dev", - "style", - "typing", - "docs", -] - -[tool.tox.env_run_base] -description = "pytest on latest dependency versions" -runner = "uv-venv-lock-runner" -package = "wheel" -wheel_build_env = ".pkg" -constrain_package_deps = true -use_frozen_constraints = true -dependency_groups = ["tests"] -env_tmp_dir = "{toxworkdir}/tmp/{envname}" -commands = [[ - "pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}", - {replace = "posargs", default = [], extend = true}, -]] - -[tool.tox.env.tests-min] -description = "pytest on minimum dependency versions" -base_python = ["3.14"] -commands = [ - [ - "uv", "pip", "install", - "blinker==1.9.0", - "click==8.1.3", - "itsdangerous==2.2.0", - "jinja2==3.1.2", - "markupsafe==2.1.1", - "werkzeug==3.1.0", - ], - [ - "pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}", - {replace = "posargs", default = [], extend = true}, - ], -] - -[tool.tox.env.tests-dev] -description = "pytest on development dependency versions (git main branch)" -base_python = ["3.10"] -commands = [ - [ - "uv", "pip", "install", - "https://github.com/pallets-eco/blinker/archive/refs/heads/main.tar.gz", - "https://github.com/pallets/click/archive/refs/heads/main.tar.gz", - "https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz", - "https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz", - "https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz", - "https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz", - ], - [ - "pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}", - {replace = "posargs", default = [], extend = true}, - ], -] - -[tool.tox.env.style] -description = "run all pre-commit hooks on all files" -dependency_groups = ["pre-commit"] -skip_install = true -commands = [["pre-commit", "run", "--all-files"]] - -[tool.tox.env.typing] -description = "run static type checkers" -dependency_groups = ["typing"] -commands = [ - ["mypy"], - ["pyright"], -] - -[tool.tox.env.docs] -description = "build docs" -dependency_groups = ["docs"] -commands = [["sphinx-build", "-E", "-W", "-b", "dirhtml", "docs", "docs/_build/dirhtml"]] - -[tool.tox.env.docs-auto] -description = "continuously rebuild docs and start a local server" -dependency_groups = ["docs", "docs-auto"] -commands = [["sphinx-autobuild", "-W", "-b", "dirhtml", "--watch", "src", "docs", "docs/_build/dirhtml"]] - -[tool.tox.env.update-actions] -description = "update GitHub Actions pins" -labels = ["update"] -dependency_groups = ["gha-update"] -skip_install = true -commands = [["gha-update"]] - -[tool.tox.env.update-pre_commit] -description = "update pre-commit pins" -labels = ["update"] -dependency_groups = ["pre-commit"] -skip_install = true -commands = [["pre-commit", "autoupdate", "--freeze", "-j4"]] - -[tool.tox.env.update-requirements] -description = "update uv lock" -labels = ["update"] -dependency_groups = [] -no_default_groups = true -skip_install = true -commands = [["uv", "lock", {replace = "posargs", default = ["-U"], extend = true}]] diff --git a/run-tests.py b/run-tests.py new file mode 100644 index 00000000..c1345848 --- /dev/null +++ b/run-tests.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +import sys, os +sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) +from flask.testsuite import main +main() diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py new file mode 100644 index 00000000..4027d8ce --- /dev/null +++ b/scripts/flask-07-upgrade.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + flask-07-upgrade + ~~~~~~~~~~~~~~~~ + + This command line script scans a whole application tree and attempts to + output an unified diff with all the changes that are necessary to easily + upgrade the application to 0.7 and to not yield deprecation warnings. + + This will also attempt to find `after_request` functions that don't modify + the response and appear to be better suited for `teardown_request`. + + This application is indeed an incredible hack, but because what it + attempts to accomplish is impossible to do statically it tries to support + the most common patterns at least. The diff it generates should be + hand reviewed and not applied blindly without making backups. + + :copyright: (c) Copyright 2011 by Armin Ronacher. + :license: see LICENSE for more details. +""" +import re +import os +import inspect +import difflib +import posixpath +from optparse import OptionParser + +try: + import ast +except ImportError: + ast = None + + +TEMPLATE_LOOKAHEAD = 4096 + +_app_re_part = r'((?:[a-zA-Z_][a-zA-Z0-9_]*app)|app|application)' +_string_re_part = r"('([^'\\]*(?:\\.[^'\\]*)*)'" \ + r'|"([^"\\]*(?:\\.[^"\\]*)*)")' + +_from_import_re = re.compile(r'^\s*from flask import\s+') +_url_for_re = re.compile(r'\b(url_for\()(%s)' % _string_re_part) +_render_template_re = re.compile(r'\b(render_template\()(%s)' % _string_re_part) +_after_request_re = re.compile(r'((?:@\S+\.(?:app_)?))(after_request)(\b\s*$)(?m)') +_module_constructor_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\s*=\s*Module' + r'\(__name__\s*(?:,\s*(?:name\s*=\s*)?(%s))?' % + _string_re_part) +_error_handler_re = re.compile(r'%s\.error_handlers\[\s*(\d+)\s*\]' % _app_re_part) +_mod_route_re = re.compile(r'@([a-zA-Z0-9_][a-zA-Z0-9_]*)\.route') +_blueprint_related = [ + (re.compile(r'request\.module'), 'request.blueprint'), + (re.compile(r'register_module'), 'register_blueprint'), + (re.compile(r'%s\.modules' % _app_re_part), '\\1.blueprints') +] + + +def make_diff(filename, old, new): + for line in difflib.unified_diff(old.splitlines(), new.splitlines(), + posixpath.normpath(posixpath.join('a', filename)), + posixpath.normpath(posixpath.join('b', filename)), + lineterm=''): + print line + + +def looks_like_teardown_function(node): + returns = [x for x in ast.walk(node) if isinstance(x, ast.Return)] + if len(returns) != 1: + return + return_def = returns[0] + resp_name = node.args.args[0] + if not isinstance(return_def.value, ast.Name) or \ + return_def.value.id != resp_name.id: + return + + for body_node in node.body: + for child in ast.walk(body_node): + if isinstance(child, ast.Name) and \ + child.id == resp_name.id: + if child is not return_def.value: + return + + return resp_name.id + + +def fix_url_for(contents, module_declarations=None): + if module_declarations is None: + skip_module_test = True + else: + skip_module_test = False + mapping = dict(module_declarations) + annotated_lines = [] + + def make_line_annotations(): + if not annotated_lines: + last_index = 0 + for line in contents.splitlines(True): + last_index += len(line) + annotated_lines.append((last_index, line)) + + def backtrack_module_name(call_start): + make_line_annotations() + for idx, (line_end, line) in enumerate(annotated_lines): + if line_end > call_start: + for _, line in reversed(annotated_lines[:idx]): + match = _mod_route_re.search(line) + if match is not None: + shortname = match.group(1) + return mapping.get(shortname) + + def handle_match(match): + if not skip_module_test: + modname = backtrack_module_name(match.start()) + if modname is None: + return match.group(0) + prefix = match.group(1) + endpoint = ast.literal_eval(match.group(2)) + if endpoint.startswith('.'): + endpoint = endpoint[1:] + elif '.' not in endpoint: + endpoint = '.' + endpoint + else: + return match.group(0) + return prefix + repr(endpoint) + return _url_for_re.sub(handle_match, contents) + + +def fix_teardown_funcs(contents): + + def is_return_line(line): + args = line.strip().split() + return args and args[0] == 'return' + + def fix_single(match, lines, lineno): + if not lines[lineno + 1].startswith('def'): + return + block_lines = inspect.getblock(lines[lineno + 1:]) + func_code = ''.join(block_lines) + if func_code[0].isspace(): + node = ast.parse('if 1:\n' + func_code).body[0].body + else: + node = ast.parse(func_code).body[0] + response_param_name = looks_like_teardown_function(node) + if response_param_name is None: + return + before = lines[:lineno] + decorator = [match.group(1) + + match.group(2).replace('after_', 'teardown_') + + match.group(3)] + body = [line.replace(response_param_name, 'exception') + for line in block_lines if + not is_return_line(line)] + after = lines[lineno + len(block_lines) + 1:] + return before + decorator + body + after + + content_lines = contents.splitlines(True) + while 1: + found_one = False + for idx, line in enumerate(content_lines): + match = _after_request_re.match(line) + if match is None: + continue + new_content_lines = fix_single(match, content_lines, idx) + if new_content_lines is not None: + content_lines = new_content_lines + break + else: + break + + return ''.join(content_lines) + + +def get_module_autoname(filename): + directory, filename = os.path.split(filename) + if filename != '__init__.py': + return os.path.splitext(filename)[0] + return os.path.basename(directory) + + +def rewrite_from_imports(prefix, fromlist, lineiter): + import_block = [prefix, fromlist] + if fromlist[0] == '(' and fromlist[-1] != ')': + for line in lineiter: + import_block.append(line) + if line.rstrip().endswith(')'): + break + elif fromlist[-1] == '\\': + for line in lineiter: + import_block.append(line) + if line.rstrip().endswith('\\'): + break + + return ''.join(import_block).replace('Module', 'Blueprint') + + +def rewrite_blueprint_imports(contents): + new_file = [] + lineiter = iter(contents.splitlines(True)) + for line in lineiter: + match = _from_import_re.search(line) + if match is not None: + new_file.extend(rewrite_from_imports(match.group(), + line[match.end():], + lineiter)) + else: + new_file.append(line) + return ''.join(new_file) + + +def rewrite_for_blueprints(contents, filename): + modules_declared = [] + def handle_match(match): + target = match.group(1) + name_param = match.group(2) + if name_param is None: + modname = get_module_autoname(filename) + else: + modname = ast.literal_eval(name_param) + modules_declared.append((target, modname)) + return '%s = %s' % (target, 'Blueprint(%r, __name__' % modname) + new_contents = _module_constructor_re.sub(handle_match, contents) + + if modules_declared: + new_contents = rewrite_blueprint_imports(new_contents) + + for pattern, replacement in _blueprint_related: + new_contents = pattern.sub(replacement, new_contents) + return new_contents, dict(modules_declared) + + +def upgrade_python_file(filename, contents, teardown): + new_contents = contents + if teardown: + new_contents = fix_teardown_funcs(new_contents) + new_contents, modules = rewrite_for_blueprints(new_contents, filename) + new_contents = fix_url_for(new_contents, modules) + new_contents = _error_handler_re.sub('\\1.error_handler_spec[None][\\2]', + new_contents) + make_diff(filename, contents, new_contents) + + +def upgrade_template_file(filename, contents): + new_contents = fix_url_for(contents, None) + make_diff(filename, contents, new_contents) + + +def walk_path(path): + this_file = os.path.realpath(__file__).rstrip('c') + for dirpath, dirnames, filenames in os.walk(path): + dirnames[:] = [x for x in dirnames if not x.startswith('.')] + for filename in filenames: + filename = os.path.join(dirpath, filename) + if os.path.realpath(filename) == this_file: + continue + if filename.endswith('.py'): + yield filename, 'python' + # skip files that are diffs. These might be false positives + # when run multiple times. + elif not filename.endswith(('.diff', '.patch', '.udiff')): + with open(filename) as f: + contents = f.read(TEMPLATE_LOOKAHEAD) + if '{% for' or '{% if' or '{{ url_for' in contents: + yield filename, 'template' + + +def scan_path(path=None, teardown=True): + for filename, type in walk_path(path): + with open(filename) as f: + contents = f.read() + if type == 'python': + upgrade_python_file(filename, contents, teardown) + elif type == 'template': + upgrade_template_file(filename, contents) + + +def main(): + """Entrypoint""" + parser = OptionParser(usage='%prog [options] [paths]') + parser.add_option('-T', '--no-teardown-detection', dest='no_teardown', + action='store_true', help='Do not attempt to ' + 'detect teardown function rewrites.') + parser.add_option('-b', '--bundled-templates', dest='bundled_tmpl', + action='store_true', help='Indicate to the system ' + 'that templates are bundled with modules. Default ' + 'is auto detect.') + options, args = parser.parse_args() + if not args: + args = ['.'] + + if ast is None: + parser.error('Python 2.6 or later is required to run the upgrade script.\n' + 'The runtime requirements for Flask 0.7 however are still ' + 'Python 2.5.') + + for path in args: + scan_path(path, teardown=not options.no_teardown) + + +if __name__ == '__main__': + main() diff --git a/scripts/flaskext_compat.py b/scripts/flaskext_compat.py new file mode 100644 index 00000000..40c8c6b5 --- /dev/null +++ b/scripts/flaskext_compat.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +""" + flaskext_compat + ~~~~~~~~~~~~~~~ + + Implements the ``flask.ext`` virtual package for versions of Flask + older than 0.7. This module is a noop if Flask 0.8 was detected. + + Usage:: + + import flaskext_compat + from flask.ext import foo + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import sys +import os +import imp + + +class ExtensionImporter(object): + """This importer redirects imports from this submodule to other locations. + This makes it possible to transition from the old flaskext.name to the + newer flask_name without people having a hard time. + """ + + def __init__(self, module_choices, wrapper_module): + self.module_choices = module_choices + self.wrapper_module = wrapper_module + self.prefix = wrapper_module + '.' + self.prefix_cutoff = wrapper_module.count('.') + 1 + + def __eq__(self, other): + return self.__class__.__module__ == other.__class__.__module__ and \ + self.__class__.__name__ == other.__class__.__name__ and \ + self.wrapper_module == other.wrapper_module and \ + self.module_choices == other.module_choices + + def __ne__(self, other): + return not self.__eq__(other) + + def install(self): + sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] + + def find_module(self, fullname, path=None): + if fullname.startswith(self.prefix): + return self + + def load_module(self, fullname): + if fullname in sys.modules: + return sys.modules[fullname] + modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] + for path in self.module_choices: + realname = path % modname + try: + __import__(realname) + except ImportError: + exc_type, exc_value, tb = sys.exc_info() + # since we only establish the entry in sys.modules at the + # very this seems to be redundant, but if recursive imports + # happen we will call into the move import a second time. + # On the second invocation we still don't have an entry for + # fullname in sys.modules, but we will end up with the same + # fake module name and that import will succeed since this + # one already has a temporary entry in the modules dict. + # Since this one "succeeded" temporarily that second + # invocation now will have created a fullname entry in + # sys.modules which we have to kill. + sys.modules.pop(fullname, None) + + # If it's an important traceback we reraise it, otherwise + # we swallow it and try the next choice. The skipped frame + # is the one from __import__ above which we don't care about + if self.is_important_traceback(realname, tb): + raise exc_type, exc_value, tb.tb_next + continue + module = sys.modules[fullname] = sys.modules[realname] + if '.' not in modname: + setattr(sys.modules[self.wrapper_module], modname, module) + return module + raise ImportError('No module named %s' % fullname) + + def is_important_traceback(self, important_module, tb): + """Walks a traceback's frames and checks if any of the frames + originated in the given important module. If that is the case then we + were able to import the module itself but apparently something went + wrong when the module was imported. (Eg: import of an import failed). + """ + while tb is not None: + if self.is_important_frame(important_module, tb): + return True + tb = tb.tb_next + return False + + def is_important_frame(self, important_module, tb): + """Checks a single frame if it's important.""" + g = tb.tb_frame.f_globals + if '__name__' not in g: + return False + + module_name = g['__name__'] + + # Python 2.7 Behavior. Modules are cleaned up late so the + # name shows up properly here. Success! + if module_name == important_module: + return True + + # Some python verisons will will clean up modules so early that the + # module name at that point is no longer set. Try guessing from + # the filename then. + filename = os.path.abspath(tb.tb_frame.f_code.co_filename) + test_string = os.path.sep + important_module.replace('.', os.path.sep) + return test_string + '.py' in filename or \ + test_string + os.path.sep + '__init__.py' in filename + + +def activate(): + import flask + ext_module = imp.new_module('flask.ext') + ext_module.__path__ = [] + flask.ext = sys.modules['flask.ext'] = ext_module + importer = ExtensionImporter(['flask_%s', 'flaskext.%s'], 'flask.ext') + importer.install() diff --git a/scripts/flaskext_test.py b/scripts/flaskext_test.py new file mode 100644 index 00000000..d1d5d991 --- /dev/null +++ b/scripts/flaskext_test.py @@ -0,0 +1,311 @@ +# -*- coding: utf-8 -*- +""" + Flask Extension Tests + ~~~~~~~~~~~~~~~~~~~~~ + + Tests the Flask extensions. + + :copyright: (c) 2010 by Ali Afshar. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import os +import sys +import shutil +import urllib2 +import tempfile +import subprocess +import argparse + +from flask import json + +from setuptools.package_index import PackageIndex +from setuptools.archive_util import unpack_archive + +flask_svc_url = 'http://flask.pocoo.org/extensions/' + + +# OS X has awful paths when using mkstemp or gettempdir(). I don't +# care about security or clashes here, so pick something that is +# actually rememberable. +if sys.platform == 'darwin': + _tempdir = '/private/tmp' +else: + _tempdir = tempfile.gettempdir() +tdir = _tempdir + '/flaskext-test' +flaskdir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + + +# virtualenv hack *cough* +os.environ['PYTHONDONTWRITEBYTECODE'] = '' + + +RESULT_TEMPATE = u'''\ + +Flask-Extension Test Results + +

Flask-Extension Test Results

+

+ This page contains the detailed test results for the test run of + all {{ 'approved' if approved }} Flask extensions. +

Summary

+ + + + + + + {%- for result in results %} + {% set outcome = 'success' if result.success else 'failed' %} + + + {%- endfor %} + +
Extension + Version + Author + License + Outcome + {%- for iptr, _ in results[0].logs|dictsort %} + {{ iptr }} + {%- endfor %} +
{{ result.name }} + {{ result.version }} + {{ result.author }} + {{ result.license }} + {{ outcome }} + {%- for iptr, _ in result.logs|dictsort %} + see log + {%- endfor %} +
+

Test Logs

+

Detailed test logs for all tests on all platforms: +{%- for result in results %} + {%- for iptr, log in result.logs|dictsort %} +

+ {{ result.name }} - {{ result.version }} [{{ iptr }}]

+
{{ log }}
+ {%- endfor %} +{%- endfor %} +''' + + +def log(msg, *args): + print '[EXTTEST]', msg % args + + +class TestResult(object): + + def __init__(self, name, folder, statuscode, interpreters): + intrptr = os.path.join(folder, '.tox/%s/bin/python' + % interpreters[0]) + self.statuscode = statuscode + self.folder = folder + self.success = statuscode == 0 + + def fetch(field): + try: + c = subprocess.Popen([intrptr, 'setup.py', + '--' + field], cwd=folder, + stdout=subprocess.PIPE) + return c.communicate()[0].strip() + except OSError: + return '?' + self.name = name + self.license = fetch('license') + self.author = fetch('author') + self.version = fetch('version') + + self.logs = {} + for interpreter in interpreters: + logfile = os.path.join(folder, '.tox/%s/log/test.log' + % interpreter) + if os.path.isfile(logfile): + self.logs[interpreter] = open(logfile).read() + else: + self.logs[interpreter] = '' + + +def create_tdir(): + try: + shutil.rmtree(tdir) + except Exception: + pass + os.mkdir(tdir) + + +def package_flask(): + distfolder = tdir + '/.flask-dist' + c = subprocess.Popen(['python', 'setup.py', 'sdist', '--formats=gztar', + '--dist', distfolder], cwd=flaskdir) + c.wait() + return os.path.join(distfolder, os.listdir(distfolder)[0]) + + +def get_test_command(checkout_dir): + if os.path.isfile(checkout_dir + '/Makefile'): + return 'make test' + return 'python setup.py test' + + +def fetch_extensions_list(): + req = urllib2.Request(flask_svc_url, headers={'accept':'application/json'}) + d = urllib2.urlopen(req).read() + data = json.loads(d) + for ext in data['extensions']: + yield ext + + +def checkout_extension(name): + log('Downloading extension %s to temporary folder', name) + root = os.path.join(tdir, name) + os.mkdir(root) + checkout_path = PackageIndex().download(name, root) + + unpack_archive(checkout_path, root) + path = None + for fn in os.listdir(root): + path = os.path.join(root, fn) + if os.path.isdir(path): + break + log('Downloaded to %s', path) + return path + + +tox_template = """[tox] +envlist=%(env)s + +[testenv] +deps= + %(deps)s + distribute + py +commands=bash flaskext-runtest.sh {envlogdir}/test.log +downloadcache=%(cache)s +""" + + +def create_tox_ini(checkout_path, interpreters, flask_dep): + tox_path = os.path.join(checkout_path, 'tox-flask-test.ini') + if not os.path.exists(tox_path): + with open(tox_path, 'w') as f: + f.write(tox_template % { + 'env': ','.join(interpreters), + 'cache': tdir, + 'deps': flask_dep + }) + return tox_path + + +def iter_extensions(only_approved=True): + for ext in fetch_extensions_list(): + if ext['approved'] or not only_approved: + yield ext['name'] + + +def test_extension(name, interpreters, flask_dep): + checkout_path = checkout_extension(name) + log('Running tests with tox in %s', checkout_path) + + # figure out the test command and write a wrapper script. We + # can't write that directly into the tox ini because tox does + # not invoke the command from the shell so we have no chance + # to pipe the output into a logfile. The /dev/null hack is + # to trick py.test (if used) into not guessing widths from the + # invoking terminal. + test_command = get_test_command(checkout_path) + log('Test command: %s', test_command) + f = open(checkout_path + '/flaskext-runtest.sh', 'w') + f.write(test_command + ' &> "$1" < /dev/null\n') + f.close() + + # if there is a tox.ini, remove it, it will cause troubles + # for us. Remove it if present, we are running tox ourselves + # afterall. + + create_tox_ini(checkout_path, interpreters, flask_dep) + rv = subprocess.call(['tox', '-c', 'tox-flask-test.ini'], cwd=checkout_path) + return TestResult(name, checkout_path, rv, interpreters) + + +def run_tests(extensions, interpreters): + results = {} + create_tdir() + log('Packaging Flask') + flask_dep = package_flask() + log('Running extension tests') + log('Temporary Environment: %s', tdir) + for name in extensions: + log('Testing %s', name) + result = test_extension(name, interpreters, flask_dep) + if result.success: + log('Extension test succeeded') + else: + log('Extension test failed') + results[name] = result + return results + + +def render_results(results, approved): + from jinja2 import Template + items = results.values() + items.sort(key=lambda x: x.name.lower()) + rv = Template(RESULT_TEMPATE, autoescape=True).render(results=items, + approved=approved) + fd, filename = tempfile.mkstemp(suffix='.html') + os.fdopen(fd, 'w').write(rv.encode('utf-8') + '\n') + return filename + + +def main(): + parser = argparse.ArgumentParser(description='Runs Flask extension tests') + parser.add_argument('--all', dest='all', action='store_true', + help='run against all extensions, not just approved') + parser.add_argument('--browse', dest='browse', action='store_true', + help='show browser with the result summary') + parser.add_argument('--env', dest='env', default='py25,py26,py27', + help='the tox environments to run against') + parser.add_argument('--extension=', dest='extension', default=None, + help='tests a single extension') + args = parser.parse_args() + + if args.extension is not None: + only_approved = False + extensions = [args.extension] + else: + only_approved = not args.all + extensions = iter_extensions(only_approved) + + results = run_tests(extensions, [x.strip() for x in args.env.split(',')]) + filename = render_results(results, only_approved) + if args.browse: + import webbrowser + webbrowser.open('file:///' + filename.lstrip('/')) + print 'Results written to', filename + + +if __name__ == '__main__': + main() diff --git a/scripts/make-release.py b/scripts/make-release.py new file mode 100644 index 00000000..31f4fa9e --- /dev/null +++ b/scripts/make-release.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + make-release + ~~~~~~~~~~~~ + + Helper script that performs a release. Does pretty much everything + automatically for us. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import sys +import os +import re +from datetime import datetime, date +from subprocess import Popen, PIPE + +_date_clean_re = re.compile(r'(\d+)(st|nd|rd|th)') + + +def parse_changelog(): + with open('CHANGES') as f: + lineiter = iter(f) + for line in lineiter: + match = re.search('^Version\s+(.*)', line.strip()) + if match is None: + continue + length = len(match.group(1)) + version = match.group(1).strip() + if lineiter.next().count('-') != len(match.group(0)): + continue + while 1: + change_info = lineiter.next().strip() + if change_info: + break + + match = re.search(r'released on (\w+\s+\d+\w+\s+\d+)' + r'(?:, codename (.*))?(?i)', change_info) + if match is None: + continue + + datestr, codename = match.groups() + return version, parse_date(datestr), codename + + +def bump_version(version): + try: + parts = map(int, version.split('.')) + except ValueError: + fail('Current version is not numeric') + parts[-1] += 1 + return '.'.join(map(str, parts)) + + +def parse_date(string): + string = _date_clean_re.sub(r'\1', string) + return datetime.strptime(string, '%B %d %Y') + + +def set_filename_version(filename, version_number, pattern): + changed = [] + def inject_version(match): + before, old, after = match.groups() + changed.append(True) + return before + version_number + after + with open(filename) as f: + contents = re.sub(r"^(\s*%s\s*=\s*')(.+?)(')(?sm)" % pattern, + inject_version, f.read()) + + if not changed: + fail('Could not find %s in %s', pattern, filename) + + with open(filename, 'w') as f: + f.write(contents) + + +def set_init_version(version): + info('Setting __init__.py version to %s', version) + set_filename_version('flask/__init__.py', version, '__version__') + + +def set_setup_version(version): + info('Setting setup.py version to %s', version) + set_filename_version('setup.py', version, 'version') + + +def build_and_upload(): + Popen([sys.executable, 'setup.py', 'release', 'sdist', 'upload']).wait() + + +def fail(message, *args): + print >> sys.stderr, 'Error:', message % args + sys.exit(1) + + +def info(message, *args): + print >> sys.stderr, message % args + + +def get_git_tags(): + return set(Popen(['git', 'tag'], stdout=PIPE).communicate()[0].splitlines()) + + +def git_is_clean(): + return Popen(['git', 'diff', '--quiet']).wait() == 0 + + +def make_git_commit(message, *args): + message = message % args + Popen(['git', 'commit', '-am', message]).wait() + + +def make_git_tag(tag): + info('Tagging "%s"', tag) + Popen(['git', 'tag', tag]).wait() + + +def main(): + os.chdir(os.path.join(os.path.dirname(__file__), '..')) + + rv = parse_changelog() + if rv is None: + fail('Could not parse changelog') + + version, release_date, codename = rv + dev_version = bump_version(version) + '-dev' + + info('Releasing %s (codename %s, release date %s)', + version, codename, release_date.strftime('%d/%m/%Y')) + tags = get_git_tags() + + if version in tags: + fail('Version "%s" is already tagged', version) + if release_date.date() != date.today(): + fail('Release date is not today (%s != %s)') + + if not git_is_clean(): + fail('You have uncommitted changes in git') + + set_init_version(version) + set_setup_version(version) + make_git_commit('Bump version number to %s', version) + make_git_tag(version) + build_and_upload() + set_init_version(dev_version) + set_setup_version(dev_version) + + +if __name__ == '__main__': + main() diff --git a/scripts/testproj/templates/index.html b/scripts/testproj/templates/index.html new file mode 100644 index 00000000..42626af7 --- /dev/null +++ b/scripts/testproj/templates/index.html @@ -0,0 +1,2 @@ +{{ url_for('static', filename='test.css') }} +{{ url_for('foo.static', filename='test.css') }} diff --git a/scripts/testproj/test.py b/scripts/testproj/test.py new file mode 100644 index 00000000..8c0f16ae --- /dev/null +++ b/scripts/testproj/test.py @@ -0,0 +1,46 @@ +from flask import Flask, Module, render_template, url_for + + +mod = Module(__name__) +mod2 = Module(__name__, 'testmod2') +mod3 = Module(__name__, name='somemod', subdomain='meh') + + +@app.after_request +def after_request(response): + g.db.close() + return response + + +@app.route('/') +def index_foo(): + x1 = url_for('somemod.index') + x2 = url_for('.index') + return render_template('test/index.html') + + +@mod.route('/') +def index(): + x1 = url_for('somemod.index') + x2 = url_for('.index') + return render_template('test/index.html') + + +@mod2.route('/') +def mod2_index(): + return render_template('testmod2/index.html') + + +@mod3.route('/') +def mod3_index(): + return render_template('something-else/index.html') + + +app = Flask(__name__) +app.register_module(mod) +app.register_module(mod2) + + +def handle_404(error): + return 'Testing', 404 +app.error_handlers[404] = handle_404 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..2d74c58f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,6 @@ +[egg_info] +tag_build = dev +tag_date = true + +[aliases] +release = egg_info -RDb '' diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..2aad7b57 --- /dev/null +++ b/setup.py @@ -0,0 +1,108 @@ +""" +Flask +----- + +Flask is a microframework for Python based on Werkzeug, Jinja 2 and good +intentions. And before you ask: It's BSD licensed! + +Flask is Fun +```````````` + +:: + + from flask import Flask + app = Flask(__name__) + + @app.route("/") + def hello(): + return "Hello World!" + + if __name__ == "__main__": + app.run() + +And Easy to Setup +````````````````` + +:: + + $ easy_install Flask + $ python hello.py + * Running on http://localhost:5000/ + +Links +````` + +* `website `_ +* `documentation `_ +* `development version + `_ + +""" +from setuptools import Command, setup + +class run_audit(Command): + """Audits source code using PyFlakes for following issues: + - Names which are used but not defined or used before they are defined. + - Names which are redefined without having been used. + """ + description = "Audit source code with PyFlakes" + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + import os, sys + try: + import pyflakes.scripts.pyflakes as flakes + except ImportError: + print "Audit requires PyFlakes installed in your system.""" + sys.exit(-1) + + warns = 0 + # Define top-level directories + dirs = ('flask', 'examples', 'scripts') + for dir in dirs: + for root, _, files in os.walk(dir): + for file in files: + if file != '__init__.py' and file.endswith('.py') : + warns += flakes.checkPath(os.path.join(root, file)) + if warns > 0: + print "Audit finished with total %d warnings." % warns + else: + print "No problems found in sourcecode." + +setup( + name='Flask', + version='0.8.1', + url='http://github.com/mitsuhiko/flask/', + license='BSD', + author='Armin Ronacher', + author_email='armin.ronacher@active-4.com', + description='A microframework based on Werkzeug, Jinja2 ' + 'and good intentions', + long_description=__doc__, + packages=['flask', 'flask.ext', 'flask.testsuite'], + include_package_data=True, + zip_safe=False, + platforms='any', + install_requires=[ + 'Werkzeug>=0.6.1', + 'Jinja2>=2.4' + ], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Libraries :: Python Modules' + ], + cmdclass={'audit': run_audit}, + test_suite='flask.testsuite.suite' +) diff --git a/src/flask/__init__.py b/src/flask/__init__.py deleted file mode 100644 index 30dce6fd..00000000 --- a/src/flask/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -from . import json as json -from .app import Flask as Flask -from .blueprints import Blueprint as Blueprint -from .config import Config as Config -from .ctx import after_this_request as after_this_request -from .ctx import copy_current_request_context as copy_current_request_context -from .ctx import has_app_context as has_app_context -from .ctx import has_request_context as has_request_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 .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_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 -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 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 template_rendered as template_rendered -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 .wrappers import Request as Request -from .wrappers import Response as Response diff --git a/src/flask/__main__.py b/src/flask/__main__.py deleted file mode 100644 index 4e28416e..00000000 --- a/src/flask/__main__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .cli import main - -main() diff --git a/src/flask/app.py b/src/flask/app.py deleted file mode 100644 index 652b9bbf..00000000 --- a/src/flask/app.py +++ /dev/null @@ -1,1625 +0,0 @@ -from __future__ import annotations - -import collections.abc as cabc -import inspect -import os -import sys -import typing as t -import weakref -from datetime import timedelta -from functools import update_wrapper -from inspect import iscoroutinefunction -from itertools import chain -from types import TracebackType -from urllib.parse import quote as _url_quote - -import click -from werkzeug.datastructures import Headers -from werkzeug.datastructures import ImmutableDict -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 RequestRedirect -from werkzeug.routing import RoutingException -from werkzeug.routing import Rule -from werkzeug.serving import is_running_from_reloader -from werkzeug.wrappers import Response as BaseResponse -from werkzeug.wsgi import get_host - -from . import cli -from . import typing as ft -from .ctx import AppContext -from .globals import _cv_app -from .globals import app_ctx -from .globals import g -from .globals import request -from .globals import session -from .helpers import _CollectErrors -from .helpers import get_debug_flag -from .helpers import get_flashed_messages -from .helpers import get_load_dotenv -from .helpers import send_from_directory -from .sansio.app import App -from .sessions import SecureCookieSessionInterface -from .sessions import SessionInterface -from .signals import appcontext_tearing_down -from .signals import got_request_exception -from .signals import request_finished -from .signals import request_started -from .signals import request_tearing_down -from .templating import Environment -from .wrappers import Request -from .wrappers import Response - -if t.TYPE_CHECKING: # pragma: no cover - from _typeshed.wsgi import StartResponse - from _typeshed.wsgi import WSGIEnvironment - - from .testing import FlaskClient - from .testing import FlaskCliRunner - from .typing import HeadersValue - -T_shell_context_processor = t.TypeVar( - "T_shell_context_processor", bound=ft.ShellContextProcessorCallable -) -T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) -T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) -T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) -T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) - - -def _make_timedelta(value: timedelta | int | None) -> timedelta | None: - if value is None or isinstance(value, timedelta): - return value - - return timedelta(seconds=value) - - -F = t.TypeVar("F", bound=t.Callable[..., t.Any]) - - -# Other methods may call the overridden method with the new ctx arg. Remove it -# and call the method with the remaining args. -def remove_ctx(f: F) -> F: - def wrapper(self: Flask, *args: t.Any, **kwargs: t.Any) -> t.Any: - if args and isinstance(args[0], AppContext): - args = args[1:] - - return f(self, *args, **kwargs) - - return update_wrapper(wrapper, f) # type: ignore[return-value] - - -# The overridden method may call super().base_method without the new ctx arg. -# Add it to the args for the call. -def add_ctx(f: F) -> F: - def wrapper(self: Flask, *args: t.Any, **kwargs: t.Any) -> t.Any: - if not args: - args = (app_ctx._get_current_object(),) - elif not isinstance(args[0], AppContext): - args = (app_ctx._get_current_object(), *args) - - return f(self, *args, **kwargs) - - return update_wrapper(wrapper, f) # type: ignore[return-value] - - -class Flask(App): - """The flask object implements a WSGI application and acts as the central - object. It is passed the name of the module or package of the - application. Once it is created it will act as a central registry for - the view functions, the URL rules, template configuration and much more. - - The name of the package is used to resolve resources from inside the - package or the folder the module is contained in depending on if the - package parameter resolves to an actual python package (a folder with - an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). - - For more information about resource loading, see :func:`open_resource`. - - Usually you create a :class:`Flask` instance in your main module or - in the :file:`__init__.py` file of your package like this:: - - from flask import Flask - app = Flask(__name__) - - .. admonition:: About the First Parameter - - The idea of the first parameter is to give Flask an idea of what - belongs to your application. This name is used to find resources - on the filesystem, can be used by extensions to improve debugging - information and a lot more. - - So it's important what you provide there. If you are using a single - module, `__name__` is always the correct value. If you however are - using a package, it's usually recommended to hardcode the name of - your package there. - - For example if your application is defined in :file:`yourapplication/app.py` - you should create it with one of the two versions below:: - - app = Flask('yourapplication') - app = Flask(__name__.split('.')[0]) - - Why is that? The application will work even with `__name__`, thanks - to how resources are looked up. However it will make debugging more - painful. Certain extensions can make assumptions based on the - import name of your application. For example the Flask-SQLAlchemy - extension will look for the code in your application that triggered - an SQL query in debug mode. If the import name is not properly set - up, that debugging information is lost. (For example it would only - pick up SQL queries in `yourapplication.app` and not - `yourapplication.views.frontend`) - - .. versionadded:: 0.7 - The `static_url_path`, `static_folder`, and `template_folder` - parameters were added. - - .. versionadded:: 0.8 - The `instance_path` and `instance_relative_config` parameters were - added. - - .. versionadded:: 0.11 - The `root_path` parameter was added. - - .. versionadded:: 1.0 - The ``host_matching`` and ``static_host`` parameters were added. - - .. versionadded:: 1.0 - The ``subdomain_matching`` parameter was added. Subdomain - matching needs to be enabled manually now. Setting - :data:`SERVER_NAME` does not implicitly enable it. - - :param import_name: the name of the application package - :param static_url_path: can be used to specify a different path for the - static files on the web. Defaults to the name - of the `static_folder` folder. - :param static_folder: The folder with static files that is served at - ``static_url_path``. Relative to the application ``root_path`` - or an absolute path. Defaults to ``'static'``. - :param static_host: the host to use when adding the static route. - Defaults to None. Required when using ``host_matching=True`` - with a ``static_folder`` configured. - :param host_matching: set ``url_map.host_matching`` attribute. - Defaults to False. - :param subdomain_matching: consider the subdomain relative to - :data:`SERVER_NAME` when matching routes. Defaults to False. - :param template_folder: the folder that contains the templates that should - be used by the application. Defaults to - ``'templates'`` folder in the root path of the - application. - :param instance_path: An alternative instance path for the application. - By default the folder ``'instance'`` next to the - package or module is assumed to be the instance - path. - :param instance_relative_config: if set to ``True`` relative filenames - for loading the config are assumed to - be relative to the instance path instead - of the application root. - :param root_path: The path to the root of the application files. - This should only be set manually when it can't be detected - automatically, such as for namespace packages. - """ - - default_config = ImmutableDict( - { - "DEBUG": None, - "TESTING": False, - "PROPAGATE_EXCEPTIONS": None, - "SECRET_KEY": None, - "SECRET_KEY_FALLBACKS": None, - "PERMANENT_SESSION_LIFETIME": timedelta(days=31), - "USE_X_SENDFILE": False, - "TRUSTED_HOSTS": None, - "SERVER_NAME": None, - "APPLICATION_ROOT": "/", - "SESSION_COOKIE_NAME": "session", - "SESSION_COOKIE_DOMAIN": None, - "SESSION_COOKIE_PATH": None, - "SESSION_COOKIE_HTTPONLY": True, - "SESSION_COOKIE_SECURE": False, - "SESSION_COOKIE_PARTITIONED": False, - "SESSION_COOKIE_SAMESITE": None, - "SESSION_REFRESH_EACH_REQUEST": True, - "MAX_CONTENT_LENGTH": None, - "MAX_FORM_MEMORY_SIZE": 500_000, - "MAX_FORM_PARTS": 1_000, - "SEND_FILE_MAX_AGE_DEFAULT": None, - "TRAP_BAD_REQUEST_ERRORS": None, - "TRAP_HTTP_EXCEPTIONS": False, - "EXPLAIN_TEMPLATE_LOADING": False, - "PREFERRED_URL_SCHEME": "http", - "TEMPLATES_AUTO_RELOAD": None, - "MAX_COOKIE_SIZE": 4093, - "PROVIDE_AUTOMATIC_OPTIONS": True, - } - ) - - #: The class that is used for request objects. See :class:`~flask.Request` - #: for more information. - request_class: type[Request] = Request - - #: The class that is used for response objects. See - #: :class:`~flask.Response` for more information. - response_class: type[Response] = Response - - #: the session interface to use. By default an instance of - #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. - #: - #: .. versionadded:: 0.8 - session_interface: SessionInterface = SecureCookieSessionInterface() - - def __init_subclass__(cls, **kwargs: t.Any) -> None: - import warnings - - # These method signatures were updated to take a ctx param. Detect - # overridden methods in subclasses that still have the old signature. - # Show a deprecation warning and wrap to call with correct args. - for method in ( - cls.handle_http_exception, - cls.handle_user_exception, - cls.handle_exception, - cls.log_exception, - cls.dispatch_request, - cls.full_dispatch_request, - cls.finalize_request, - cls.make_default_options_response, - cls.preprocess_request, - cls.process_response, - cls.do_teardown_request, - cls.do_teardown_appcontext, - ): - base_method = getattr(Flask, method.__name__) - - if method is base_method: - # not overridden - continue - - # get the second parameter (first is self) - iter_params = iter(inspect.signature(method).parameters.values()) - next(iter_params) - param = next(iter_params, None) - - # must have second parameter named ctx or annotated AppContext - if param is None or not ( - # no annotation, match name - (param.annotation is inspect.Parameter.empty and param.name == "ctx") - or ( - # string annotation, access path ends with AppContext - isinstance(param.annotation, str) - and param.annotation.rpartition(".")[2] == "AppContext" - ) - or ( - # class annotation - inspect.isclass(param.annotation) - and issubclass(param.annotation, AppContext) - ) - ): - warnings.warn( - f"The '{method.__name__}' method now takes 'ctx: AppContext'" - " as the first parameter. The old signature is deprecated" - " and will not be supported in Flask 4.0.", - DeprecationWarning, - stacklevel=2, - ) - setattr(cls, method.__name__, remove_ctx(method)) - setattr(Flask, method.__name__, add_ctx(base_method)) - - def __init__( - self, - import_name: str, - static_url_path: str | None = None, - static_folder: str | os.PathLike[str] | None = "static", - static_host: str | None = None, - host_matching: bool = False, - subdomain_matching: bool = False, - template_folder: str | os.PathLike[str] | None = "templates", - instance_path: str | None = None, - instance_relative_config: bool = False, - root_path: str | None = None, - ): - super().__init__( - import_name=import_name, - static_url_path=static_url_path, - static_folder=static_folder, - static_host=static_host, - host_matching=host_matching, - subdomain_matching=subdomain_matching, - template_folder=template_folder, - instance_path=instance_path, - instance_relative_config=instance_relative_config, - root_path=root_path, - ) - - #: The Click command group for registering CLI commands for this - #: object. The commands are available from the ``flask`` command - #: once the application has been discovered and blueprints have - #: been registered. - self.cli = cli.AppGroup() - - # Set the name of the Click group in case someone wants to add - # the app's commands to another CLI tool. - self.cli.name = self.name - - # Add a static route using the provided static_url_path, static_host, - # and static_folder if there is a configured static_folder. - # Note we do this without checking if static_folder exists. - # For one, it might be created while the server is running (e.g. during - # development). Also, Google App Engine stores static files somewhere - if self.has_static_folder: - assert bool(static_host) == host_matching, ( - "Invalid static_host/host_matching combination" - ) - # Use a weakref to avoid creating a reference cycle between the app - # and the view function (see #3761). - self_ref = weakref.ref(self) - self.add_url_rule( - f"{self.static_url_path}/", - endpoint="static", - host=static_host, - view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore - ) - - def get_send_file_max_age(self, filename: str | None) -> int | None: - """Used by :func:`send_file` to determine the ``max_age`` cache - value for a given file path if it wasn't passed. - - By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from - the configuration of :data:`~flask.current_app`. This defaults - to ``None``, which tells the browser to use conditional requests - instead of a timed cache, which is usually preferable. - - Note this is a duplicate of the same method in the Flask - class. - - .. versionchanged:: 2.0 - The default configuration is ``None`` instead of 12 hours. - - .. versionadded:: 0.9 - """ - value = self.config["SEND_FILE_MAX_AGE_DEFAULT"] - - if value is None: - return None - - if isinstance(value, timedelta): - return int(value.total_seconds()) - - return value # type: ignore[no-any-return] - - def send_static_file(self, filename: str) -> Response: - """The view function used to serve files from - :attr:`static_folder`. A route is automatically registered for - this view at :attr:`static_url_path` if :attr:`static_folder` is - set. - - Note this is a duplicate of the same method in the Flask - class. - - .. versionadded:: 0.5 - - """ - if not self.has_static_folder: - raise RuntimeError("'static_folder' must be set to serve static_files.") - - # send_file only knows to call get_send_file_max_age on the app, - # call it here so it works for blueprints too. - max_age = self.get_send_file_max_age(filename) - return send_from_directory( - t.cast(str, self.static_folder), filename, max_age=max_age - ) - - def open_resource( - self, resource: str, mode: str = "rb", encoding: str | None = None - ) -> t.IO[t.AnyStr]: - """Open a resource file relative to :attr:`root_path` for reading. - - For example, if the file ``schema.sql`` is next to the file - ``app.py`` where the ``Flask`` app is defined, it can be opened - with: - - .. code-block:: python - - with app.open_resource("schema.sql") as f: - conn.executescript(f.read()) - - :param resource: Path to the resource relative to :attr:`root_path`. - :param mode: Open the file in this mode. Only reading is supported, - valid values are ``"r"`` (or ``"rt"``) and ``"rb"``. - :param encoding: Open the file with this encoding when opening in text - mode. This is ignored when opening in binary mode. - - .. versionchanged:: 3.1 - Added the ``encoding`` parameter. - """ - if mode not in {"r", "rt", "rb"}: - raise ValueError("Resources can only be opened for reading.") - - path = os.path.join(self.root_path, resource) - - if mode == "rb": - return open(path, mode) # pyright: ignore - - return open(path, mode, encoding=encoding) - - def open_instance_resource( - self, resource: str, mode: str = "rb", encoding: str | None = "utf-8" - ) -> t.IO[t.AnyStr]: - """Open a resource file relative to the application's instance folder - :attr:`instance_path`. Unlike :meth:`open_resource`, files in the - instance folder can be opened for writing. - - :param resource: Path to the resource relative to :attr:`instance_path`. - :param mode: Open the file in this mode. - :param encoding: Open the file with this encoding when opening in text - mode. This is ignored when opening in binary mode. - - .. versionchanged:: 3.1 - Added the ``encoding`` parameter. - """ - path = os.path.join(self.instance_path, resource) - - if "b" in mode: - return open(path, mode) - - return open(path, mode, encoding=encoding) - - def create_jinja_environment(self) -> Environment: - """Create the Jinja environment based on :attr:`jinja_options` - and the various Jinja-related methods of the app. Changing - :attr:`jinja_options` after this will have no effect. Also adds - Flask-related globals and filters to the environment. - - .. versionchanged:: 0.11 - ``Environment.auto_reload`` set in accordance with - ``TEMPLATES_AUTO_RELOAD`` configuration option. - - .. versionadded:: 0.5 - """ - options = dict(self.jinja_options) - - if "autoescape" not in options: - options["autoescape"] = self.select_jinja_autoescape - - if "auto_reload" not in options: - auto_reload = self.config["TEMPLATES_AUTO_RELOAD"] - - if auto_reload is None: - auto_reload = self.debug - - options["auto_reload"] = auto_reload - - rv = self.jinja_environment(self, **options) - rv.globals.update( - url_for=self.url_for, - get_flashed_messages=get_flashed_messages, - config=self.config, - # request, session and g are normally added with the - # context processor for efficiency reasons but for imported - # templates we also want the proxies in there. - request=request, - session=session, - g=g, - ) - rv.policies["json.dumps_function"] = self.json.dumps - return rv - - def create_url_adapter(self, request: Request | None) -> MapAdapter | None: - """Creates a URL adapter for the given request. The URL adapter - is created at a point where the request context is not yet set - up so the request is passed explicitly. - - .. versionchanged:: 3.1 - If :data:`SERVER_NAME` is set, it does not restrict requests to - only that domain, for both ``subdomain_matching`` and - ``host_matching``. - - .. versionchanged:: 1.0 - :data:`SERVER_NAME` no longer implicitly enables subdomain - matching. Use :attr:`subdomain_matching` instead. - - .. versionchanged:: 0.9 - This can be called outside a request when the URL adapter is created - for an application context. - - .. versionadded:: 0.6 - """ - if request is not None: - if (trusted_hosts := self.config["TRUSTED_HOSTS"]) is not None: - request.trusted_hosts = trusted_hosts - - # Check trusted_hosts here until bind_to_environ does. - request.host = get_host(request.environ, request.trusted_hosts) # pyright: ignore - subdomain = None - server_name = self.config["SERVER_NAME"] - - if self.url_map.host_matching: - # Don't pass SERVER_NAME, otherwise it's used and the actual - # host is ignored, which breaks host matching. - server_name = None - elif not self.subdomain_matching: - # Werkzeug doesn't implement subdomain matching yet. Until then, - # disable it by forcing the current subdomain to the default, or - # the empty string. - subdomain = self.url_map.default_subdomain or "" - - return self.url_map.bind_to_environ( - request.environ, server_name=server_name, subdomain=subdomain - ) - - # Need at least SERVER_NAME to match/build outside a request. - if self.config["SERVER_NAME"] is not None: - return self.url_map.bind( - self.config["SERVER_NAME"], - script_name=self.config["APPLICATION_ROOT"], - url_scheme=self.config["PREFERRED_URL_SCHEME"], - ) - - return None - - def raise_routing_exception(self, request: Request) -> t.NoReturn: - """Intercept routing exceptions and possibly do something else. - - In debug mode, intercept a routing redirect and replace it with - an error if the body will be discarded. - - With modern Werkzeug this shouldn't occur, since it now uses a - 308 status which tells the browser to resend the method and - body. - - .. versionchanged:: 2.1 - Don't intercept 307 and 308 redirects. - - :meta private: - :internal: - """ - if ( - not self.debug - or not isinstance(request.routing_exception, RequestRedirect) - or request.routing_exception.code in {307, 308} - or request.method in {"GET", "HEAD", "OPTIONS"} - ): - raise request.routing_exception # type: ignore[misc] - - from .debughelpers import FormDataRoutingRedirect - - raise FormDataRoutingRedirect(request) - - def update_template_context( - self, ctx: AppContext, context: dict[str, t.Any] - ) -> None: - """Update the template context with some commonly used variables. - This injects request, session, config and g into the template - context as well as everything template context processors want - to inject. Note that the as of Flask 0.6, the original values - in the context will not be overridden if a context processor - decides to return a value with the same key. - - :param context: the context as a dictionary that is updated in place - to add extra variables. - """ - names: t.Iterable[str | None] = (None,) - - # A template may be rendered outside a request context. - if ctx.has_request: - names = chain(names, reversed(ctx.request.blueprints)) - - # The values passed to render_template take precedence. Keep a - # copy to re-apply after all context functions. - orig_ctx = context.copy() - - for name in names: - if name in self.template_context_processors: - for func in self.template_context_processors[name]: - context.update(self.ensure_sync(func)()) - - context.update(orig_ctx) - - def make_shell_context(self) -> dict[str, t.Any]: - """Returns the shell context for an interactive shell for this - application. This runs all the registered shell context - processors. - - .. versionadded:: 0.11 - """ - rv = {"app": self, "g": g} - for processor in self.shell_context_processors: - rv.update(processor()) - return rv - - def run( - self, - host: str | None = None, - port: int | None = None, - debug: bool | None = None, - load_dotenv: bool = True, - **options: t.Any, - ) -> None: - """Runs the application on a local development server. - - Do not use ``run()`` in a production setting. It is not intended to - meet security and performance requirements for a production server. - Instead, see :doc:`/deploying/index` for WSGI server recommendations. - - If the :attr:`debug` flag is set the server will automatically reload - for code changes and show a debugger in case an exception happened. - - If you want to run the application in debug mode, but disable the - code execution on the interactive debugger, you can pass - ``use_evalex=False`` as parameter. This will keep the debugger's - traceback screen active, but disable code execution. - - It is not recommended to use this function for development with - automatic reloading as this is badly supported. Instead you should - be using the :command:`flask` command line script's ``run`` support. - - .. admonition:: Keep in Mind - - Flask will suppress any server error with a generic error page - unless it is in debug mode. As such to enable just the - interactive debugger without the code reloading, you have to - invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. - Setting ``use_debugger`` to ``True`` without being in debug mode - won't catch any exceptions because there won't be any to - catch. - - :param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to - have the server available externally as well. Defaults to - ``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config variable - if present. - :param port: the port of the webserver. Defaults to ``5000`` or the - port defined in the ``SERVER_NAME`` config variable if present. - :param debug: if given, enable or disable debug mode. See - :attr:`debug`. - :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` - files to set environment variables. Will also change the working - directory to the directory containing the first file found. - :param options: the options to be forwarded to the underlying Werkzeug - server. See :func:`werkzeug.serving.run_simple` for more - information. - - .. versionchanged:: 1.0 - If installed, python-dotenv will be used to load environment - variables from :file:`.env` and :file:`.flaskenv` files. - - The :envvar:`FLASK_DEBUG` environment variable will override :attr:`debug`. - - Threaded mode is enabled by default. - - .. versionchanged:: 0.10 - The default port is now picked from the ``SERVER_NAME`` - variable. - """ - # Ignore this call so that it doesn't start another server if - # the 'flask run' command is used. - if os.environ.get("FLASK_RUN_FROM_CLI") == "true": - if not is_running_from_reloader(): - click.secho( - " * Ignoring a call to 'app.run()' that would block" - " the current 'flask' CLI command.\n" - " Only call 'app.run()' in an 'if __name__ ==" - ' "__main__"\' guard.', - fg="red", - ) - - return - - if get_load_dotenv(load_dotenv): - cli.load_dotenv() - - # if set, env var overrides existing value - if "FLASK_DEBUG" in os.environ: - self.debug = get_debug_flag() - - # debug passed to method overrides all other sources - if debug is not None: - self.debug = bool(debug) - - server_name = self.config.get("SERVER_NAME") - sn_host = sn_port = None - - if server_name: - sn_host, _, sn_port = server_name.partition(":") - - if not host: - if sn_host: - host = sn_host - else: - host = "127.0.0.1" - - if port or port == 0: - port = int(port) - elif sn_port: - port = int(sn_port) - else: - port = 5000 - - options.setdefault("use_reloader", self.debug) - options.setdefault("use_debugger", self.debug) - options.setdefault("threaded", True) - - cli.show_server_banner(self.debug, self.name) - - from werkzeug.serving import run_simple - - try: - run_simple(t.cast(str, host), port, self, **options) - finally: - # reset the first request information if the development server - # reset normally. This makes it possible to restart the server - # without reloader and that stuff from an interactive shell. - self._got_first_request = False - - def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> FlaskClient: - """Creates a test client for this application. For information - about unit testing head over to :doc:`/testing`. - - Note that if you are testing for assertions or exceptions in your - application code, you must set ``app.testing = True`` in order for the - exceptions to propagate to the test client. Otherwise, the exception - will be handled by the application (not visible to the test client) and - the only indication of an AssertionError or other exception will be a - 500 status code response to the test client. See the :attr:`testing` - attribute. For example:: - - app.testing = True - client = app.test_client() - - The test client can be used in a ``with`` block to defer the closing down - of the context until the end of the ``with`` block. This is useful if - you want to access the context locals for testing:: - - with app.test_client() as c: - rv = c.get('/?vodka=42') - assert request.args['vodka'] == '42' - - Additionally, you may pass optional keyword arguments that will then - be passed to the application's :attr:`test_client_class` constructor. - For example:: - - from flask.testing import FlaskClient - - class CustomClient(FlaskClient): - def __init__(self, *args, **kwargs): - self._authentication = kwargs.pop("authentication") - super(CustomClient,self).__init__( *args, **kwargs) - - app.test_client_class = CustomClient - client = app.test_client(authentication='Basic ....') - - See :class:`~flask.testing.FlaskClient` for more information. - - .. versionchanged:: 0.4 - added support for ``with`` block usage for the client. - - .. versionadded:: 0.7 - The `use_cookies` parameter was added as well as the ability - to override the client to be used by setting the - :attr:`test_client_class` attribute. - - .. versionchanged:: 0.11 - Added `**kwargs` to support passing additional keyword arguments to - the constructor of :attr:`test_client_class`. - """ - cls = self.test_client_class - if cls is None: - from .testing import FlaskClient as cls - return cls( # type: ignore - self, self.response_class, use_cookies=use_cookies, **kwargs - ) - - def test_cli_runner(self, **kwargs: t.Any) -> FlaskCliRunner: - """Create a CLI runner for testing CLI commands. - See :ref:`testing-cli`. - - Returns an instance of :attr:`test_cli_runner_class`, by default - :class:`~flask.testing.FlaskCliRunner`. The Flask app object is - passed as the first argument. - - .. versionadded:: 1.0 - """ - cls = self.test_cli_runner_class - - if cls is None: - from .testing import FlaskCliRunner as cls - - return cls(self, **kwargs) # type: ignore - - def handle_http_exception( - self, ctx: AppContext, e: HTTPException - ) -> HTTPException | ft.ResponseReturnValue: - """Handles an HTTP exception. By default this will invoke the - registered error handlers and fall back to returning the - exception as response. - - .. versionchanged:: 1.0.3 - ``RoutingException``, used internally for actions such as - slash redirects during routing, is not passed to error - handlers. - - .. versionchanged:: 1.0 - Exceptions are looked up by code *and* by MRO, so - ``HTTPException`` subclasses can be handled with a catch-all - handler for the base ``HTTPException``. - - .. versionadded:: 0.3 - """ - # Proxy exceptions don't have error codes. We want to always return - # those unchanged as errors - if e.code is None: - return e - - # RoutingExceptions are used internally to trigger routing - # actions, such as slash redirects raising RequestRedirect. They - # are not raised or handled in user code. - if isinstance(e, RoutingException): - return e - - handler = self._find_error_handler(e, ctx.request.blueprints) - if handler is None: - return e - return self.ensure_sync(handler)(e) # type: ignore[no-any-return] - - def handle_user_exception( - self, ctx: AppContext, e: Exception - ) -> HTTPException | ft.ResponseReturnValue: - """This method is called whenever an exception occurs that - should be handled. A special case is :class:`~werkzeug - .exceptions.HTTPException` which is forwarded to the - :meth:`handle_http_exception` method. This function will either - return a response value or reraise the exception with the same - traceback. - - .. versionchanged:: 1.0 - Key errors raised from request data like ``form`` show the - bad key in debug mode rather than a generic bad request - message. - - .. versionadded:: 0.7 - """ - if isinstance(e, BadRequestKeyError) and ( - self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"] - ): - e.show_exception = True - - if isinstance(e, HTTPException) and not self.trap_http_exception(e): - return self.handle_http_exception(ctx, e) - - handler = self._find_error_handler(e, ctx.request.blueprints) - - if handler is None: - raise - - return self.ensure_sync(handler)(e) # type: ignore[no-any-return] - - def handle_exception(self, ctx: AppContext, e: Exception) -> Response: - """Handle an exception that did not have an error handler - associated with it, or that was raised from an error handler. - This always causes a 500 ``InternalServerError``. - - Always sends the :data:`got_request_exception` signal. - - If :data:`PROPAGATE_EXCEPTIONS` is ``True``, such as in debug - mode, the error will be re-raised so that the debugger can - display it. Otherwise, the original exception is logged, and - an :exc:`~werkzeug.exceptions.InternalServerError` is returned. - - If an error handler is registered for ``InternalServerError`` or - ``500``, it will be used. For consistency, the handler will - always receive the ``InternalServerError``. The original - unhandled exception is available as ``e.original_exception``. - - .. versionchanged:: 1.1.0 - Always passes the ``InternalServerError`` instance to the - handler, setting ``original_exception`` to the unhandled - error. - - .. versionchanged:: 1.1.0 - ``after_request`` functions and other finalization is done - even for the default 500 response when there is no handler. - - .. versionadded:: 0.3 - """ - exc_info = sys.exc_info() - got_request_exception.send(self, _async_wrapper=self.ensure_sync, exception=e) - propagate = self.config["PROPAGATE_EXCEPTIONS"] - - if propagate is None: - propagate = self.testing or self.debug - - if propagate: - # Re-raise if called with an active exception, otherwise - # raise the passed in exception. - if exc_info[1] is e: - raise - - raise e - - self.log_exception(ctx, exc_info) - server_error: InternalServerError | ft.ResponseReturnValue - server_error = InternalServerError(original_exception=e) - handler = self._find_error_handler(server_error, ctx.request.blueprints) - - if handler is not None: - server_error = self.ensure_sync(handler)(server_error) - - return self.finalize_request(ctx, server_error, from_error_handler=True) - - def log_exception( - self, - ctx: AppContext, - exc_info: tuple[type, BaseException, TracebackType] | tuple[None, None, None], - ) -> None: - """Logs an exception. This is called by :meth:`handle_exception` - if debugging is disabled and right before the handler is called. - The default implementation logs the exception as error on the - :attr:`logger`. - - .. versionadded:: 0.8 - """ - self.logger.error( - f"Exception on {ctx.request.path} [{ctx.request.method}]", exc_info=exc_info - ) - - def dispatch_request(self, ctx: AppContext) -> ft.ResponseReturnValue: - """Does the request dispatching. Matches the URL and returns the - return value of the view or error handler. This does not have to - be a response object. In order to convert the return value to a - proper response object, call :func:`make_response`. - - .. versionchanged:: 0.7 - This no longer does the exception handling, this code was - moved to the new :meth:`full_dispatch_request`. - """ - req = ctx.request - - if req.routing_exception is not None: - self.raise_routing_exception(req) - rule: Rule = req.url_rule # type: ignore[assignment] - # if we provide automatic options for this URL and the - # request came with the OPTIONS method, reply automatically - if ( - getattr(rule, "provide_automatic_options", False) - and req.method == "OPTIONS" - ): - return self.make_default_options_response(ctx) - # otherwise dispatch to the handler for that endpoint - view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment] - return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return] - - def full_dispatch_request(self, ctx: AppContext) -> Response: - """Dispatches the request and on top of that performs request - pre and postprocessing as well as HTTP exception catching and - error handling. - - .. versionadded:: 0.7 - """ - if not self._got_first_request and self.should_ignore_error is not None: - import warnings - - warnings.warn( - "The 'should_ignore_error' method is deprecated and will" - " be removed in Flask 3.3. Handle errors as needed in" - " teardown handlers instead.", - DeprecationWarning, - stacklevel=1, - ) - - self._got_first_request = True - - try: - request_started.send(self, _async_wrapper=self.ensure_sync) - rv = self.preprocess_request(ctx) - if rv is None: - rv = self.dispatch_request(ctx) - except Exception as e: - rv = self.handle_user_exception(ctx, e) - return self.finalize_request(ctx, rv) - - def finalize_request( - self, - ctx: AppContext, - rv: ft.ResponseReturnValue | HTTPException, - from_error_handler: bool = False, - ) -> Response: - """Given the return value from a view function this finalizes - the request by converting it into a response and invoking the - postprocessing functions. This is invoked for both normal - request dispatching as well as error handlers. - - Because this means that it might be called as a result of a - failure a special safe mode is available which can be enabled - with the `from_error_handler` flag. If enabled, failures in - response processing will be logged and otherwise ignored. - - :internal: - """ - response = self.make_response(rv) - try: - response = self.process_response(ctx, response) - request_finished.send( - self, _async_wrapper=self.ensure_sync, response=response - ) - except Exception: - if not from_error_handler: - raise - self.logger.exception( - "Request finalizing failed with an error while handling an error" - ) - return response - - def make_default_options_response(self, ctx: AppContext) -> Response: - """This method is called to create the default ``OPTIONS`` response. - This can be changed through subclassing to change the default - behavior of ``OPTIONS`` responses. - - .. versionadded:: 0.7 - """ - methods = ctx.url_adapter.allowed_methods() # type: ignore[union-attr] - rv = self.response_class() - rv.allow.update(methods) - return rv - - def ensure_sync(self, func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: - """Ensure that the function is synchronous for WSGI workers. - Plain ``def`` functions are returned as-is. ``async def`` - functions are wrapped to run and wait for the response. - - Override this method to change how the app runs async views. - - .. versionadded:: 2.0 - """ - if iscoroutinefunction(func): - return self.async_to_sync(func) - - return func - - def async_to_sync( - self, func: t.Callable[..., t.Coroutine[t.Any, t.Any, t.Any]] - ) -> t.Callable[..., t.Any]: - """Return a sync function that will run the coroutine function. - - .. code-block:: python - - result = app.async_to_sync(func)(*args, **kwargs) - - Override this method to change how the app converts async code - to be synchronously callable. - - .. versionadded:: 2.0 - """ - try: - from asgiref.sync import async_to_sync as asgiref_async_to_sync - except ImportError: - raise RuntimeError( - "Install Flask with the 'async' extra in order to use async views." - ) from None - - return asgiref_async_to_sync(func) - - def url_for( - self, - /, - endpoint: str, - *, - _anchor: str | None = None, - _method: str | None = None, - _scheme: str | None = None, - _external: bool | None = None, - **values: t.Any, - ) -> str: - """Generate a URL to the given endpoint with the given values. - - This is called by :func:`flask.url_for`, and can be called - directly as well. - - An *endpoint* is the name of a URL rule, usually added with - :meth:`@app.route() `, and usually the same name as the - view function. A route defined in a :class:`~flask.Blueprint` - will prepend the blueprint's name separated by a ``.`` to the - endpoint. - - In some cases, such as email messages, you want URLs to include - the scheme and domain, like ``https://example.com/hello``. When - not in an active request, URLs will be external by default, but - this requires setting :data:`SERVER_NAME` so Flask knows what - domain to use. :data:`APPLICATION_ROOT` and - :data:`PREFERRED_URL_SCHEME` should also be configured as - needed. This config is only used when not in an active request. - - Functions can be decorated with :meth:`url_defaults` to modify - keyword arguments before the URL is built. - - If building fails for some reason, such as an unknown endpoint - or incorrect values, the app's :meth:`handle_url_build_error` - method is called. If that returns a string, that is returned, - otherwise a :exc:`~werkzeug.routing.BuildError` is raised. - - :param endpoint: The endpoint name associated with the URL to - generate. If this starts with a ``.``, the current blueprint - name (if any) will be used. - :param _anchor: If given, append this as ``#anchor`` to the URL. - :param _method: If given, generate the URL associated with this - method for the endpoint. - :param _scheme: If given, the URL will have this scheme if it - is external. - :param _external: If given, prefer the URL to be internal - (False) or require it to be external (True). External URLs - include the scheme and domain. When not in an active - request, URLs are external by default. - :param values: Values to use for the variable parts of the URL - rule. Unknown keys are appended as query string arguments, - like ``?a=b&c=d``. - - .. versionadded:: 2.2 - Moved from ``flask.url_for``, which calls this method. - """ - if (ctx := _cv_app.get(None)) is not None and ctx.has_request: - url_adapter = ctx.url_adapter - blueprint_name = ctx.request.blueprint - - # If the endpoint starts with "." and the request matches a - # blueprint, the endpoint is relative to the blueprint. - if endpoint[:1] == ".": - if blueprint_name is not None: - endpoint = f"{blueprint_name}{endpoint}" - else: - endpoint = endpoint[1:] - - # When in a request, generate a URL without scheme and - # domain by default, unless a scheme is given. - if _external is None: - _external = _scheme is not None - else: - # If called by helpers.url_for, an app context is active, - # use its url_adapter. Otherwise, app.url_for was called - # directly, build an adapter. - if ctx is not None: - url_adapter = ctx.url_adapter - else: - url_adapter = self.create_url_adapter(None) - - if url_adapter is None: - raise RuntimeError( - "Unable to build URLs outside an active request" - " without 'SERVER_NAME' configured. Also configure" - " 'APPLICATION_ROOT' and 'PREFERRED_URL_SCHEME' as" - " needed." - ) - - # When outside a request, generate a URL with scheme and - # domain by default. - if _external is None: - _external = True - - # It is an error to set _scheme when _external=False, in order - # to avoid accidental insecure URLs. - if _scheme is not None and not _external: - raise ValueError("When specifying '_scheme', '_external' must be True.") - - self.inject_url_defaults(endpoint, values) - - try: - rv = url_adapter.build( # type: ignore[union-attr] - endpoint, - values, - method=_method, - url_scheme=_scheme, - force_external=_external, - ) - except BuildError as error: - values.update( - _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external - ) - return self.handle_url_build_error(error, endpoint, values) - - if _anchor is not None: - _anchor = _url_quote(_anchor, safe="%!#$&'()*+,/:;=?@") - rv = f"{rv}#{_anchor}" - - return rv - - def make_response(self, rv: ft.ResponseReturnValue) -> Response: - """Convert the return value from a view function to an instance of - :attr:`response_class`. - - :param rv: the return value from the view function. The view function - must return a response. Returning ``None``, or the view ending - without returning, is not allowed. The following types are allowed - for ``view_rv``: - - ``str`` - A response object is created with the string encoded to UTF-8 - as the body. - - ``bytes`` - A response object is created with the bytes as the body. - - ``dict`` - A dictionary that will be jsonify'd before being returned. - - ``list`` - A list that will be jsonify'd before being returned. - - ``generator`` or ``iterator`` - A generator that returns ``str`` or ``bytes`` to be - streamed as the response. - - ``tuple`` - Either ``(body, status, headers)``, ``(body, status)``, or - ``(body, headers)``, where ``body`` is any of the other types - allowed here, ``status`` is a string or an integer, and - ``headers`` is a dictionary or a list of ``(key, value)`` - tuples. If ``body`` is a :attr:`response_class` instance, - ``status`` overwrites the exiting value and ``headers`` are - extended. - - :attr:`response_class` - The object is returned unchanged. - - other :class:`~werkzeug.wrappers.Response` class - The object is coerced to :attr:`response_class`. - - :func:`callable` - The function is called as a WSGI application. The result is - used to create a response object. - - .. versionchanged:: 2.2 - A generator will be converted to a streaming response. - A list will be converted to a JSON response. - - .. versionchanged:: 1.1 - A dict will be converted to a JSON response. - - .. versionchanged:: 0.9 - Previously a tuple was interpreted as the arguments for the - response object. - """ - - status: int | None = None - headers: HeadersValue | None = None - - # unpack tuple returns - if isinstance(rv, tuple): - len_rv = len(rv) - - # a 3-tuple is unpacked directly - if len_rv == 3: - rv, status, headers = rv # type: ignore[misc] - # decide if a 2-tuple has status or headers - elif len_rv == 2: - if isinstance(rv[1], (Headers, dict, tuple, list)): - rv, headers = rv # pyright: ignore - else: - rv, status = rv # type: ignore[assignment,misc] - # other sized tuples are not allowed - else: - raise TypeError( - "The view function did not return a valid response tuple." - " The tuple must have the form (body, status, headers)," - " (body, status), or (body, headers)." - ) - - # the body must not be None - if rv is None: - raise TypeError( - f"The view function for {request.endpoint!r} did not" - " return a valid response. The function either returned" - " None or ended without a return statement." - ) - - # make sure the body is an instance of the response class - if not isinstance(rv, self.response_class): - if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, cabc.Iterator): - # let the response class set the status and headers instead of - # waiting to do it manually, so that the class can handle any - # special logic - rv = self.response_class( - rv, # pyright: ignore - status=status, - headers=headers, # type: ignore[arg-type] - ) - status = headers = None - elif isinstance(rv, (dict, list)): - rv = self.json.response(rv) - elif isinstance(rv, BaseResponse) or callable(rv): - # evaluate a WSGI callable, or coerce a different response - # class to the correct type - try: - rv = self.response_class.force_type( - rv, # type: ignore[arg-type] - request.environ, - ) - except TypeError as e: - raise TypeError( - f"{e}\nThe view function did not return a valid" - " response. The return type must be a string," - " dict, list, tuple with headers or status," - " Response instance, or WSGI callable, but it" - f" was a {type(rv).__name__}." - ).with_traceback(sys.exc_info()[2]) from None - else: - raise TypeError( - "The view function did not return a valid" - " response. The return type must be a string," - " dict, list, tuple with headers or status," - " Response instance, or WSGI callable, but it was a" - f" {type(rv).__name__}." - ) - - rv = t.cast(Response, rv) - # prefer the status if it was provided - if status is not None: - if isinstance(status, (str, bytes, bytearray)): - rv.status = status - else: - rv.status_code = status - - # extend existing headers with provided headers - if headers: - rv.headers.update(headers) - - return rv - - def preprocess_request(self, ctx: AppContext) -> ft.ResponseReturnValue | None: - """Called before the request is dispatched. Calls - :attr:`url_value_preprocessors` registered with the app and the - current blueprint (if any). Then calls :attr:`before_request_funcs` - registered with the app and the blueprint. - - If any :meth:`before_request` handler returns a non-None value, the - value is handled as if it was the return value from the view, and - further request handling is stopped. - """ - req = ctx.request - names = (None, *reversed(req.blueprints)) - - for name in names: - if name in self.url_value_preprocessors: - for url_func in self.url_value_preprocessors[name]: - url_func(req.endpoint, req.view_args) - - for name in names: - if name in self.before_request_funcs: - for before_func in self.before_request_funcs[name]: - rv = self.ensure_sync(before_func)() - - if rv is not None: - return rv # type: ignore[no-any-return] - - return None - - def process_response(self, ctx: AppContext, response: Response) -> Response: - """Can be overridden in order to modify the response object - before it's sent to the WSGI server. By default this will - call all the :meth:`after_request` decorated functions. - - .. versionchanged:: 0.5 - As of Flask 0.5 the functions registered for after request - execution are called in reverse order of registration. - - :param response: a :attr:`response_class` object. - :return: a new response object or the same, has to be an - instance of :attr:`response_class`. - """ - for func in ctx._after_request_functions: - response = self.ensure_sync(func)(response) - - for name in chain(ctx.request.blueprints, (None,)): - if name in self.after_request_funcs: - for func in reversed(self.after_request_funcs[name]): - response = self.ensure_sync(func)(response) - - if not self.session_interface.is_null_session(ctx._get_session()): - self.session_interface.save_session(self, ctx._get_session(), response) - - return response - - def do_teardown_request( - self, ctx: AppContext, exc: BaseException | None = None - ) -> None: - """Called after the request is dispatched and the response is finalized, - right before the request context is popped. Called by - :meth:`.AppContext.pop`. - - This calls all functions decorated with :meth:`teardown_request`, and - :meth:`Blueprint.teardown_request` if a blueprint handled the request. - Finally, the :data:`request_tearing_down` signal is sent. - - :param exc: An unhandled exception raised while dispatching the request. - Passed to each teardown function. - - .. versionchanged:: 3.2 - All callbacks are called rather than stopping on the first error. - - .. versionchanged:: 0.9 - Added the ``exc`` argument. - """ - collect_errors = _CollectErrors() - - for name in chain(ctx.request.blueprints, (None,)): - if name in self.teardown_request_funcs: - for func in reversed(self.teardown_request_funcs[name]): - with collect_errors: - self.ensure_sync(func)(exc) - - with collect_errors: - request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) - - collect_errors.raise_any("Errors during request teardown") - - def do_teardown_appcontext( - self, ctx: AppContext, exc: BaseException | None = None - ) -> None: - """Called right before the application context is popped. Called by - :meth:`.AppContext.pop`. - - This calls all functions decorated with :meth:`teardown_appcontext`. - Then the :data:`appcontext_tearing_down` signal is sent. - - :param exc: An unhandled exception raised while the context was active. - Passed to each teardown function. - - .. versionchanged:: 3.2 - All callbacks are called rather than stopping on the first error. - - .. versionadded:: 0.9 - """ - collect_errors = _CollectErrors() - - for func in reversed(self.teardown_appcontext_funcs): - with collect_errors: - self.ensure_sync(func)(exc) - - with collect_errors: - appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) - - collect_errors.raise_any("Errors during app teardown") - - def app_context(self) -> AppContext: - """Create an :class:`.AppContext`. When the context is pushed, - :data:`.current_app` and :data:`.g` become available. - - A context is automatically pushed when handling each request, and when - running any ``flask`` CLI command. Use this as a ``with`` block to - manually push a context outside of those situations, such as during - setup or testing. - - .. code-block:: python - - with app.app_context(): - init_db() - - See :doc:`/appcontext`. - - .. versionadded:: 0.9 - """ - return AppContext(self) - - def request_context(self, environ: WSGIEnvironment) -> AppContext: - """Create an :class:`.AppContext` with request information representing - the given WSGI environment. A context is automatically pushed when - handling each request. When the context is pushed, :data:`.request`, - :data:`.session`, :data:`g:, and :data:`.current_app` become available. - - This method should not be used in your own code. Creating a valid WSGI - environ is not trivial. Use :meth:`test_request_context` to correctly - create a WSGI environ and request context instead. - - See :doc:`/appcontext`. - - :param environ: A WSGI environment. - """ - return AppContext.from_environ(self, environ) - - def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> AppContext: - """Create an :class:`.AppContext` with request information created from - the given arguments. When the context is pushed, :data:`.request`, - :data:`.session`, :data:`g:, and :data:`.current_app` become available. - - This is useful during testing to run a function that uses request data - without dispatching a full request. Use this as a ``with`` block to push - a context. - - .. code-block:: python - - with app.test_request_context(...): - generate_report() - - See :doc:`/appcontext`. - - Takes the same arguments as Werkzeug's - :class:`~werkzeug.test.EnvironBuilder`, with some defaults from - the application. See the linked Werkzeug docs for most of the - available arguments. Flask-specific behavior is listed here. - - :param path: URL path being requested. - :param base_url: Base URL where the app is being served, which - ``path`` is relative to. If not given, built from - :data:`PREFERRED_URL_SCHEME`, ``subdomain``, :data:`SERVER_NAME`, - and :data:`APPLICATION_ROOT`. - :param subdomain: Subdomain name to prepend to :data:`SERVER_NAME`. - :param url_scheme: Scheme to use instead of - :data:`PREFERRED_URL_SCHEME`. - :param data: The request body text or bytes,or a dict of form data. - :param json: If given, this is serialized as JSON and passed as - ``data``. Also defaults ``content_type`` to - ``application/json``. - :param args: Other positional arguments passed to - :class:`~werkzeug.test.EnvironBuilder`. - :param kwargs: Other keyword arguments passed to - :class:`~werkzeug.test.EnvironBuilder`. - """ - from .testing import EnvironBuilder - - builder = EnvironBuilder(self, *args, **kwargs) - - try: - environ = builder.get_environ() - finally: - builder.close() - - return self.request_context(environ) - - def wsgi_app( - self, environ: WSGIEnvironment, start_response: StartResponse - ) -> cabc.Iterable[bytes]: - """The actual WSGI application. This is not implemented in - :meth:`__call__` so that middlewares can be applied without - losing a reference to the app object. Instead of doing this:: - - app = MyMiddleware(app) - - It's a better idea to do this instead:: - - app.wsgi_app = MyMiddleware(app.wsgi_app) - - Then you still have the original application object around and - can continue to call methods on it. - - .. versionchanged:: 0.7 - Teardown events for the request and app contexts are called - even if an unhandled error occurs. Other events may not be - called depending on when an error occurs during dispatch. - - :param environ: A WSGI environment. - :param start_response: A callable accepting a status code, - a list of headers, and an optional exception context to - start the response. - """ - ctx = self.request_context(environ) - error: BaseException | None = None - try: - try: - ctx.push() - response = self.full_dispatch_request(ctx) - except Exception as e: - error = e - response = self.handle_exception(ctx, e) - except: - error = sys.exc_info()[1] - raise - return response(environ, start_response) - finally: - if "werkzeug.debug.preserve_context" in environ: - environ["werkzeug.debug.preserve_context"](ctx) - - if ( - error is not None - and self.should_ignore_error is not None - and self.should_ignore_error(error) - ): - error = None - - ctx.pop(error) - - def __call__( - self, environ: WSGIEnvironment, start_response: StartResponse - ) -> cabc.Iterable[bytes]: - """The WSGI server calls the Flask application object as the - WSGI application. This calls :meth:`wsgi_app`, which can be - wrapped to apply middleware. - """ - return self.wsgi_app(environ, start_response) diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py deleted file mode 100644 index b6d4e433..00000000 --- a/src/flask/blueprints.py +++ /dev/null @@ -1,128 +0,0 @@ -from __future__ import annotations - -import os -import typing as t -from datetime import timedelta - -from .cli import AppGroup -from .globals import current_app -from .helpers import send_from_directory -from .sansio.blueprints import Blueprint as SansioBlueprint -from .sansio.blueprints import BlueprintSetupState as BlueprintSetupState # noqa -from .sansio.scaffold import _sentinel - -if t.TYPE_CHECKING: # pragma: no cover - from .wrappers import Response - - -class Blueprint(SansioBlueprint): - def __init__( - self, - name: str, - import_name: str, - static_folder: str | os.PathLike[str] | None = None, - static_url_path: str | None = None, - template_folder: str | os.PathLike[str] | None = None, - url_prefix: str | None = None, - subdomain: str | None = None, - url_defaults: dict[str, t.Any] | None = None, - root_path: str | None = None, - cli_group: str | None = _sentinel, # type: ignore - ) -> None: - super().__init__( - name, - import_name, - static_folder, - static_url_path, - template_folder, - url_prefix, - subdomain, - url_defaults, - root_path, - cli_group, - ) - - #: The Click command group for registering CLI commands for this - #: object. The commands are available from the ``flask`` command - #: once the application has been discovered and blueprints have - #: been registered. - self.cli = AppGroup() - - # Set the name of the Click group in case someone wants to add - # the app's commands to another CLI tool. - self.cli.name = self.name - - def get_send_file_max_age(self, filename: str | None) -> int | None: - """Used by :func:`send_file` to determine the ``max_age`` cache - value for a given file path if it wasn't passed. - - By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from - the configuration of :data:`~flask.current_app`. This defaults - to ``None``, which tells the browser to use conditional requests - instead of a timed cache, which is usually preferable. - - Note this is a duplicate of the same method in the Flask - class. - - .. versionchanged:: 2.0 - The default configuration is ``None`` instead of 12 hours. - - .. versionadded:: 0.9 - """ - value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] - - if value is None: - return None - - if isinstance(value, timedelta): - return int(value.total_seconds()) - - return value # type: ignore[no-any-return] - - def send_static_file(self, filename: str) -> Response: - """The view function used to serve files from - :attr:`static_folder`. A route is automatically registered for - this view at :attr:`static_url_path` if :attr:`static_folder` is - set. - - Note this is a duplicate of the same method in the Flask - class. - - .. versionadded:: 0.5 - - """ - if not self.has_static_folder: - raise RuntimeError("'static_folder' must be set to serve static_files.") - - # send_file only knows to call get_send_file_max_age on the app, - # call it here so it works for blueprints too. - max_age = self.get_send_file_max_age(filename) - return send_from_directory( - t.cast(str, self.static_folder), filename, max_age=max_age - ) - - def open_resource( - self, resource: str, mode: str = "rb", encoding: str | None = "utf-8" - ) -> t.IO[t.AnyStr]: - """Open a resource file relative to :attr:`root_path` for reading. The - blueprint-relative equivalent of the app's :meth:`~.Flask.open_resource` - method. - - :param resource: Path to the resource relative to :attr:`root_path`. - :param mode: Open the file in this mode. Only reading is supported, - valid values are ``"r"`` (or ``"rt"``) and ``"rb"``. - :param encoding: Open the file with this encoding when opening in text - mode. This is ignored when opening in binary mode. - - .. versionchanged:: 3.1 - Added the ``encoding`` parameter. - """ - if mode not in {"r", "rt", "rb"}: - raise ValueError("Resources can only be opened for reading.") - - path = os.path.join(self.root_path, resource) - - if mode == "rb": - return open(path, mode) # pyright: ignore - - return open(path, mode, encoding=encoding) diff --git a/src/flask/cli.py b/src/flask/cli.py deleted file mode 100644 index 1a9159ec..00000000 --- a/src/flask/cli.py +++ /dev/null @@ -1,1127 +0,0 @@ -from __future__ import annotations - -import ast -import collections.abc as cabc -import importlib.metadata -import inspect -import os -import platform -import re -import sys -import traceback -import typing as t -from functools import update_wrapper -from operator import itemgetter -from types import ModuleType - -import click -from click.core import ParameterSource -from werkzeug import run_simple -from werkzeug.serving import is_running_from_reloader -from werkzeug.utils import import_string - -from .globals import current_app -from .helpers import get_debug_flag -from .helpers import get_load_dotenv - -if t.TYPE_CHECKING: - import ssl - - from _typeshed.wsgi import StartResponse - from _typeshed.wsgi import WSGIApplication - from _typeshed.wsgi import WSGIEnvironment - - from .app import Flask - - -class NoAppException(click.UsageError): - """Raised if an application cannot be found or loaded.""" - - -def find_best_app(module: ModuleType) -> Flask: - """Given a module instance this tries to find the best possible - application in the module or raises an exception. - """ - from . import Flask - - # Search for the most common names first. - for attr_name in ("app", "application"): - app = getattr(module, attr_name, None) - - if isinstance(app, Flask): - return app - - # Otherwise find the only object that is a Flask instance. - matches = [v for v in module.__dict__.values() if isinstance(v, Flask)] - - if len(matches) == 1: - return matches[0] - elif len(matches) > 1: - raise NoAppException( - "Detected multiple Flask applications in module" - f" '{module.__name__}'. Use '{module.__name__}:name'" - " to specify the correct one." - ) - - # Search for app factory functions. - for attr_name in ("create_app", "make_app"): - app_factory = getattr(module, attr_name, None) - - if inspect.isfunction(app_factory): - try: - app = app_factory() - - if isinstance(app, Flask): - return app - except TypeError as e: - if not _called_with_wrong_args(app_factory): - raise - - raise NoAppException( - f"Detected factory '{attr_name}' in module '{module.__name__}'," - " but could not call it without arguments. Use" - f" '{module.__name__}:{attr_name}(args)'" - " to specify arguments." - ) from e - - raise NoAppException( - "Failed to find Flask application or factory in module" - f" '{module.__name__}'. Use '{module.__name__}:name'" - " to specify one." - ) - - -def _called_with_wrong_args(f: t.Callable[..., Flask]) -> bool: - """Check whether calling a function raised a ``TypeError`` because - the call failed or because something in the factory raised the - error. - - :param f: The function that was called. - :return: ``True`` if the call failed. - """ - tb = sys.exc_info()[2] - - try: - while tb is not None: - if tb.tb_frame.f_code is f.__code__: - # In the function, it was called successfully. - return False - - tb = tb.tb_next - - # Didn't reach the function. - return True - finally: - # Delete tb to break a circular reference. - # https://docs.python.org/2/library/sys.html#sys.exc_info - del tb - - -def find_app_by_string(module: ModuleType, app_name: str) -> Flask: - """Check if the given string is a variable name or a function. Call - a function to get the app instance, or return the variable directly. - """ - from . import Flask - - # Parse app_name as a single expression to determine if it's a valid - # attribute name or function call. - try: - expr = ast.parse(app_name.strip(), mode="eval").body - except SyntaxError: - raise NoAppException( - f"Failed to parse {app_name!r} as an attribute name or function call." - ) from None - - if isinstance(expr, ast.Name): - name = expr.id - args = [] - kwargs = {} - 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}." - ) - - name = expr.func.id - - # Parse the positional and keyword arguments as literals. - try: - args = [ast.literal_eval(arg) for arg in expr.args] - kwargs = { - kw.arg: ast.literal_eval(kw.value) - for kw in expr.keywords - if kw.arg is not None - } - except ValueError: - # literal_eval gives cryptic error messages, show a generic - # message with the full expression instead. - raise NoAppException( - 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." - ) - - try: - attr = getattr(module, name) - except AttributeError as 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. - if inspect.isfunction(attr): - try: - app = attr(*args, **kwargs) - except TypeError as e: - if not _called_with_wrong_args(attr): - raise - - raise NoAppException( - f"The factory {app_name!r} in module" - f" {module.__name__!r} could not be called with the" - " specified arguments." - ) from e - else: - app = attr - - if isinstance(app, Flask): - return app - - raise NoAppException( - "A valid Flask application was not obtained from" - f" '{module.__name__}:{app_name}'." - ) - - -def prepare_import(path: str) -> str: - """Given a filename this will try to calculate the python path, add it - to the search path and return the actual module name that is expected. - """ - path = os.path.realpath(path) - - fname, ext = os.path.splitext(path) - if ext == ".py": - path = fname - - if os.path.basename(path) == "__init__": - path = os.path.dirname(path) - - module_name = [] - - # move up until outside package structure (no __init__.py) - while True: - path, name = os.path.split(path) - module_name.append(name) - - if not os.path.exists(os.path.join(path, "__init__.py")): - break - - if sys.path[0] != path: - sys.path.insert(0, path) - - return ".".join(module_name[::-1]) - - -@t.overload -def locate_app( - module_name: str, app_name: str | None, raise_if_not_found: t.Literal[True] = True -) -> Flask: ... - - -@t.overload -def locate_app( - module_name: str, app_name: str | None, raise_if_not_found: t.Literal[False] = ... -) -> Flask | None: ... - - -def locate_app( - module_name: str, app_name: str | None, raise_if_not_found: bool = True -) -> Flask | None: - try: - __import__(module_name) - except ImportError: - # Reraise the ImportError if it occurred within the imported module. - # Determine this by checking whether the trace has a depth > 1. - if sys.exc_info()[2].tb_next: # type: ignore[union-attr] - raise NoAppException( - f"While importing {module_name!r}, an ImportError was" - f" raised:\n\n{traceback.format_exc()}" - ) from None - elif raise_if_not_found: - raise NoAppException(f"Could not import {module_name!r}.") from None - else: - return None - - module = sys.modules[module_name] - - if app_name is None: - return find_best_app(module) - else: - return find_app_by_string(module, app_name) - - -def get_version(ctx: click.Context, param: click.Parameter, value: t.Any) -> None: - if not value or ctx.resilient_parsing: - return - - flask_version = importlib.metadata.version("flask") - werkzeug_version = importlib.metadata.version("werkzeug") - - click.echo( - f"Python {platform.python_version()}\n" - f"Flask {flask_version}\n" - f"Werkzeug {werkzeug_version}", - color=ctx.color, - ) - ctx.exit() - - -version_option = click.Option( - ["--version"], - help="Show the Flask version.", - expose_value=False, - callback=get_version, - is_flag=True, - is_eager=True, -) - - -class ScriptInfo: - """Helper object to deal with Flask applications. This is usually not - necessary to interface with as it's used internally in the dispatching - to click. In future versions of Flask this object will most likely play - a bigger role. Typically it's created automatically by the - :class:`FlaskGroup` but you can also manually create it and pass it - onwards as click object. - - .. versionchanged:: 3.1 - Added the ``load_dotenv_defaults`` parameter and attribute. - """ - - def __init__( - self, - app_import_path: str | None = None, - create_app: t.Callable[..., Flask] | None = None, - set_debug_flag: bool = True, - load_dotenv_defaults: bool = True, - ) -> None: - #: Optionally the import path for the Flask application. - self.app_import_path = app_import_path - #: Optionally a function that is passed the script info to create - #: the instance of the application. - self.create_app = create_app - #: A dictionary with arbitrary data that can be associated with - #: this script info. - self.data: dict[t.Any, t.Any] = {} - self.set_debug_flag = set_debug_flag - - self.load_dotenv_defaults = get_load_dotenv(load_dotenv_defaults) - """Whether default ``.flaskenv`` and ``.env`` files should be loaded. - - ``ScriptInfo`` doesn't load anything, this is for reference when doing - the load elsewhere during processing. - - .. versionadded:: 3.1 - """ - - self._loaded_app: Flask | None = None - - def load_app(self) -> Flask: - """Loads the Flask app (if not yet loaded) and returns it. Calling - this multiple times will just result in the already loaded app to - be returned. - """ - if self._loaded_app is not None: - return self._loaded_app - app: Flask | None = None - if self.create_app is not None: - app = self.create_app() - else: - if self.app_import_path: - 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: - for path in ("wsgi.py", "app.py"): - import_name = prepare_import(path) - app = locate_app(import_name, None, raise_if_not_found=False) - - if app is not None: - break - - if app is None: - raise NoAppException( - "Could not locate a Flask application. Use the" - " 'flask --app' option, 'FLASK_APP' environment" - " variable, or a 'wsgi.py' or 'app.py' file in the" - " current directory." - ) - - if self.set_debug_flag: - # Update the app's debug flag through the descriptor so that - # other values repopulate as well. - app.debug = get_debug_flag() - - self._loaded_app = app - return app - - -pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) - -F = t.TypeVar("F", bound=t.Callable[..., t.Any]) - - -def with_appcontext(f: F) -> F: - """Wraps a callback so that it's guaranteed to be executed with the - script's application context. - - Custom commands (and their options) registered under ``app.cli`` or - ``blueprint.cli`` will always have an app context available, this - decorator is not required in that case. - - .. versionchanged:: 2.2 - The app context is active for subcommands as well as the - decorated callback. The app context is always available to - ``app.cli`` command and parameter callbacks. - """ - - @click.pass_context - def decorator(ctx: click.Context, /, *args: t.Any, **kwargs: t.Any) -> t.Any: - if not current_app: - app = ctx.ensure_object(ScriptInfo).load_app() - ctx.with_resource(app.app_context()) - - return ctx.invoke(f, *args, **kwargs) - - return update_wrapper(decorator, f) # type: ignore[return-value] - - -class AppGroup(click.Group): - """This works similar to a regular click :class:`~click.Group` but it - changes the behavior of the :meth:`command` decorator so that it - automatically wraps the functions in :func:`with_appcontext`. - - Not to be confused with :class:`FlaskGroup`. - """ - - def command( # type: ignore[override] - self, *args: t.Any, **kwargs: t.Any - ) -> t.Callable[[t.Callable[..., t.Any]], click.Command]: - """This works exactly like the method of the same name on a regular - :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` - unless it's disabled by passing ``with_appcontext=False``. - """ - wrap_for_ctx = kwargs.pop("with_appcontext", True) - - def decorator(f: t.Callable[..., t.Any]) -> click.Command: - if wrap_for_ctx: - f = with_appcontext(f) - return super(AppGroup, self).command(*args, **kwargs)(f) # type: ignore[no-any-return] - - return decorator - - def group( # type: ignore[override] - self, *args: t.Any, **kwargs: t.Any - ) -> t.Callable[[t.Callable[..., t.Any]], click.Group]: - """This works exactly like the method of the same name on a regular - :class:`click.Group` but it defaults the group class to - :class:`AppGroup`. - """ - kwargs.setdefault("cls", AppGroup) - return super().group(*args, **kwargs) # type: ignore[no-any-return] - - -def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None: - if value is None: - return None - - info = ctx.ensure_object(ScriptInfo) - info.app_import_path = value - return value - - -# This option is eager so the app will be available if --help is given. -# --help is also eager, so --app must be before it in the param list. -# no_args_is_help bypasses eager processing, so this option must be -# processed manually in that case to ensure FLASK_APP gets picked up. -_app_option = click.Option( - ["-A", "--app"], - metavar="IMPORT", - help=( - "The Flask application or factory function to load, in the form 'module:name'." - " Module can be a dotted import or file path. Name is not required if it is" - " 'app', 'application', 'create_app', or 'make_app', and can be 'name(args)' to" - " pass arguments." - ), - is_eager=True, - expose_value=False, - callback=_set_app, -) - - -def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None: - # If the flag isn't provided, it will default to False. Don't use - # that, let debug be set by env in that case. - source = ctx.get_parameter_source(param.name) - - if source is not None and source in ( - ParameterSource.DEFAULT, - ParameterSource.DEFAULT_MAP, - ): - return None - - # Set with env var instead of ScriptInfo.load so that it can be - # accessed early during a factory function. - os.environ["FLASK_DEBUG"] = "1" if value else "0" - return value - - -_debug_option = click.Option( - ["--debug/--no-debug"], - help="Set debug mode.", - expose_value=False, - callback=_set_debug, -) - - -def _env_file_callback( - ctx: click.Context, param: click.Option, value: str | None -) -> str | None: - try: - import dotenv # noqa: F401 - except ImportError: - # Only show an error if a value was passed, otherwise we still want to - # call load_dotenv and show a message without exiting. - if value is not None: - raise click.BadParameter( - "python-dotenv must be installed to load an env file.", - ctx=ctx, - param=param, - ) from None - - # Load if a value was passed, or we want to load default files, or both. - if value is not None or ctx.obj.load_dotenv_defaults: - load_dotenv(value, load_defaults=ctx.obj.load_dotenv_defaults) - - return value - - -# This option is eager so env vars are loaded as early as possible to be -# used by other options. -_env_file_option = click.Option( - ["-e", "--env-file"], - type=click.Path(exists=True, dir_okay=False), - help=( - "Load environment variables from this file, taking precedence over" - " those set by '.env' and '.flaskenv'. Variables set directly in the" - " environment take highest precedence. python-dotenv must be installed." - ), - is_eager=True, - expose_value=False, - callback=_env_file_callback, -) - - -class FlaskGroup(AppGroup): - """Special subclass of the :class:`AppGroup` group that supports - loading more commands from the configured Flask app. Normally a - developer does not have to interface with this class but there are - some very advanced use cases for which it makes sense to create an - instance of this. see :ref:`custom-scripts`. - - :param add_default_commands: if this is True then the default run and - shell commands will be added. - :param add_version_option: adds the ``--version`` option. - :param create_app: an optional callback that is passed the script info and - returns the loaded app. - :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` - files to set environment variables. Will also change the working - directory to the directory containing the first file found. - :param set_debug_flag: Set the app's debug flag. - - .. versionchanged:: 3.1 - ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files. - - .. versionchanged:: 2.2 - Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options. - - .. versionchanged:: 2.2 - An app context is pushed when running ``app.cli`` commands, so - ``@with_appcontext`` is no longer required for those commands. - - .. versionchanged:: 1.0 - If installed, python-dotenv will be used to load environment variables - from :file:`.env` and :file:`.flaskenv` files. - """ - - def __init__( - self, - add_default_commands: bool = True, - create_app: t.Callable[..., Flask] | None = None, - add_version_option: bool = True, - load_dotenv: bool = True, - set_debug_flag: bool = True, - **extra: t.Any, - ) -> None: - params: list[click.Parameter] = list(extra.pop("params", None) or ()) - # Processing is done with option callbacks instead of a group - # callback. This allows users to make a custom group callback - # without losing the behavior. --env-file must come first so - # that it is eagerly evaluated before --app. - params.extend((_env_file_option, _app_option, _debug_option)) - - if add_version_option: - params.append(version_option) - - if "context_settings" not in extra: - extra["context_settings"] = {} - - extra["context_settings"].setdefault("auto_envvar_prefix", "FLASK") - - super().__init__(params=params, **extra) - - self.create_app = create_app - self.load_dotenv = load_dotenv - self.set_debug_flag = set_debug_flag - - if add_default_commands: - self.add_command(run_command) - self.add_command(shell_command) - self.add_command(routes_command) - - self._loaded_plugin_commands = False - - def _load_plugin_commands(self) -> None: - if self._loaded_plugin_commands: - return - - for ep in importlib.metadata.entry_points(group="flask.commands"): - self.add_command(ep.load(), ep.name) - - self._loaded_plugin_commands = True - - def get_command(self, ctx: click.Context, name: str) -> click.Command | None: - self._load_plugin_commands() - # Look up built-in and plugin commands, which should be - # available even if the app fails to load. - rv = super().get_command(ctx, name) - - if rv is not None: - return rv - - info = ctx.ensure_object(ScriptInfo) - - # Look up commands provided by the app, showing an error and - # continuing if the app couldn't be loaded. - try: - app = info.load_app() - except NoAppException as e: - click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") - return None - - # Push an app context for the loaded app unless it is already - # active somehow. This makes the context available to parameter - # and command callbacks without needing @with_appcontext. - if not current_app or current_app._get_current_object() is not app: - ctx.with_resource(app.app_context()) - - return app.cli.get_command(ctx, name) - - def list_commands(self, ctx: click.Context) -> list[str]: - self._load_plugin_commands() - # Start with the built-in and plugin commands. - rv = set(super().list_commands(ctx)) - info = ctx.ensure_object(ScriptInfo) - - # Add commands provided by the app, showing an error and - # continuing if the app couldn't be loaded. - try: - rv.update(info.load_app().cli.list_commands(ctx)) - except NoAppException as e: - # When an app couldn't be loaded, show the error message - # without the traceback. - click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") - except Exception: - # When any other errors occurred during loading, show the - # full traceback. - click.secho(f"{traceback.format_exc()}\n", err=True, fg="red") - - return sorted(rv) - - def make_context( - self, - info_name: str | None, - args: list[str], - parent: click.Context | None = None, - **extra: t.Any, - ) -> click.Context: - # Set a flag to tell app.run to become a no-op. If app.run was - # not in a __name__ == __main__ guard, it would start the server - # when importing, blocking whatever command is being called. - os.environ["FLASK_RUN_FROM_CLI"] = "true" - - if "obj" not in extra and "obj" not in self.context_settings: - extra["obj"] = ScriptInfo( - create_app=self.create_app, - set_debug_flag=self.set_debug_flag, - load_dotenv_defaults=self.load_dotenv, - ) - - return super().make_context(info_name, args, parent=parent, **extra) - - def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: - if (not args and self.no_args_is_help) or ( - len(args) == 1 and args[0] in self.get_help_option_names(ctx) - ): - # Attempt to load --env-file and --app early in case they - # were given as env vars. Otherwise no_args_is_help will not - # see commands from app.cli. - _env_file_option.handle_parse_result(ctx, {}, []) - _app_option.handle_parse_result(ctx, {}, []) - - return super().parse_args(ctx, args) - - -def _path_is_ancestor(path: str, other: str) -> bool: - """Take ``other`` and remove the length of ``path`` from it. Then join it - to ``path``. If it is the original value, ``path`` is an ancestor of - ``other``.""" - return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other - - -def load_dotenv( - path: str | os.PathLike[str] | None = None, load_defaults: bool = True -) -> bool: - """Load "dotenv" files to set environment variables. A given path takes - precedence over ``.env``, which takes precedence over ``.flaskenv``. After - loading and combining these files, values are only set if the key is not - already set in ``os.environ``. - - This is a no-op if `python-dotenv`_ is not installed. - - .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme - - :param path: Load the file at this location. - :param load_defaults: Search for and load the default ``.flaskenv`` and - ``.env`` files. - :return: ``True`` if at least one env var was loaded. - - .. versionchanged:: 3.1 - Added the ``load_defaults`` parameter. A given path takes precedence - over default files. - - .. versionchanged:: 2.0 - The current directory is not changed to the location of the - loaded file. - - .. versionchanged:: 2.0 - When loading the env files, set the default encoding to UTF-8. - - .. versionchanged:: 1.1.0 - Returns ``False`` when python-dotenv is not installed, or when - the given path isn't a file. - - .. versionadded:: 1.0 - """ - try: - import dotenv - except ImportError: - if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"): - click.secho( - " * Tip: There are .env files present. Install python-dotenv" - " to use them.", - fg="yellow", - err=True, - ) - - return False - - data: dict[str, str | None] = {} - - if load_defaults: - for default_name in (".flaskenv", ".env"): - if not (default_path := dotenv.find_dotenv(default_name, usecwd=True)): - continue - - data |= dotenv.dotenv_values(default_path, encoding="utf-8") - - if path is not None and os.path.isfile(path): - data |= dotenv.dotenv_values(path, encoding="utf-8") - - for key, value in data.items(): - if key in os.environ or value is None: - continue - - os.environ[key] = value - - return bool(data) # True if at least one env var was loaded. - - -def show_server_banner(debug: bool, app_import_path: str | None) -> None: - """Show extra startup messages the first time the server is run, - ignoring the reloader. - """ - if is_running_from_reloader(): - return - - if app_import_path is not None: - click.echo(f" * Serving Flask app '{app_import_path}'") - - if debug is not None: - click.echo(f" * Debug mode: {'on' if debug else 'off'}") - - -class CertParamType(click.ParamType[t.Any]): - """Click option type for the ``--cert`` option. Allows either an - existing file, the string ``'adhoc'``, or an import for a - :class:`~ssl.SSLContext` object. - """ - - name = "path" - - def __init__(self) -> None: - self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) - - def convert( - self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None - ) -> t.Any: - try: - import ssl - except ImportError: - raise click.BadParameter( - 'Using "--cert" requires Python to be compiled with SSL support.', - ctx, - param, - ) from None - - try: - return self.path_type(value, param, ctx) - except click.BadParameter: - value = click.STRING(value, param, ctx).lower() # type: ignore[union-attr] - - if value == "adhoc": - try: - import cryptography # noqa: F401 - except ImportError: - raise click.BadParameter( - "Using ad-hoc certificates requires the cryptography library.", - ctx, - param, - ) from None - - return value - - obj = import_string(value, silent=True) - - if isinstance(obj, ssl.SSLContext): - return obj - - raise - - -def _validate_key(ctx: click.Context, param: click.Parameter, value: t.Any) -> t.Any: - """The ``--key`` option must be specified when ``--cert`` is a file. - Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed. - """ - cert = ctx.params.get("cert") - is_adhoc = cert == "adhoc" - - try: - import ssl - except ImportError: - is_context = False - else: - is_context = isinstance(cert, ssl.SSLContext) - - if value is not None: - if is_adhoc: - raise click.BadParameter( - 'When "--cert" is "adhoc", "--key" is not used.', ctx, param - ) - - if is_context: - raise click.BadParameter( - 'When "--cert" is an SSLContext object, "--key" is not used.', - ctx, - param, - ) - - if not cert: - raise click.BadParameter('"--cert" must also be specified.', ctx, param) - - ctx.params["cert"] = cert, value - - else: - if cert and not (is_adhoc or is_context): - raise click.BadParameter('Required when using "--cert".', ctx, param) - - return value - - -class SeparatedPathType(click.Path): - """Click option type that accepts a list of values separated by the - OS's path separator (``:``, ``;`` on Windows). Each value is - validated as a :class:`click.Path` type. - """ - - def convert( - self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None - ) -> t.Any: - items = self.split_envvar_value(value) - # can't call no-arg super() inside list comprehension until Python 3.12 - super_convert = super().convert - return [super_convert(item, param, ctx) for item in items] - - -@click.command("run", short_help="Run a development server.") -@click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.") -@click.option("--port", "-p", default=5000, help="The port to bind to.") -@click.option( - "--cert", - type=CertParamType(), - help="Specify a certificate file to use HTTPS.", - is_eager=True, -) -@click.option( - "--key", - type=click.Path(exists=True, dir_okay=False, resolve_path=True), - callback=_validate_key, - expose_value=False, - help="The key file to use when specifying a certificate.", -) -@click.option( - "--reload/--no-reload", - default=None, - help="Enable or disable the reloader. By default the reloader " - "is active if debug is enabled.", -) -@click.option( - "--debugger/--no-debugger", - default=None, - help="Enable or disable the debugger. By default the debugger " - "is active if debug is enabled.", -) -@click.option( - "--with-threads/--without-threads", - default=True, - help="Enable or disable multithreading.", -) -@click.option( - "--extra-files", - default=None, - type=SeparatedPathType(), - help=( - "Extra files that trigger a reload on change. Multiple paths" - f" are separated by {os.path.pathsep!r}." - ), -) -@click.option( - "--exclude-patterns", - default=None, - type=SeparatedPathType(), - help=( - "Files matching these fnmatch patterns will not trigger a reload" - " on change. Multiple patterns are separated by" - f" {os.path.pathsep!r}." - ), -) -@pass_script_info -def run_command( - info: ScriptInfo, - host: str, - port: int, - reload: bool, - debugger: bool, - with_threads: bool, - cert: ssl.SSLContext | tuple[str, str | None] | t.Literal["adhoc"] | None, - extra_files: list[str] | None, - exclude_patterns: list[str] | None, -) -> None: - """Run a local development server. - - This server is for development purposes only. It does not provide - the stability, security, or performance of production WSGI servers. - - The reloader and debugger are enabled by default with the '--debug' - option. - """ - try: - app: WSGIApplication = info.load_app() # pyright: ignore - except Exception as e: - if is_running_from_reloader(): - # When reloading, print out the error immediately, but raise - # it later so the debugger or server can handle it. - traceback.print_exc() - err = e - - def app( - environ: WSGIEnvironment, start_response: StartResponse - ) -> cabc.Iterable[bytes]: - raise err from None - - else: - # When not reloading, raise the error immediately so the - # command fails. - raise e from None - - debug = get_debug_flag() - - if reload is None: - reload = debug - - if debugger is None: - debugger = debug - - show_server_banner(debug, info.app_import_path) - - run_simple( - host, - port, - app, - use_reloader=reload, - use_debugger=debugger, - threaded=with_threads, - ssl_context=cert, - extra_files=extra_files, - exclude_patterns=exclude_patterns, - ) - - -run_command.params.insert(0, _debug_option) - - -@click.command("shell", short_help="Run a shell in the app context.") -@with_appcontext -def shell_command() -> None: - """Run an interactive Python shell in the context of a given - Flask application. The application will populate the default - namespace of this shell according to its configuration. - - This is useful for executing small snippets of management code - without having to manually configure the application. - """ - import code - - banner = ( - f"Python {sys.version} on {sys.platform}\n" - f"App: {current_app.import_name}\n" - f"Instance: {current_app.instance_path}" - ) - ctx: dict[str, t.Any] = {} - - # Support the regular Python interpreter startup script if someone - # is using it. - startup = os.environ.get("PYTHONSTARTUP") - if startup and os.path.isfile(startup): - with open(startup) as f: - eval(compile(f.read(), startup, "exec"), ctx) - - ctx.update(current_app.make_shell_context()) - - # Site, customize, or startup script can set a hook to call when - # entering interactive mode. The default one sets up readline with - # tab and history completion. - interactive_hook = getattr(sys, "__interactivehook__", None) - - if interactive_hook is not None: - try: - import readline - from rlcompleter import Completer - except ImportError: - pass - else: - # rlcompleter uses __main__.__dict__ by default, which is - # flask.__main__. Use the shell context instead. - readline.set_completer(Completer(ctx).complete) - - interactive_hook() - - code.interact(banner=banner, local=ctx) - - -@click.command("routes", short_help="Show the routes for the app.") -@click.option( - "--sort", - "-s", - type=click.Choice(("endpoint", "methods", "domain", "rule", "match")), - default="endpoint", - help=( - "Method to sort routes by. 'match' is the order that Flask will match routes" - " when dispatching a request." - ), -) -@click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.") -@with_appcontext -def routes_command(sort: str, all_methods: bool) -> None: - """Show all registered routes with endpoints and methods.""" - rules = list(current_app.url_map.iter_rules()) - - if not rules: - click.echo("No routes were registered.") - return - - ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"} - host_matching = current_app.url_map.host_matching - has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules) - rows = [] - - for rule in rules: - row = [ - rule.endpoint, - ", ".join(sorted((rule.methods or set()) - ignored_methods)), - ] - - if has_domain: - row.append((rule.host if host_matching else rule.subdomain) or "") - - row.append(rule.rule) - rows.append(row) - - headers = ["Endpoint", "Methods"] - sorts = ["endpoint", "methods"] - - if has_domain: - headers.append("Host" if host_matching else "Subdomain") - sorts.append("domain") - - headers.append("Rule") - sorts.append("rule") - - try: - rows.sort(key=itemgetter(sorts.index(sort))) - except ValueError: - pass - - rows.insert(0, headers) - widths = [max(len(row[i]) for row in rows) for i in range(len(headers))] - rows.insert(1, ["-" * w for w in widths]) - template = " ".join(f"{{{i}:<{w}}}" for i, w in enumerate(widths)) - - for row in rows: - click.echo(template.format(*row)) - - -cli = FlaskGroup( - name="flask", - help="""\ -A general utility script for Flask applications. - -An application to load must be given with the '--app' option, -'FLASK_APP' environment variable, or with a 'wsgi.py' or 'app.py' file -in the current directory. -""", -) - - -def main() -> None: - cli.main() - - -if __name__ == "__main__": - main() diff --git a/src/flask/config.py b/src/flask/config.py deleted file mode 100644 index 34ef1a57..00000000 --- a/src/flask/config.py +++ /dev/null @@ -1,367 +0,0 @@ -from __future__ import annotations - -import errno -import json -import os -import types -import typing as t - -from werkzeug.utils import import_string - -if t.TYPE_CHECKING: - import typing_extensions as te - - from .sansio.app import App - - -T = t.TypeVar("T") - - -class ConfigAttribute(t.Generic[T]): - """Makes an attribute forward to the config""" - - def __init__( - self, name: str, get_converter: t.Callable[[t.Any], T] | None = None - ) -> None: - self.__name__ = name - self.get_converter = get_converter - - @t.overload - def __get__(self, obj: None, owner: None) -> te.Self: ... - - @t.overload - def __get__(self, obj: App, owner: type[App]) -> T: ... - - def __get__(self, obj: App | None, owner: type[App] | None = None) -> T | te.Self: - if obj is None: - return self - - rv = obj.config[self.__name__] - - if self.get_converter is not None: - rv = self.get_converter(rv) - - return rv # type: ignore[no-any-return] - - def __set__(self, obj: App, value: t.Any) -> None: - obj.config[self.__name__] = value - - -class Config(dict): # type: ignore[type-arg] - """Works exactly like a dict but provides ways to fill it from files - or special dictionaries. There are two common patterns to populate the - config. - - Either you can fill the config from a config file:: - - app.config.from_pyfile('yourconfig.cfg') - - Or alternatively you can define the configuration options in the - module that calls :meth:`from_object` or provide an import path to - a module that should be loaded. It is also possible to tell it to - use the same module and with that provide the configuration values - just before the call:: - - DEBUG = True - SECRET_KEY = 'development key' - app.config.from_object(__name__) - - In both cases (loading from any Python file or loading from modules), - only uppercase keys are added to the config. This makes it possible to use - lowercase values in the config file for temporary values that are not added - to the config or to define the config keys in the same file that implements - the application. - - Probably the most interesting way to load configurations is from an - environment variable pointing to a file:: - - app.config.from_envvar('YOURAPPLICATION_SETTINGS') - - In this case before launching the application you have to set this - environment variable to the file you want to use. On Linux and OS X - use the export statement:: - - export YOURAPPLICATION_SETTINGS='/path/to/config/file' - - On windows use `set` instead. - - :param root_path: path to which files are read relative from. When the - config object is created by the application, this is - the application's :attr:`~flask.Flask.root_path`. - :param defaults: an optional dictionary of default values - """ - - def __init__( - self, - root_path: str | os.PathLike[str], - defaults: dict[str, t.Any] | None = None, - ) -> None: - super().__init__(defaults or {}) - self.root_path = root_path - - def from_envvar(self, variable_name: str, silent: bool = False) -> bool: - """Loads a configuration from an environment variable pointing to - a configuration file. This is basically just a shortcut with nicer - error messages for this line of code:: - - app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) - - :param variable_name: name of the environment variable - :param silent: set to ``True`` if you want silent failure for missing - files. - :return: ``True`` if the file was loaded successfully. - """ - rv = os.environ.get(variable_name) - if not rv: - if silent: - return False - raise RuntimeError( - f"The environment variable {variable_name!r} is not set" - " and as such configuration could not be loaded. Set" - " this variable and make it point to a configuration" - " file" - ) - return self.from_pyfile(rv, silent=silent) - - def from_prefixed_env( - self, prefix: str = "FLASK", *, loads: t.Callable[[str], t.Any] = json.loads - ) -> bool: - """Load any environment variables that start with ``FLASK_``, - dropping the prefix from the env key for the config key. Values - are passed through a loading function to attempt to convert them - to more specific types than strings. - - Keys are loaded in :func:`sorted` order. - - The default loading function attempts to parse values as any - valid JSON type, including dicts and lists. - - Specific items in nested dicts can be set by separating the - keys with double underscores (``__``). If an intermediate key - doesn't exist, it will be initialized to an empty dict. - - :param prefix: Load env vars that start with this prefix, - separated with an underscore (``_``). - :param loads: Pass each string value to this function and use - the returned value as the config value. If any error is - raised it is ignored and the value remains a string. The - default is :func:`json.loads`. - - .. versionadded:: 2.1 - """ - prefix = f"{prefix}_" - - for key in sorted(os.environ): - if not key.startswith(prefix): - continue - - value = os.environ[key] - key = key.removeprefix(prefix) - - try: - value = loads(value) - except Exception: - # Keep the value as a string if loading failed. - pass - - if "__" not in key: - # A non-nested key, set directly. - self[key] = value - continue - - # Traverse nested dictionaries with keys separated by "__". - current = self - *parts, tail = key.split("__") - - for part in parts: - # If an intermediate dict does not exist, create it. - if part not in current: - current[part] = {} - - current = current[part] - - current[tail] = value - - return True - - def from_pyfile( - self, filename: str | os.PathLike[str], silent: bool = False - ) -> bool: - """Updates the values in the config from a Python file. This function - behaves as if the file was imported as module with the - :meth:`from_object` function. - - :param filename: the filename of the config. This can either be an - absolute filename or a filename relative to the - root path. - :param silent: set to ``True`` if you want silent failure for missing - files. - :return: ``True`` if the file was loaded successfully. - - .. versionadded:: 0.7 - `silent` parameter. - """ - filename = os.path.join(self.root_path, filename) - d = types.ModuleType("config") - d.__file__ = filename - try: - with open(filename, mode="rb") as config_file: - exec(compile(config_file.read(), filename, "exec"), d.__dict__) - except OSError as e: - if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR): - return False - e.strerror = f"Unable to load configuration file ({e.strerror})" - raise - self.from_object(d) - return True - - def from_object(self, obj: object | str) -> None: - """Updates the values from the given object. An object can be of one - of the following two types: - - - a string: in this case the object with that name will be imported - - an actual object reference: that object is used directly - - Objects are usually either modules or classes. :meth:`from_object` - loads only the uppercase attributes of the module/class. A ``dict`` - object will not work with :meth:`from_object` because the keys of a - ``dict`` are not attributes of the ``dict`` class. - - Example of module-based configuration:: - - app.config.from_object('yourapplication.default_config') - from yourapplication import default_config - app.config.from_object(default_config) - - Nothing is done to the object before loading. If the object is a - class and has ``@property`` attributes, it needs to be - instantiated before being passed to this method. - - You should not use this function to load the actual configuration but - rather configuration defaults. The actual config should be loaded - with :meth:`from_pyfile` and ideally from a location not within the - package because the package might be installed system wide. - - See :ref:`config-dev-prod` for an example of class-based configuration - using :meth:`from_object`. - - :param obj: an import name or object - """ - if isinstance(obj, str): - obj = import_string(obj) - for key in dir(obj): - if key.isupper(): - self[key] = getattr(obj, key) - - def from_file( - self, - filename: str | os.PathLike[str], - load: t.Callable[[t.IO[t.Any]], t.Mapping[str, t.Any]], - silent: bool = False, - text: bool = True, - ) -> bool: - """Update the values in the config from a file that is loaded - using the ``load`` parameter. The loaded data is passed to the - :meth:`from_mapping` method. - - .. code-block:: python - - import json - app.config.from_file("config.json", load=json.load) - - import tomllib - app.config.from_file("config.toml", load=tomllib.load, text=False) - - :param filename: The path to the data file. This can be an - absolute path or relative to the config root path. - :param load: A callable that takes a file handle and returns a - mapping of loaded data from the file. - :type load: ``Callable[[Reader], Mapping]`` where ``Reader`` - implements a ``read`` method. - :param silent: Ignore the file if it doesn't exist. - :param text: Open the file in text or binary mode. - :return: ``True`` if the file was loaded successfully. - - .. versionchanged:: 2.3 - The ``text`` parameter was added. - - .. versionadded:: 2.0 - """ - filename = os.path.join(self.root_path, filename) - - try: - with open(filename, "r" if text else "rb") as f: - obj = load(f) - except OSError as e: - if silent and e.errno in (errno.ENOENT, errno.EISDIR): - return False - - e.strerror = f"Unable to load configuration file ({e.strerror})" - raise - - return self.from_mapping(obj) - - def from_mapping( - self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any - ) -> bool: - """Updates the config like :meth:`update` ignoring items with - non-upper keys. - - :return: Always returns ``True``. - - .. versionadded:: 0.11 - """ - mappings: dict[str, t.Any] = {} - if mapping is not None: - mappings.update(mapping) - mappings.update(kwargs) - for key, value in mappings.items(): - if key.isupper(): - self[key] = value - return True - - def get_namespace( - self, namespace: str, lowercase: bool = True, trim_namespace: bool = True - ) -> dict[str, t.Any]: - """Returns a dictionary containing a subset of configuration options - that match the specified namespace/prefix. Example usage:: - - app.config['IMAGE_STORE_TYPE'] = 'fs' - app.config['IMAGE_STORE_PATH'] = '/var/app/images' - app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com' - image_store_config = app.config.get_namespace('IMAGE_STORE_') - - The resulting dictionary `image_store_config` would look like:: - - { - 'type': 'fs', - 'path': '/var/app/images', - 'base_url': 'http://img.website.com' - } - - This is often useful when configuration options map directly to - keyword arguments in functions or class constructors. - - :param namespace: a configuration namespace - :param lowercase: a flag indicating if the keys of the resulting - dictionary should be lowercase - :param trim_namespace: a flag indicating if the keys of the resulting - dictionary should not include the namespace - - .. versionadded:: 0.11 - """ - rv = {} - for k, v in self.items(): - if not k.startswith(namespace): - continue - if trim_namespace: - key = k[len(namespace) :] - else: - key = k - if lowercase: - key = key.lower() - rv[key] = v - return rv - - def __repr__(self) -> str: - return f"<{type(self).__name__} {dict.__repr__(self)}>" diff --git a/src/flask/ctx.py b/src/flask/ctx.py deleted file mode 100644 index d4d0de65..00000000 --- a/src/flask/ctx.py +++ /dev/null @@ -1,540 +0,0 @@ -from __future__ import annotations - -import contextvars -import typing as t -from functools import update_wrapper -from types import TracebackType - -from werkzeug.exceptions import HTTPException -from werkzeug.routing import MapAdapter - -from . import typing as ft -from .globals import _cv_app -from .helpers import _CollectErrors -from .signals import appcontext_popped -from .signals import appcontext_pushed - -if t.TYPE_CHECKING: - import typing_extensions as te - from _typeshed.wsgi import WSGIEnvironment - - from .app import Flask - from .sessions import SessionMixin - from .wrappers import Request - - -# a singleton sentinel value for parameter defaults -_sentinel = object() - - -class _AppCtxGlobals: - """A plain object. Used as a namespace for storing data during an - application context. - - Creating an app context automatically creates this object, which is - made available as the :data:`.g` proxy. - - .. describe:: 'key' in g - - Check whether an attribute is present. - - .. versionadded:: 0.10 - - .. describe:: iter(g) - - Return an iterator over the attribute names. - - .. versionadded:: 0.10 - """ - - # Define attr methods to let mypy know this is a namespace object - # that has arbitrary attributes. - - def __getattr__(self, name: str) -> t.Any: - try: - return self.__dict__[name] - except KeyError: - raise AttributeError(name) from None - - def __setattr__(self, name: str, value: t.Any) -> None: - self.__dict__[name] = value - - def __delattr__(self, name: str) -> None: - try: - del self.__dict__[name] - except KeyError: - raise AttributeError(name) from None - - def get(self, name: str, default: t.Any | None = None) -> t.Any: - """Get an attribute by name, or a default value. Like - :meth:`dict.get`. - - :param name: Name of attribute to get. - :param default: Value to return if the attribute is not present. - - .. versionadded:: 0.10 - """ - return self.__dict__.get(name, default) - - def pop(self, name: str, default: t.Any = _sentinel) -> t.Any: - """Get and remove an attribute by name. Like :meth:`dict.pop`. - - :param name: Name of attribute to pop. - :param default: Value to return if the attribute is not present, - instead of raising a ``KeyError``. - - .. versionadded:: 0.11 - """ - if default is _sentinel: - return self.__dict__.pop(name) - else: - return self.__dict__.pop(name, default) - - def setdefault(self, name: str, default: t.Any = None) -> t.Any: - """Get the value of an attribute if it is present, otherwise - set and return a default value. Like :meth:`dict.setdefault`. - - :param name: Name of attribute to get. - :param default: Value to set and return if the attribute is not - present. - - .. versionadded:: 0.11 - """ - return self.__dict__.setdefault(name, default) - - def __contains__(self, item: str) -> bool: - return item in self.__dict__ - - def __iter__(self) -> t.Iterator[str]: - return iter(self.__dict__) - - def __repr__(self) -> str: - ctx = _cv_app.get(None) - if ctx is not None: - return f"" - return object.__repr__(self) - - -def after_this_request( - f: ft.AfterRequestCallable[t.Any], -) -> ft.AfterRequestCallable[t.Any]: - """Decorate a function to run after the current request. The behavior is the - same as :meth:`.Flask.after_request`, except it only applies to the current - request, rather than every request. Therefore, it must be used within a - request context, rather than during setup. - - .. code-block:: python - - @app.route("/") - def index(): - @after_this_request - def add_header(response): - response.headers["X-Foo"] = "Parachute" - return response - - return "Hello, World!" - - .. versionadded:: 0.9 - """ - ctx = _cv_app.get(None) - - if ctx is None or not ctx.has_request: - raise RuntimeError( - "'after_this_request' can only be used when a request" - " context is active, such as in a view function." - ) - - ctx._after_request_functions.append(f) - return f - - -F = t.TypeVar("F", bound=t.Callable[..., t.Any]) - - -def copy_current_request_context(f: F) -> F: - """Decorate a function to run inside the current request context. This can - be used when starting a background task, otherwise it will not see the app - and request objects that were active in the parent. - - .. warning:: - - Due to the following caveats, it is often safer (and simpler) to pass - the data you need when starting the task, rather than using this and - relying on the context objects. - - In order to avoid execution switching partially though reading data, either - read the request body (access ``form``, ``json``, ``data``, etc) before - starting the task, or use a lock. This can be an issue when using threading, - but shouldn't be an issue when using greenlet/gevent or asyncio. - - If the task will access ``session``, be sure to do so in the parent as well - so that the ``Vary: cookie`` header will be set. Modifying ``session`` in - the task should be avoided, as it may execute after the response cookie has - already been written. - - .. code-block:: python - - import gevent - from flask import copy_current_request_context - - @app.route('/') - def index(): - @copy_current_request_context - def do_some_work(): - # do some work here, it can access flask.request or - # flask.session like you would otherwise in the view function. - ... - gevent.spawn(do_some_work) - return 'Regular response' - - .. versionadded:: 0.10 - """ - # Store the context that was active when the decorator was applied. - original = _cv_app.get(None) - - if original is None: - raise RuntimeError( - "'copy_current_request_context' can only be used when a" - " request context is active, such as in a view function." - ) - - def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any: - # Copy the context before pushing, so each worker acts independently. - with original.copy() as ctx: - return ctx.app.ensure_sync(f)(*args, **kwargs) - - return update_wrapper(wrapper, f) # type: ignore[return-value] - - -def has_request_context() -> bool: - """Test if an app context is active and if it has request information. - - .. code-block:: python - - from flask import has_request_context, request - - if has_request_context(): - remote_addr = request.remote_addr - - If a request context is active, the :data:`.request` and :data:`.session` - context proxies will available and ``True``, otherwise ``False``. You can - use that to test the data you use, rather than using this function. - - .. code-block:: python - - from flask import request - - if request: - remote_addr = request.remote_addr - - .. versionadded:: 0.7 - """ - return (ctx := _cv_app.get(None)) is not None and ctx.has_request - - -def has_app_context() -> bool: - """Test if an app context is active. Unlike :func:`has_request_context` - this can be true outside a request, such as in a CLI command. - - .. code-block:: python - - from flask import has_app_context, g - - if has_app_context(): - g.cached_data = ... - - If an app context is active, the :data:`.g` and :data:`.current_app` context - proxies will available and ``True``, otherwise ``False``. You can use that - to test the data you use, rather than using this function. - - from flask import g - - if g: - g.cached_data = ... - - .. versionadded:: 0.9 - """ - return _cv_app.get(None) is not None - - -class AppContext: - """An app context contains information about an app, and about the request - when handling a request. A context is pushed at the beginning of each - request and CLI command, and popped at the end. The context is referred to - as a "request context" if it has request information, and an "app context" - if not. - - Do not use this class directly. Use :meth:`.Flask.app_context` to create an - app context if needed during setup, and :meth:`.Flask.test_request_context` - to create a request context if needed during tests. - - When the context is popped, it will evaluate all the teardown functions - registered with :meth:`~flask.Flask.teardown_request` (if handling a - request) then :meth:`.Flask.teardown_appcontext`. - - When using the interactive debugger, the context will be restored so - ``request`` is still accessible. Similarly, the test client can preserve the - context after the request ends. However, teardown functions may already have - closed some resources such as database connections, and will run again when - the restored context is popped. - - :param app: The application this context represents. - :param request: The request data this context represents. - :param session: The session data this context represents. If not given, - loaded from the request on first access. - - .. versionchanged:: 3.2 - Merged with ``RequestContext``. The ``RequestContext`` alias will be - removed in Flask 4.0. - - .. versionchanged:: 3.2 - A combined app and request context is pushed for every request and CLI - command, rather than trying to detect if an app context is already - pushed. - - .. versionchanged:: 3.2 - The session is loaded the first time it is accessed, rather than when - the context is pushed. - """ - - def __init__( - self, - app: Flask, - *, - request: Request | None = None, - session: SessionMixin | None = None, - ) -> None: - self.app = app - """The application represented by this context. Accessed through - :data:`.current_app`. - """ - - self.g: _AppCtxGlobals = app.app_ctx_globals_class() - """The global data for this context. Accessed through :data:`.g`.""" - - self.url_adapter: MapAdapter | None = None - """The URL adapter bound to the request, or the app if not in a request. - May be ``None`` if binding failed. - """ - - self._request: Request | None = request - self._session: SessionMixin | None = session - self._flashes: list[tuple[str, str]] | None = None - self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = [] - - try: - self.url_adapter = app.create_url_adapter(self._request) - except HTTPException as e: - if self._request is not None: - self._request.routing_exception = e - - self._cv_token: contextvars.Token[AppContext] | None = None - """The previous state to restore when popping.""" - - self._push_count: int = 0 - """Track nested pushes of this context. Cleanup will only run once the - original push has been popped. - """ - - @classmethod - def from_environ(cls, app: Flask, environ: WSGIEnvironment, /) -> te.Self: - """Create an app context with request data from the given WSGI environ. - - :param app: The application this context represents. - :param environ: The request data this context represents. - """ - request = app.request_class(environ) - request.json_module = app.json - return cls(app, request=request) - - @property - def has_request(self) -> bool: - """True if this context was created with request data.""" - return self._request is not None - - def copy(self) -> te.Self: - """Create a new context with the same data objects as this context. See - :func:`.copy_current_request_context`. - - .. versionchanged:: 1.1 - The current session data is used instead of reloading the original data. - - .. versionadded:: 0.10 - """ - return self.__class__( - self.app, - request=self._request, - session=self._session, - ) - - @property - def request(self) -> Request: - """The request object associated with this context. Accessed through - :data:`.request`. Only available in request contexts, otherwise raises - :exc:`RuntimeError`. - """ - if self._request is None: - raise RuntimeError("There is no request in this context.") - - return self._request - - def _get_session(self) -> SessionMixin: - """Open the session if it is not already open for this request context.""" - if self._request is None: - raise RuntimeError("There is no request in this context.") - - if self._session is None: - si = self.app.session_interface - self._session = si.open_session(self.app, self.request) - - if self._session is None: - self._session = si.make_null_session(self.app) - - return self._session - - @property - def session(self) -> SessionMixin: - """The session object associated with this context. Accessed through - :data:`.session`. Only available in request contexts, otherwise raises - :exc:`RuntimeError`. Accessing this sets :attr:`.SessionMixin.accessed`. - """ - session = self._get_session() - session.accessed = True - return session - - def match_request(self) -> None: - """Apply routing to the current request, storing either the matched - endpoint and args, or a routing exception. - """ - try: - result = self.url_adapter.match(return_rule=True) # type: ignore[union-attr] - except HTTPException as e: - self._request.routing_exception = e # type: ignore[union-attr] - else: - self._request.url_rule, self._request.view_args = result # type: ignore[union-attr] - - def push(self) -> None: - """Push this context so that it is the active context. If this is a - request context, calls :meth:`match_request` to perform routing with - the context active. - - Typically, this is not used directly. Instead, use a ``with`` block - to manage the context. - - In some situations, such as streaming or testing, the context may be - pushed multiple times. It will only trigger matching and signals if it - is not currently pushed. - """ - self._push_count += 1 - - if self._cv_token is not None: - return - - self._cv_token = _cv_app.set(self) - appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync) - - if self._request is not None: - # Open the session at the moment that the request context is available. - # This allows a custom open_session method to use the request context. - self._get_session() - - # Match the request URL after loading the session, so that the - # session is available in custom URL converters. - if self.url_adapter is not None: - self.match_request() - - def pop(self, exc: BaseException | None = None) -> None: - """Pop this context so that it is no longer the active context. Then - call teardown functions and signals. - - Typically, this is not used directly. Instead, use a ``with`` block - to manage the context. - - This context must currently be the active context, otherwise a - :exc:`RuntimeError` is raised. In some situations, such as streaming or - testing, the context may have been pushed multiple times. It will only - trigger cleanup once it has been popped as many times as it was pushed. - Until then, it will remain the active context. - - :param exc: An unhandled exception that was raised while the context was - active. Passed to teardown functions. - - .. versionchanged:: 0.9 - Added the ``exc`` argument. - """ - if self._cv_token is None: - raise RuntimeError(f"Cannot pop this context ({self!r}), it is not pushed.") - - ctx = _cv_app.get(None) - - if ctx is None or self._cv_token is None: - raise RuntimeError( - f"Cannot pop this context ({self!r}), there is no active context." - ) - - if ctx is not self: - raise RuntimeError( - f"Cannot pop this context ({self!r}), it is not the active" - f" context ({ctx!r})." - ) - - self._push_count -= 1 - - if self._push_count > 0: - return - - collect_errors = _CollectErrors() - - if self._request is not None: - with collect_errors: - self.app.do_teardown_request(self, exc) - - with collect_errors: - self._request.close() - - with collect_errors: - self.app.do_teardown_appcontext(self, exc) - - _cv_app.reset(self._cv_token) - self._cv_token = None - - with collect_errors: - appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync) - - collect_errors.raise_any("Errors during context teardown") - - def __enter__(self) -> te.Self: - self.push() - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - tb: TracebackType | None, - ) -> None: - self.pop(exc_value) - - def __repr__(self) -> str: - if self._request is not None: - return ( - f"<{type(self).__name__} {id(self)} of {self.app.name}," - f" {self.request.method} {self.request.url!r}>" - ) - - return f"<{type(self).__name__} {id(self)} of {self.app.name}>" - - -def __getattr__(name: str) -> t.Any: - import warnings - - if name == "RequestContext": - warnings.warn( - "'RequestContext' has merged with 'AppContext', and will be removed" - " in Flask 4.0. Use 'AppContext' instead.", - DeprecationWarning, - stacklevel=2, - ) - return AppContext - - raise AttributeError(name) diff --git a/src/flask/debughelpers.py b/src/flask/debughelpers.py deleted file mode 100644 index 61884e1a..00000000 --- a/src/flask/debughelpers.py +++ /dev/null @@ -1,179 +0,0 @@ -from __future__ import annotations - -import typing as t - -from jinja2.loaders import BaseLoader -from werkzeug.routing import RequestRedirect - -from .blueprints import Blueprint -from .globals import _cv_app -from .sansio.app import App - -if t.TYPE_CHECKING: - from .sansio.scaffold import Scaffold - from .wrappers import Request - - -class UnexpectedUnicodeError(AssertionError, UnicodeError): - """Raised in places where we want some better error reporting for - unexpected unicode or binary data. - """ - - -class DebugFilesKeyError(KeyError, AssertionError): - """Raised from request.files during debugging. The idea is that it can - provide a better error message than just a generic KeyError/BadRequest. - """ - - def __init__(self, request: Request, key: str) -> None: - form_matches = request.form.getlist(key) - buf = [ - f"You tried to access the file {key!r} in the request.files" - " dictionary but it does not exist. The mimetype for the" - f" request is {request.mimetype!r} instead of" - " 'multipart/form-data' which means that no file contents" - " were transmitted. To fix this error you should provide" - ' enctype="multipart/form-data" in your form.' - ] - if form_matches: - names = ", ".join(repr(x) for x in form_matches) - buf.append( - "\n\nThe browser instead transmitted some file names. " - f"This was submitted: {names}" - ) - self.msg = "".join(buf) - - def __str__(self) -> str: - return self.msg - - -class FormDataRoutingRedirect(AssertionError): - """This exception is raised in debug mode if a routing redirect - would cause the browser to drop the method or body. This happens - when method is not GET, HEAD or OPTIONS and the status code is not - 307 or 308. - """ - - def __init__(self, request: Request) -> None: - exc = request.routing_exception - assert isinstance(exc, RequestRedirect) - buf = [ - f"A request was sent to '{request.url}', but routing issued" - f" a redirect to the canonical URL '{exc.new_url}'." - ] - - if f"{request.base_url}/" == exc.new_url.partition("?")[0]: - buf.append( - " The URL was defined with a trailing slash. Flask" - " will redirect to the URL with a trailing slash if it" - " was accessed without one." - ) - - buf.append( - " Send requests to the canonical URL, or use 307 or 308 for" - " routing redirects. Otherwise, browsers will drop form" - " data.\n\n" - "This exception is only raised in debug mode." - ) - super().__init__("".join(buf)) - - -def attach_enctype_error_multidict(request: Request) -> None: - """Patch ``request.files.__getitem__`` to raise a descriptive error - about ``enctype=multipart/form-data``. - - :param request: The request to patch. - :meta private: - """ - oldcls = request.files.__class__ - - class newcls(oldcls): # type: ignore[valid-type, misc] - def __getitem__(self, key: str) -> t.Any: - try: - return super().__getitem__(key) - except KeyError as e: - if key not in request.form: - raise - - raise DebugFilesKeyError(request, key).with_traceback( - e.__traceback__ - ) from None - - newcls.__name__ = oldcls.__name__ - newcls.__module__ = oldcls.__module__ - request.files.__class__ = newcls - - -def _dump_loader_info(loader: BaseLoader) -> t.Iterator[str]: - yield f"class: {type(loader).__module__}.{type(loader).__name__}" - for key, value in sorted(loader.__dict__.items()): - if key.startswith("_"): - continue - if isinstance(value, (tuple, list)): - if not all(isinstance(x, str) for x in value): - continue - yield f"{key}:" - for item in value: - yield f" - {item}" - continue - elif not isinstance(value, (str, int, float, bool)): - continue - yield f"{key}: {value!r}" - - -def explain_template_loading_attempts( - app: App, - template: str, - attempts: list[ - tuple[ - BaseLoader, - Scaffold, - tuple[str, str | None, t.Callable[[], bool] | None] | None, - ] - ], -) -> None: - """This should help developers understand what failed""" - info = [f"Locating template {template!r}:"] - total_found = 0 - blueprint = None - - if (ctx := _cv_app.get(None)) is not None and ctx.has_request: - blueprint = ctx.request.blueprint - - for idx, (loader, srcobj, triple) in enumerate(attempts): - if isinstance(srcobj, App): - src_info = f"application {srcobj.import_name!r}" - elif isinstance(srcobj, Blueprint): - src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})" - else: - src_info = repr(srcobj) - - info.append(f"{idx + 1:5}: trying loader of {src_info}") - - for line in _dump_loader_info(loader): - info.append(f" {line}") - - if triple is None: - detail = "no match" - else: - detail = f"found ({triple[1] or ''!r})" - total_found += 1 - info.append(f" -> {detail}") - - seems_fishy = False - if total_found == 0: - info.append("Error: the template could not be found.") - seems_fishy = True - elif total_found > 1: - info.append("Warning: multiple loaders returned a match for the template.") - seems_fishy = True - - if blueprint is not None and seems_fishy: - info.append( - " The template was looked up from an endpoint that belongs" - f" to the blueprint {blueprint!r}." - ) - info.append(" Maybe you did not place a template in the right folder?") - info.append(" See https://flask.palletsprojects.com/blueprints/#templates") - - app.logger.info("\n".join(info)) diff --git a/src/flask/globals.py b/src/flask/globals.py deleted file mode 100644 index f4a7298e..00000000 --- a/src/flask/globals.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import annotations - -import typing as t -from contextvars import ContextVar - -from werkzeug.local import LocalProxy - -if t.TYPE_CHECKING: # pragma: no cover - from .app import Flask - from .ctx import _AppCtxGlobals - from .ctx import AppContext - from .sessions import SessionMixin - from .wrappers import Request - - T = t.TypeVar("T", covariant=True) - - class ProxyMixin(t.Protocol[T]): - def _get_current_object(self) -> T: ... - - # These subclasses inform type checkers that the proxy objects look like the - # proxied type along with the _get_current_object method. - class FlaskProxy(ProxyMixin[Flask], Flask): ... - - class AppContextProxy(ProxyMixin[AppContext], AppContext): ... - - class _AppCtxGlobalsProxy(ProxyMixin[_AppCtxGlobals], _AppCtxGlobals): ... - - class RequestProxy(ProxyMixin[Request], Request): ... - - class SessionMixinProxy(ProxyMixin[SessionMixin], SessionMixin): ... - - -_no_app_msg = """\ -Working outside of application context. - -Attempted to use functionality that expected a current application to be set. To -solve this, set up an app context using 'with app.app_context()'. See the -documentation on app context for more information.\ -""" -_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx") -app_ctx: AppContextProxy = LocalProxy( # type: ignore[assignment] - _cv_app, unbound_message=_no_app_msg -) -current_app: FlaskProxy = LocalProxy( # type: ignore[assignment] - _cv_app, "app", unbound_message=_no_app_msg -) -g: _AppCtxGlobalsProxy = LocalProxy( # type: ignore[assignment] - _cv_app, "g", unbound_message=_no_app_msg -) - -_no_req_msg = """\ -Working outside of request context. - -Attempted to use functionality that expected an active HTTP request. See the -documentation on request context for more information.\ -""" -request: RequestProxy = LocalProxy( # type: ignore[assignment] - _cv_app, "request", unbound_message=_no_req_msg -) -session: SessionMixinProxy = LocalProxy( # type: ignore[assignment] - _cv_app, "session", unbound_message=_no_req_msg -) - - -def __getattr__(name: str) -> t.Any: - import warnings - - if name == "request_ctx": - warnings.warn( - "'request_ctx' has merged with 'app_ctx', and will be removed" - " in Flask 4.0. Use 'app_ctx' instead.", - DeprecationWarning, - stacklevel=2, - ) - return app_ctx - - raise AttributeError(name) diff --git a/src/flask/helpers.py b/src/flask/helpers.py deleted file mode 100644 index fb7f6eba..00000000 --- a/src/flask/helpers.py +++ /dev/null @@ -1,682 +0,0 @@ -from __future__ import annotations - -import importlib.util -import os -import sys -import typing as t -from datetime import datetime -from functools import cache -from functools import update_wrapper -from types import TracebackType - -import werkzeug.utils -from werkzeug.exceptions import abort as _wz_abort -from werkzeug.utils import redirect as _wz_redirect -from werkzeug.wrappers import Response as BaseResponse - -from .globals import _cv_app -from .globals import app_ctx -from .globals import current_app -from .globals import request -from .globals import session -from .signals import message_flashed - -if t.TYPE_CHECKING: # pragma: no cover - from .wrappers import Response - - -def get_debug_flag() -> bool: - """Get whether debug mode should be enabled for the app, indicated by the - :envvar:`FLASK_DEBUG` environment variable. The default is ``False``. - """ - val = os.environ.get("FLASK_DEBUG") - return bool(val and val.lower() not in {"0", "false", "no"}) - - -def get_load_dotenv(default: bool = True) -> bool: - """Get whether the user has disabled loading default dotenv files by - setting :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load - the files. - - :param default: What to return if the env var isn't set. - """ - val = os.environ.get("FLASK_SKIP_DOTENV") - - if not val: - return default - - return val.lower() in ("0", "false", "no") - - -@t.overload -def stream_with_context( - generator_or_function: t.Iterator[t.AnyStr], -) -> t.Iterator[t.AnyStr]: ... - - -@t.overload -def stream_with_context( - generator_or_function: t.Callable[..., t.Iterator[t.AnyStr]], -) -> t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: ... - - -def stream_with_context( - generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]], -) -> t.Iterator[t.AnyStr] | t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: - """Wrap a response generator function so that it runs inside the current - request context. This keeps :data:`.request`, :data:`.session`, and :data:`.g` - available, even though at the point the generator runs the request context - will typically have ended. - - .. warning:: - - Due to the following caveat, it is often safer to pass the data you - need as arguments to the generator, rather than relying on the context - objects. - - More headers cannot be sent after the body has begun. Therefore, you must - make sure all headers are set before starting the response. In particular, - if the generator will access ``session``, be sure to do so in the view as - well so that the ``Vary: cookie`` header will be set. Do not modify the - session in the generator, as the ``Set-Cookie`` header will already be sent. - - Use it as a decorator on a generator function: - - .. code-block:: python - - from flask import stream_with_context, request, Response - - @app.get("/stream") - def streamed_response(): - @stream_with_context - def generate(): - yield "Hello " - yield request.args["name"] - yield "!" - - return Response(generate()) - - Or use it as a wrapper around a created generator: - - .. code-block:: python - - from flask import stream_with_context, request, Response - - @app.get("/stream") - def streamed_response(): - def generate(): - yield "Hello " - yield request.args["name"] - yield "!" - - return Response(stream_with_context(generate())) - - .. versionadded:: 0.9 - """ - try: - gen = iter(generator_or_function) # type: ignore[arg-type] - except TypeError: - - def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: - gen = generator_or_function(*args, **kwargs) # type: ignore[operator] - return stream_with_context(gen) - - return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type] - - def generator() -> t.Iterator[t.AnyStr]: - if (ctx := _cv_app.get(None)) is None: - raise RuntimeError( - "'stream_with_context' can only be used when a request" - " context is active, such as in a view function." - ) - - with ctx: - yield None # type: ignore[misc] - - try: - yield from gen - finally: - # Clean up in case the user wrapped a WSGI iterator. - if hasattr(gen, "close"): - gen.close() - - # Execute the generator to the sentinel value. This captures the current - # context and pushes it to preserve it. Further iteration will yield from - # the original iterator. - wrapped_g = generator() - next(wrapped_g) - return wrapped_g - - -def make_response(*args: t.Any) -> Response: - """Sometimes it is necessary to set additional headers in a view. Because - views do not have to return response objects but can return a value that - is converted into a response object by Flask itself, it becomes tricky to - add headers to it. This function can be called instead of using a return - and you will get a response object which you can use to attach headers. - - If view looked like this and you want to add a new header:: - - def index(): - return render_template('index.html', foo=42) - - You can now do something like this:: - - def index(): - response = make_response(render_template('index.html', foo=42)) - response.headers['X-Parachutes'] = 'parachutes are cool' - return response - - This function accepts the very same arguments you can return from a - view function. This for example creates a response with a 404 error - code:: - - response = make_response(render_template('not_found.html'), 404) - - The other use case of this function is to force the return value of a - view function into a response which is helpful with view - decorators:: - - response = make_response(view_function()) - response.headers['X-Parachutes'] = 'parachutes are cool' - - Internally this function does the following things: - - - if no arguments are passed, it creates a new response argument - - if one argument is passed, :meth:`flask.Flask.make_response` - is invoked with it. - - if more than one argument is passed, the arguments are passed - to the :meth:`flask.Flask.make_response` function as tuple. - - .. versionadded:: 0.6 - """ - if not args: - return current_app.response_class() - if len(args) == 1: - args = args[0] - return current_app.make_response(args) - - -def url_for( - endpoint: str, - *, - _anchor: str | None = None, - _method: str | None = None, - _scheme: str | None = None, - _external: bool | None = None, - **values: t.Any, -) -> str: - """Generate a URL to the given endpoint with the given values. - - This requires an active request or application context, and calls - :meth:`current_app.url_for() `. See that method - for full documentation. - - :param endpoint: The endpoint name associated with the URL to - generate. If this starts with a ``.``, the current blueprint - name (if any) will be used. - :param _anchor: If given, append this as ``#anchor`` to the URL. - :param _method: If given, generate the URL associated with this - method for the endpoint. - :param _scheme: If given, the URL will have this scheme if it is - external. - :param _external: If given, prefer the URL to be internal (False) or - require it to be external (True). External URLs include the - scheme and domain. When not in an active request, URLs are - external by default. - :param values: Values to use for the variable parts of the URL rule. - Unknown keys are appended as query string arguments, like - ``?a=b&c=d``. - - .. versionchanged:: 2.2 - Calls ``current_app.url_for``, allowing an app to override the - behavior. - - .. versionchanged:: 0.10 - The ``_scheme`` parameter was added. - - .. versionchanged:: 0.9 - The ``_anchor`` and ``_method`` parameters were added. - - .. versionchanged:: 0.9 - Calls ``app.handle_url_build_error`` on build errors. - """ - return current_app.url_for( - endpoint, - _anchor=_anchor, - _method=_method, - _scheme=_scheme, - _external=_external, - **values, - ) - - -def redirect( - location: str, code: int = 303, Response: type[BaseResponse] | None = None -) -> BaseResponse: - """Create a redirect response object. - - If :data:`~flask.current_app` is available, it will use its - :meth:`~flask.Flask.redirect` method, otherwise it will use - :func:`werkzeug.utils.redirect`. - - :param location: The URL to redirect to. - :param code: The status code for the redirect. - :param Response: The response class to use. Not used when - ``current_app`` is active, which uses ``app.response_class``. - - .. versionchanged:: 3.2 - ``code`` defaults to ``303`` instead of ``302``. - - .. versionadded:: 2.2 - Calls ``current_app.redirect`` if available instead of always - using Werkzeug's default ``redirect``. - """ - if (ctx := _cv_app.get(None)) is not None: - return ctx.app.redirect(location, code=code) - - return _wz_redirect(location, code=code, Response=Response) - - -def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: - """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given - status code. - - If :data:`~flask.current_app` is available, it will call its - :attr:`~flask.Flask.aborter` object, otherwise it will use - :func:`werkzeug.exceptions.abort`. - - :param code: The status code for the exception, which must be - registered in ``app.aborter``. - :param args: Passed to the exception. - :param kwargs: Passed to the exception. - - .. versionadded:: 2.2 - Calls ``current_app.aborter`` if available instead of always - using Werkzeug's default ``abort``. - """ - if (ctx := _cv_app.get(None)) is not None: - ctx.app.aborter(code, *args, **kwargs) - - _wz_abort(code, *args, **kwargs) - - -def get_template_attribute(template_name: str, attribute: str) -> t.Any: - """Loads a macro (or variable) a template exports. This can be used to - invoke a macro from within Python code. If you for example have a - template named :file:`_cider.html` with the following contents: - - .. sourcecode:: html+jinja - - {% macro hello(name) %}Hello {{ name }}!{% endmacro %} - - You can access this from Python code like this:: - - hello = get_template_attribute('_cider.html', 'hello') - return hello('World') - - .. versionadded:: 0.2 - - :param template_name: the name of the template - :param attribute: the name of the variable of macro to access - """ - return getattr(current_app.jinja_env.get_template(template_name).module, attribute) - - -def flash(message: str, category: str = "message") -> None: - """Flashes a message to the next request. In order to remove the - flashed message from the session and to display it to the user, - the template has to call :func:`get_flashed_messages`. - - .. versionchanged:: 0.3 - `category` parameter added. - - :param message: the message to be flashed. - :param category: the category for the message. The following values - are recommended: ``'message'`` for any kind of message, - ``'error'`` for errors, ``'info'`` for information - messages and ``'warning'`` for warnings. However any - kind of string can be used as category. - """ - # Original implementation: - # - # session.setdefault('_flashes', []).append((category, message)) - # - # This assumed that changes made to mutable structures in the session are - # always in sync with the session object, which is not true for session - # implementations that use external storage for keeping their keys/values. - flashes = session.get("_flashes", []) - flashes.append((category, message)) - session["_flashes"] = flashes - app = current_app._get_current_object() - message_flashed.send( - app, - _async_wrapper=app.ensure_sync, - message=message, - category=category, - ) - - -def get_flashed_messages( - with_categories: bool = False, category_filter: t.Iterable[str] = () -) -> list[str] | list[tuple[str, str]]: - """Pulls all flashed messages from the session and returns them. - Further calls in the same request to the function will return - the same messages. By default just the messages are returned, - but when `with_categories` is set to ``True``, the return value will - be a list of tuples in the form ``(category, message)`` instead. - - Filter the flashed messages to one or more categories by providing those - categories in `category_filter`. This allows rendering categories in - separate html blocks. The `with_categories` and `category_filter` - arguments are distinct: - - * `with_categories` controls whether categories are returned with message - text (``True`` gives a tuple, where ``False`` gives just the message text). - * `category_filter` filters the messages down to only those matching the - provided categories. - - See :doc:`/patterns/flashing` for examples. - - .. versionchanged:: 0.3 - `with_categories` parameter added. - - .. versionchanged:: 0.9 - `category_filter` parameter added. - - :param with_categories: set to ``True`` to also receive categories. - :param category_filter: filter of categories to limit return values. Only - categories in the list will be returned. - """ - flashes = app_ctx._flashes - if flashes is None: - flashes = session.pop("_flashes") if "_flashes" in session else [] - app_ctx._flashes = flashes - if category_filter: - flashes = list(filter(lambda f: f[0] in category_filter, flashes)) - if not with_categories: - return [x[1] for x in flashes] - return flashes - - -def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]: - ctx = app_ctx._get_current_object() - - if kwargs.get("max_age") is None: - kwargs["max_age"] = ctx.app.get_send_file_max_age - - kwargs.update( - environ=ctx.request.environ, - use_x_sendfile=ctx.app.config["USE_X_SENDFILE"], - response_class=ctx.app.response_class, - _root_path=ctx.app.root_path, - ) - return kwargs - - -def send_file( - path_or_file: os.PathLike[t.AnyStr] | str | t.IO[bytes], - mimetype: str | None = None, - as_attachment: bool = False, - download_name: str | None = None, - conditional: bool = True, - etag: bool | str = True, - last_modified: datetime | int | float | None = None, - max_age: None | (int | t.Callable[[str | None], int | None]) = None, -) -> Response: - """Send the contents of a file to the client. - - The first argument can be a file path or a file-like object. Paths - are preferred in most cases because Werkzeug can manage the file and - get extra information from the path. Passing a file-like object - requires that the file is opened in binary mode, and is mostly - useful when building a file in memory with :class:`io.BytesIO`. - - Never pass file paths provided by a user. The path is assumed to be - trusted, so a user could craft a path to access a file you didn't - intend. Use :func:`send_from_directory` to safely serve - user-requested paths from within a directory. - - If the WSGI server sets a ``file_wrapper`` in ``environ``, it is - used, otherwise Werkzeug's built-in wrapper is used. Alternatively, - if the HTTP server supports ``X-Sendfile``, configuring Flask with - ``USE_X_SENDFILE = True`` will tell the server to send the given - path, which is much more efficient than reading it in Python. - - :param path_or_file: The path to the file to send, relative to the - current working directory if a relative path is given. - Alternatively, a file-like object opened in binary mode. Make - sure the file pointer is seeked to the start of the data. - :param mimetype: The MIME type to send for the file. If not - provided, it will try to detect it from the file name. - :param as_attachment: Indicate to a browser that it should offer to - save the file instead of displaying it. - :param download_name: The default name browsers will use when saving - the file. Defaults to the passed file name. - :param conditional: Enable conditional and range responses based on - request headers. Requires passing a file path and ``environ``. - :param etag: Calculate an ETag for the file, which requires passing - a file path. Can also be a string to use instead. - :param last_modified: The last modified time to send for the file, - in seconds. If not provided, it will try to detect it from the - file path. - :param max_age: How long the client should cache the file, in - seconds. If set, ``Cache-Control`` will be ``public``, otherwise - it will be ``no-cache`` to prefer conditional caching. - - .. versionchanged:: 2.0 - ``download_name`` replaces the ``attachment_filename`` - parameter. If ``as_attachment=False``, it is passed with - ``Content-Disposition: inline`` instead. - - .. versionchanged:: 2.0 - ``max_age`` replaces the ``cache_timeout`` parameter. - ``conditional`` is enabled and ``max_age`` is not set by - default. - - .. versionchanged:: 2.0 - ``etag`` replaces the ``add_etags`` parameter. It can be a - string to use instead of generating one. - - .. versionchanged:: 2.0 - Passing a file-like object that inherits from - :class:`~io.TextIOBase` will raise a :exc:`ValueError` rather - than sending an empty file. - - .. versionadded:: 2.0 - Moved the implementation to Werkzeug. This is now a wrapper to - pass some Flask-specific arguments. - - .. versionchanged:: 1.1 - ``filename`` may be a :class:`~os.PathLike` object. - - .. versionchanged:: 1.1 - Passing a :class:`~io.BytesIO` object supports range requests. - - .. versionchanged:: 1.0.3 - Filenames are encoded with ASCII instead of Latin-1 for broader - compatibility with WSGI servers. - - .. versionchanged:: 1.0 - UTF-8 filenames as specified in :rfc:`2231` are supported. - - .. versionchanged:: 0.12 - The filename is no longer automatically inferred from file - objects. If you want to use automatic MIME and etag support, - pass a filename via ``filename_or_fp`` or - ``attachment_filename``. - - .. versionchanged:: 0.12 - ``attachment_filename`` is preferred over ``filename`` for MIME - detection. - - .. versionchanged:: 0.9 - ``cache_timeout`` defaults to - :meth:`Flask.get_send_file_max_age`. - - .. versionchanged:: 0.7 - MIME guessing and etag support for file-like objects was - removed because it was unreliable. Pass a filename if you are - able to, otherwise attach an etag yourself. - - .. versionchanged:: 0.5 - The ``add_etags``, ``cache_timeout`` and ``conditional`` - parameters were added. The default behavior is to add etags. - - .. versionadded:: 0.2 - """ - return werkzeug.utils.send_file( # type: ignore[return-value] - **_prepare_send_file_kwargs( - path_or_file=path_or_file, - environ=request.environ, - mimetype=mimetype, - as_attachment=as_attachment, - download_name=download_name, - conditional=conditional, - etag=etag, - last_modified=last_modified, - max_age=max_age, - ) - ) - - -def send_from_directory( - directory: os.PathLike[str] | str, - path: os.PathLike[str] | str, - **kwargs: t.Any, -) -> Response: - """Send a file from within a directory using :func:`send_file`. - - .. code-block:: python - - @app.route("/uploads/") - def download_file(name): - return send_from_directory( - app.config['UPLOAD_FOLDER'], name, as_attachment=True - ) - - This is a secure way to serve files from a folder, such as static - files or uploads. Uses :func:`~werkzeug.security.safe_join` to - ensure the path coming from the client is not maliciously crafted to - point outside the specified directory. - - If the final path does not point to an existing regular file, - raises a 404 :exc:`~werkzeug.exceptions.NotFound` error. - - :param directory: The directory that ``path`` must be located under, - relative to the current application's root path. This *must not* - be a value provided by the client, otherwise it becomes insecure. - :param path: The path to the file to send, relative to - ``directory``. - :param kwargs: Arguments to pass to :func:`send_file`. - - .. versionchanged:: 2.0 - ``path`` replaces the ``filename`` parameter. - - .. versionadded:: 2.0 - Moved the implementation to Werkzeug. This is now a wrapper to - pass some Flask-specific arguments. - - .. versionadded:: 0.5 - """ - return werkzeug.utils.send_from_directory( # type: ignore[return-value] - directory, path, **_prepare_send_file_kwargs(**kwargs) - ) - - -def get_root_path(import_name: str) -> str: - """Find the root path of a package, or the path that contains a - module. If it cannot be found, returns the current working - directory. - - Not to be confused with the value returned by :func:`find_package`. - - :meta private: - """ - # Module already imported and has a file attribute. Use that first. - mod = sys.modules.get(import_name) - - if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None: - return os.path.dirname(os.path.abspath(mod.__file__)) - - # Next attempt: check the loader. - try: - spec = importlib.util.find_spec(import_name) - - if spec is None: - raise ValueError - except (ImportError, ValueError): - loader = None - else: - loader = spec.loader - - # Loader does not exist or we're referring to an unloaded main - # module or a main module without path (interactive sessions), go - # with the current working directory. - if loader is None: - return os.getcwd() - - if hasattr(loader, "get_filename"): - filepath = loader.get_filename(import_name) # pyright: ignore - else: - # Fall back to imports. - __import__(import_name) - mod = sys.modules[import_name] - filepath = getattr(mod, "__file__", None) - - # If we don't have a file path it might be because it is a - # namespace package. In this case pick the root path from the - # first module that is contained in the package. - if filepath is None: - raise RuntimeError( - "No root path can be found for the provided module" - f" {import_name!r}. This can happen because the module" - " came from an import hook that does not provide file" - " name information or because it's a namespace package." - " In this case the root path needs to be explicitly" - " provided." - ) - - # filepath is import_name.py for a module, or __init__.py for a package. - return os.path.dirname(os.path.abspath(filepath)) # type: ignore[no-any-return] - - -@cache -def _split_blueprint_path(name: str) -> list[str]: - out: list[str] = [name] - - if "." in name: - out.extend(_split_blueprint_path(name.rpartition(".")[0])) - - return out - - -class _CollectErrors: - """A context manager that records and silences an error raised within it. - Used to run all teardown functions, then raise any errors afterward. - """ - - def __init__(self) -> None: - self.errors: list[BaseException] = [] - - def __enter__(self) -> None: - pass - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool: - if exc_val is not None: - self.errors.append(exc_val) - - return True - - def raise_any(self, message: str) -> None: - """Raise if any errors were collected.""" - if self.errors: - if sys.version_info >= (3, 11): - raise BaseExceptionGroup(message, self.errors) # noqa: F821 - else: - raise self.errors[0] diff --git a/src/flask/json/__init__.py b/src/flask/json/__init__.py deleted file mode 100644 index 742812f2..00000000 --- a/src/flask/json/__init__.py +++ /dev/null @@ -1,170 +0,0 @@ -from __future__ import annotations - -import json as _json -import typing as t - -from ..globals import current_app -from .provider import _default - -if t.TYPE_CHECKING: # pragma: no cover - from ..wrappers import Response - - -def dumps(obj: t.Any, **kwargs: t.Any) -> str: - """Serialize data as JSON. - - If :data:`~flask.current_app` is available, it will use its - :meth:`app.json.dumps() ` - method, otherwise it will use :func:`json.dumps`. - - :param obj: The data to serialize. - :param kwargs: Arguments passed to the ``dumps`` implementation. - - .. versionchanged:: 2.3 - The ``app`` parameter was removed. - - .. versionchanged:: 2.2 - Calls ``current_app.json.dumps``, allowing an app to override - the behavior. - - .. versionchanged:: 2.0.2 - :class:`decimal.Decimal` is supported by converting to a string. - - .. versionchanged:: 2.0 - ``encoding`` will be removed in Flask 2.1. - - .. versionchanged:: 1.0.3 - ``app`` can be passed directly, rather than requiring an app - context for configuration. - """ - if current_app: - return current_app.json.dumps(obj, **kwargs) - - kwargs.setdefault("default", _default) - return _json.dumps(obj, **kwargs) - - -def dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: - """Serialize data as JSON and write to a file. - - If :data:`~flask.current_app` is available, it will use its - :meth:`app.json.dump() ` - method, otherwise it will use :func:`json.dump`. - - :param obj: The data to serialize. - :param fp: A file opened for writing text. Should use the UTF-8 - encoding to be valid JSON. - :param kwargs: Arguments passed to the ``dump`` implementation. - - .. versionchanged:: 2.3 - The ``app`` parameter was removed. - - .. versionchanged:: 2.2 - Calls ``current_app.json.dump``, allowing an app to override - the behavior. - - .. versionchanged:: 2.0 - Writing to a binary file, and the ``encoding`` argument, will be - removed in Flask 2.1. - """ - if current_app: - current_app.json.dump(obj, fp, **kwargs) - else: - kwargs.setdefault("default", _default) - _json.dump(obj, fp, **kwargs) - - -def loads(s: str | bytes, **kwargs: t.Any) -> t.Any: - """Deserialize data as JSON. - - If :data:`~flask.current_app` is available, it will use its - :meth:`app.json.loads() ` - method, otherwise it will use :func:`json.loads`. - - :param s: Text or UTF-8 bytes. - :param kwargs: Arguments passed to the ``loads`` implementation. - - .. versionchanged:: 2.3 - The ``app`` parameter was removed. - - .. versionchanged:: 2.2 - Calls ``current_app.json.loads``, allowing an app to override - the behavior. - - .. versionchanged:: 2.0 - ``encoding`` will be removed in Flask 2.1. The data must be a - string or UTF-8 bytes. - - .. versionchanged:: 1.0.3 - ``app`` can be passed directly, rather than requiring an app - context for configuration. - """ - if current_app: - return current_app.json.loads(s, **kwargs) - - return _json.loads(s, **kwargs) - - -def load(fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: - """Deserialize data as JSON read from a file. - - If :data:`~flask.current_app` is available, it will use its - :meth:`app.json.load() ` - method, otherwise it will use :func:`json.load`. - - :param fp: A file opened for reading text or UTF-8 bytes. - :param kwargs: Arguments passed to the ``load`` implementation. - - .. versionchanged:: 2.3 - The ``app`` parameter was removed. - - .. versionchanged:: 2.2 - Calls ``current_app.json.load``, allowing an app to override - the behavior. - - .. versionchanged:: 2.2 - The ``app`` parameter will be removed in Flask 2.3. - - .. versionchanged:: 2.0 - ``encoding`` will be removed in Flask 2.1. The file must be text - mode, or binary mode with UTF-8 bytes. - """ - if current_app: - return current_app.json.load(fp, **kwargs) - - return _json.load(fp, **kwargs) - - -def jsonify(*args: t.Any, **kwargs: t.Any) -> Response: - """Serialize the given arguments as JSON, and return a - :class:`~flask.Response` object with the ``application/json`` - mimetype. A dict or list returned from a view will be converted to a - JSON response automatically without needing to call this. - - This requires an active app context, and calls - :meth:`app.json.response() `. - - In debug mode, the output is formatted with indentation to make it - easier to read. This may also be controlled by the provider. - - Either positional or keyword arguments can be given, not both. - If no arguments are given, ``None`` is serialized. - - :param args: A single value to serialize, or multiple values to - treat as a list to serialize. - :param kwargs: Treat as a dict to serialize. - - .. versionchanged:: 2.2 - Calls ``current_app.json.response``, allowing an app to override - the behavior. - - .. versionchanged:: 2.0.2 - :class:`decimal.Decimal` is supported by converting to a string. - - .. versionchanged:: 0.11 - Added support for serializing top-level arrays. This was a - security risk in ancient browsers. See :ref:`security-json`. - - .. versionadded:: 0.2 - """ - return current_app.json.response(*args, **kwargs) # type: ignore[return-value] diff --git a/src/flask/json/provider.py b/src/flask/json/provider.py deleted file mode 100644 index f37cb7b2..00000000 --- a/src/flask/json/provider.py +++ /dev/null @@ -1,215 +0,0 @@ -from __future__ import annotations - -import dataclasses -import decimal -import json -import typing as t -import uuid -import weakref -from datetime import date - -from werkzeug.http import http_date - -if t.TYPE_CHECKING: # pragma: no cover - from werkzeug.sansio.response import Response - - from ..sansio.app import App - - -class JSONProvider: - """A standard set of JSON operations for an application. Subclasses - of this can be used to customize JSON behavior or use different - JSON libraries. - - To implement a provider for a specific library, subclass this base - class and implement at least :meth:`dumps` and :meth:`loads`. All - other methods have default implementations. - - To use a different provider, either subclass ``Flask`` and set - :attr:`~flask.Flask.json_provider_class` to a provider class, or set - :attr:`app.json ` to an instance of the class. - - :param app: An application instance. This will be stored as a - :class:`weakref.proxy` on the :attr:`_app` attribute. - - .. versionadded:: 2.2 - """ - - def __init__(self, app: App) -> None: - self._app: App = weakref.proxy(app) - - def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: - """Serialize data as JSON. - - :param obj: The data to serialize. - :param kwargs: May be passed to the underlying JSON library. - """ - raise NotImplementedError - - def dump(self, obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: - """Serialize data as JSON and write to a file. - - :param obj: The data to serialize. - :param fp: A file opened for writing text. Should use the UTF-8 - encoding to be valid JSON. - :param kwargs: May be passed to the underlying JSON library. - """ - fp.write(self.dumps(obj, **kwargs)) - - def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: - """Deserialize data as JSON. - - :param s: Text or UTF-8 bytes. - :param kwargs: May be passed to the underlying JSON library. - """ - raise NotImplementedError - - def load(self, fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: - """Deserialize data as JSON read from a file. - - :param fp: A file opened for reading text or UTF-8 bytes. - :param kwargs: May be passed to the underlying JSON library. - """ - return self.loads(fp.read(), **kwargs) - - def _prepare_response_obj( - self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] - ) -> t.Any: - if args and kwargs: - raise TypeError("app.json.response() takes either args or kwargs, not both") - - if not args and not kwargs: - return None - - if len(args) == 1: - return args[0] - - return args or kwargs - - def response(self, *args: t.Any, **kwargs: t.Any) -> Response: - """Serialize the given arguments as JSON, and return a - :class:`~flask.Response` object with the ``application/json`` - mimetype. - - The :func:`~flask.json.jsonify` function calls this method for - the current application. - - Either positional or keyword arguments can be given, not both. - If no arguments are given, ``None`` is serialized. - - :param args: A single value to serialize, or multiple values to - treat as a list to serialize. - :param kwargs: Treat as a dict to serialize. - """ - obj = self._prepare_response_obj(args, kwargs) - return self._app.response_class(self.dumps(obj), mimetype="application/json") - - -def _default(o: t.Any) -> t.Any: - if isinstance(o, date): - return http_date(o) - - if isinstance(o, (decimal.Decimal, uuid.UUID)): - return str(o) - - if dataclasses and dataclasses.is_dataclass(o): - return dataclasses.asdict(o) # type: ignore[arg-type] - - if hasattr(o, "__html__"): - return str(o.__html__()) - - raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable") - - -class DefaultJSONProvider(JSONProvider): - """Provide JSON operations using Python's built-in :mod:`json` - library. Serializes the following additional data types: - - - :class:`datetime.datetime` and :class:`datetime.date` are - serialized to :rfc:`822` strings. This is the same as the HTTP - date format. - - :class:`uuid.UUID` is serialized to a string. - - :class:`dataclasses.dataclass` is passed to - :func:`dataclasses.asdict`. - - :class:`~markupsafe.Markup` (or any object with a ``__html__`` - method) will call the ``__html__`` method to get a string. - """ - - default: t.Callable[[t.Any], t.Any] = staticmethod(_default) - """Apply this function to any object that :meth:`json.dumps` does - not know how to serialize. It should return a valid JSON type or - raise a ``TypeError``. - """ - - ensure_ascii = True - """Replace non-ASCII characters with escape sequences. This may be - more compatible with some clients, but can be disabled for better - performance and size. - """ - - sort_keys = True - """Sort the keys in any serialized dicts. This may be useful for - some caching situations, but can be disabled for better performance. - When enabled, keys must all be strings, they are not converted - before sorting. - """ - - compact: bool | None = None - """If ``True``, or ``None`` out of debug mode, the :meth:`response` - output will not add indentation, newlines, or spaces. If ``False``, - or ``None`` in debug mode, it will use a non-compact representation. - """ - - mimetype = "application/json" - """The mimetype set in :meth:`response`.""" - - def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: - """Serialize data as JSON to a string. - - Keyword arguments are passed to :func:`json.dumps`. Sets some - parameter defaults from the :attr:`default`, - :attr:`ensure_ascii`, and :attr:`sort_keys` attributes. - - :param obj: The data to serialize. - :param kwargs: Passed to :func:`json.dumps`. - """ - kwargs.setdefault("default", self.default) - kwargs.setdefault("ensure_ascii", self.ensure_ascii) - kwargs.setdefault("sort_keys", self.sort_keys) - return json.dumps(obj, **kwargs) - - def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: - """Deserialize data as JSON from a string or bytes. - - :param s: Text or UTF-8 bytes. - :param kwargs: Passed to :func:`json.loads`. - """ - return json.loads(s, **kwargs) - - def response(self, *args: t.Any, **kwargs: t.Any) -> Response: - """Serialize the given arguments as JSON, and return a - :class:`~flask.Response` object with it. The response mimetype - will be "application/json" and can be changed with - :attr:`mimetype`. - - If :attr:`compact` is ``False`` or debug mode is enabled, the - output will be formatted to be easier to read. - - Either positional or keyword arguments can be given, not both. - If no arguments are given, ``None`` is serialized. - - :param args: A single value to serialize, or multiple values to - treat as a list to serialize. - :param kwargs: Treat as a dict to serialize. - """ - obj = self._prepare_response_obj(args, kwargs) - dump_args: dict[str, t.Any] = {} - - if (self.compact is None and self._app.debug) or self.compact is False: - dump_args.setdefault("indent", 2) - else: - dump_args.setdefault("separators", (",", ":")) - - return self._app.response_class( - f"{self.dumps(obj, **dump_args)}\n", mimetype=self.mimetype - ) diff --git a/src/flask/json/tag.py b/src/flask/json/tag.py deleted file mode 100644 index 8dc3629b..00000000 --- a/src/flask/json/tag.py +++ /dev/null @@ -1,327 +0,0 @@ -""" -Tagged JSON -~~~~~~~~~~~ - -A compact representation for lossless serialization of non-standard JSON -types. :class:`~flask.sessions.SecureCookieSessionInterface` uses this -to serialize the session data, but it may be useful in other places. It -can be extended to support other types. - -.. autoclass:: TaggedJSONSerializer - :members: - -.. autoclass:: JSONTag - :members: - -Let's see an example that adds support for -:class:`~collections.OrderedDict`. Dicts don't have an order in JSON, so -to handle this we will dump the items as a list of ``[key, value]`` -pairs. Subclass :class:`JSONTag` and give it the new key ``' od'`` to -identify the type. The session serializer processes dicts first, so -insert the new tag at the front of the order since ``OrderedDict`` must -be processed before ``dict``. - -.. code-block:: python - - from flask.json.tag import JSONTag - - class TagOrderedDict(JSONTag): - __slots__ = ('serializer',) - key = ' od' - - def check(self, value): - return isinstance(value, OrderedDict) - - def to_json(self, value): - return [[k, self.serializer.tag(v)] for k, v in iteritems(value)] - - def to_python(self, value): - return OrderedDict(value) - - app.session_interface.serializer.register(TagOrderedDict, index=0) -""" - -from __future__ import annotations - -import typing as t -from base64 import b64decode -from base64 import b64encode -from datetime import datetime -from uuid import UUID - -from markupsafe import Markup -from werkzeug.http import http_date -from werkzeug.http import parse_date - -from ..json import dumps -from ..json import loads - - -class JSONTag: - """Base class for defining type tags for :class:`TaggedJSONSerializer`.""" - - __slots__ = ("serializer",) - - #: The tag to mark the serialized object with. If empty, this tag is - #: only used as an intermediate step during tagging. - key: str = "" - - def __init__(self, serializer: TaggedJSONSerializer) -> None: - """Create a tagger for the given serializer.""" - self.serializer = serializer - - def check(self, value: t.Any) -> bool: - """Check if the given value should be tagged by this tag.""" - raise NotImplementedError - - def to_json(self, value: t.Any) -> t.Any: - """Convert the Python object to an object that is a valid JSON type. - The tag will be added later.""" - raise NotImplementedError - - def to_python(self, value: t.Any) -> t.Any: - """Convert the JSON representation back to the correct type. The tag - will already be removed.""" - raise NotImplementedError - - def tag(self, value: t.Any) -> dict[str, t.Any]: - """Convert the value to a valid JSON type and add the tag structure - around it.""" - return {self.key: self.to_json(value)} - - -class TagDict(JSONTag): - """Tag for 1-item dicts whose only key matches a registered tag. - - Internally, the dict key is suffixed with `__`, and the suffix is removed - when deserializing. - """ - - __slots__ = () - key = " di" - - def check(self, value: t.Any) -> bool: - return ( - isinstance(value, dict) - and len(value) == 1 - and next(iter(value)) in self.serializer.tags - ) - - def to_json(self, value: t.Any) -> t.Any: - key = next(iter(value)) - return {f"{key}__": self.serializer.tag(value[key])} - - def to_python(self, value: t.Any) -> t.Any: - key = next(iter(value)) - return {key[:-2]: value[key]} - - -class PassDict(JSONTag): - __slots__ = () - - def check(self, value: t.Any) -> bool: - return isinstance(value, dict) - - def to_json(self, value: t.Any) -> t.Any: - # JSON objects may only have string keys, so don't bother tagging the - # key here. - return {k: self.serializer.tag(v) for k, v in value.items()} - - tag = to_json - - -class TagTuple(JSONTag): - __slots__ = () - key = " t" - - def check(self, value: t.Any) -> bool: - return isinstance(value, tuple) - - def to_json(self, value: t.Any) -> t.Any: - return [self.serializer.tag(item) for item in value] - - def to_python(self, value: t.Any) -> t.Any: - return tuple(value) - - -class PassList(JSONTag): - __slots__ = () - - def check(self, value: t.Any) -> bool: - return isinstance(value, list) - - def to_json(self, value: t.Any) -> t.Any: - return [self.serializer.tag(item) for item in value] - - tag = to_json - - -class TagBytes(JSONTag): - __slots__ = () - key = " b" - - def check(self, value: t.Any) -> bool: - return isinstance(value, bytes) - - def to_json(self, value: t.Any) -> t.Any: - return b64encode(value).decode("ascii") - - def to_python(self, value: t.Any) -> t.Any: - return b64decode(value) - - -class TagMarkup(JSONTag): - """Serialize anything matching the :class:`~markupsafe.Markup` API by - having a ``__html__`` method to the result of that method. Always - deserializes to an instance of :class:`~markupsafe.Markup`.""" - - __slots__ = () - key = " m" - - def check(self, value: t.Any) -> bool: - return callable(getattr(value, "__html__", None)) - - def to_json(self, value: t.Any) -> t.Any: - return str(value.__html__()) - - def to_python(self, value: t.Any) -> t.Any: - return Markup(value) - - -class TagUUID(JSONTag): - __slots__ = () - key = " u" - - def check(self, value: t.Any) -> bool: - return isinstance(value, UUID) - - def to_json(self, value: t.Any) -> t.Any: - return value.hex - - def to_python(self, value: t.Any) -> t.Any: - return UUID(value) - - -class TagDateTime(JSONTag): - __slots__ = () - key = " d" - - def check(self, value: t.Any) -> bool: - return isinstance(value, datetime) - - def to_json(self, value: t.Any) -> t.Any: - return http_date(value) - - def to_python(self, value: t.Any) -> t.Any: - return parse_date(value) - - -class TaggedJSONSerializer: - """Serializer that uses a tag system to compactly represent objects that - are not JSON types. Passed as the intermediate serializer to - :class:`itsdangerous.Serializer`. - - The following extra types are supported: - - * :class:`dict` - * :class:`tuple` - * :class:`bytes` - * :class:`~markupsafe.Markup` - * :class:`~uuid.UUID` - * :class:`~datetime.datetime` - """ - - __slots__ = ("tags", "order") - - #: Tag classes to bind when creating the serializer. Other tags can be - #: added later using :meth:`~register`. - default_tags = [ - TagDict, - PassDict, - TagTuple, - PassList, - TagBytes, - TagMarkup, - TagUUID, - TagDateTime, - ] - - def __init__(self) -> None: - self.tags: dict[str, JSONTag] = {} - self.order: list[JSONTag] = [] - - for cls in self.default_tags: - self.register(cls) - - def register( - self, - tag_class: type[JSONTag], - force: bool = False, - index: int | None = None, - ) -> None: - """Register a new tag with this serializer. - - :param tag_class: tag class to register. Will be instantiated with this - serializer instance. - :param force: overwrite an existing tag. If false (default), a - :exc:`KeyError` is raised. - :param index: index to insert the new tag in the tag order. Useful when - the new tag is a special case of an existing tag. If ``None`` - (default), the tag is appended to the end of the order. - - :raise KeyError: if the tag key is already registered and ``force`` is - not true. - """ - tag = tag_class(self) - key = tag.key - - if key: - if not force and key in self.tags: - raise KeyError(f"Tag '{key}' is already registered.") - - self.tags[key] = tag - - if index is None: - self.order.append(tag) - else: - self.order.insert(index, tag) - - def tag(self, value: t.Any) -> t.Any: - """Convert a value to a tagged representation if necessary.""" - for tag in self.order: - if tag.check(value): - return tag.tag(value) - - return value - - def untag(self, value: dict[str, t.Any]) -> t.Any: - """Convert a tagged representation back to the original type.""" - if len(value) != 1: - return value - - key = next(iter(value)) - - if key not in self.tags: - return value - - return self.tags[key].to_python(value[key]) - - def _untag_scan(self, value: t.Any) -> t.Any: - if isinstance(value, dict): - # untag each item recursively - value = {k: self._untag_scan(v) for k, v in value.items()} - # untag the dict itself - value = self.untag(value) - elif isinstance(value, list): - # untag each item recursively - value = [self._untag_scan(item) for item in value] - - return value - - def dumps(self, value: t.Any) -> str: - """Tag the value and dump it to a compact JSON string.""" - return dumps(self.tag(value), separators=(",", ":")) - - def loads(self, value: str) -> t.Any: - """Load data from a JSON string and deserialized any tagged objects.""" - return self._untag_scan(loads(value)) diff --git a/src/flask/logging.py b/src/flask/logging.py deleted file mode 100644 index 0cb8f437..00000000 --- a/src/flask/logging.py +++ /dev/null @@ -1,79 +0,0 @@ -from __future__ import annotations - -import logging -import sys -import typing as t - -from werkzeug.local import LocalProxy - -from .globals import request - -if t.TYPE_CHECKING: # pragma: no cover - from .sansio.app import App - - -@LocalProxy -def wsgi_errors_stream() -> t.TextIO: - """Find the most appropriate error stream for the application. If a request - is active, log to ``wsgi.errors``, otherwise use ``sys.stderr``. - - If you configure your own :class:`logging.StreamHandler`, you may want to - use this for the stream. If you are using file or dict configuration and - can't import this directly, you can refer to it as - ``ext://flask.logging.wsgi_errors_stream``. - """ - if request: - return request.environ["wsgi.errors"] # type: ignore[no-any-return] - - return sys.stderr - - -def has_level_handler(logger: logging.Logger) -> bool: - """Check if there is a handler in the logging chain that will handle the - given logger's :meth:`effective level <~logging.Logger.getEffectiveLevel>`. - """ - level = logger.getEffectiveLevel() - current = logger - - while current: - if any(handler.level <= level for handler in current.handlers): - return True - - if not current.propagate: - break - - current = current.parent # type: ignore - - return False - - -#: Log messages to :func:`~flask.logging.wsgi_errors_stream` with the format -#: ``[%(asctime)s] %(levelname)s in %(module)s: %(message)s``. -default_handler = logging.StreamHandler(wsgi_errors_stream) # type: ignore -default_handler.setFormatter( - logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s") -) - - -def create_logger(app: App) -> logging.Logger: - """Get the Flask app's logger and configure it if needed. - - The logger name will be the same as - :attr:`app.import_name `. - - When :attr:`~flask.Flask.debug` is enabled, set the logger level to - :data:`logging.DEBUG` if it is not set. - - If there is no handler for the logger's effective level, add a - :class:`~logging.StreamHandler` for - :func:`~flask.logging.wsgi_errors_stream` with a basic format. - """ - logger = logging.getLogger(app.name) - - if app.debug and not logger.level: - logger.setLevel(logging.DEBUG) - - if not has_level_handler(logger): - logger.addHandler(default_handler) - - return logger diff --git a/src/flask/sansio/README.md b/src/flask/sansio/README.md deleted file mode 100644 index 623ac198..00000000 --- a/src/flask/sansio/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Sansio - -This folder contains code that can be used by alternative Flask -implementations, for example Quart. The code therefore cannot do any -IO, nor be part of a likely IO path. Finally this code cannot use the -Flask globals. diff --git a/src/flask/sansio/app.py b/src/flask/sansio/app.py deleted file mode 100644 index 74c5b323..00000000 --- a/src/flask/sansio/app.py +++ /dev/null @@ -1,1013 +0,0 @@ -from __future__ import annotations - -import logging -import os -import sys -import typing as t -from datetime import timedelta -from itertools import chain - -from werkzeug.exceptions import Aborter -from werkzeug.exceptions import BadRequest -from werkzeug.exceptions import BadRequestKeyError -from werkzeug.routing import BuildError -from werkzeug.routing import Map -from werkzeug.routing import Rule -from werkzeug.sansio.response import Response -from werkzeug.utils import cached_property -from werkzeug.utils import redirect as _wz_redirect - -from .. import typing as ft -from ..config import Config -from ..config import ConfigAttribute -from ..ctx import _AppCtxGlobals -from ..helpers import _split_blueprint_path -from ..helpers import get_debug_flag -from ..json.provider import DefaultJSONProvider -from ..json.provider import JSONProvider -from ..logging import create_logger -from ..templating import DispatchingJinjaLoader -from ..templating import Environment -from .scaffold import _endpoint_from_view_func -from .scaffold import find_package -from .scaffold import Scaffold -from .scaffold import setupmethod - -if t.TYPE_CHECKING: # pragma: no cover - from werkzeug.wrappers import Response as BaseResponse - - from ..testing import FlaskClient - from ..testing import FlaskCliRunner - from .blueprints import Blueprint - -T_shell_context_processor = t.TypeVar( - "T_shell_context_processor", bound=ft.ShellContextProcessorCallable -) -T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) -T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) -T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) -T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) - - -def _make_timedelta(value: timedelta | int | None) -> timedelta | None: - if value is None or isinstance(value, timedelta): - return value - - return timedelta(seconds=value) - - -class App(Scaffold): - """The flask object implements a WSGI application and acts as the central - object. It is passed the name of the module or package of the - application. Once it is created it will act as a central registry for - the view functions, the URL rules, template configuration and much more. - - The name of the package is used to resolve resources from inside the - package or the folder the module is contained in depending on if the - package parameter resolves to an actual python package (a folder with - an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). - - For more information about resource loading, see :func:`open_resource`. - - Usually you create a :class:`Flask` instance in your main module or - in the :file:`__init__.py` file of your package like this:: - - from flask import Flask - app = Flask(__name__) - - .. admonition:: About the First Parameter - - The idea of the first parameter is to give Flask an idea of what - belongs to your application. This name is used to find resources - on the filesystem, can be used by extensions to improve debugging - information and a lot more. - - So it's important what you provide there. If you are using a single - module, `__name__` is always the correct value. If you however are - using a package, it's usually recommended to hardcode the name of - your package there. - - For example if your application is defined in :file:`yourapplication/app.py` - you should create it with one of the two versions below:: - - app = Flask('yourapplication') - app = Flask(__name__.split('.')[0]) - - Why is that? The application will work even with `__name__`, thanks - to how resources are looked up. However it will make debugging more - painful. Certain extensions can make assumptions based on the - import name of your application. For example the Flask-SQLAlchemy - extension will look for the code in your application that triggered - an SQL query in debug mode. If the import name is not properly set - up, that debugging information is lost. (For example it would only - pick up SQL queries in `yourapplication.app` and not - `yourapplication.views.frontend`) - - .. versionadded:: 0.7 - The `static_url_path`, `static_folder`, and `template_folder` - parameters were added. - - .. versionadded:: 0.8 - The `instance_path` and `instance_relative_config` parameters were - added. - - .. versionadded:: 0.11 - The `root_path` parameter was added. - - .. versionadded:: 1.0 - The ``host_matching`` and ``static_host`` parameters were added. - - .. versionadded:: 1.0 - The ``subdomain_matching`` parameter was added. Subdomain - matching needs to be enabled manually now. Setting - :data:`SERVER_NAME` does not implicitly enable it. - - :param import_name: the name of the application package - :param static_url_path: can be used to specify a different path for the - static files on the web. Defaults to the name - of the `static_folder` folder. - :param static_folder: The folder with static files that is served at - ``static_url_path``. Relative to the application ``root_path`` - or an absolute path. Defaults to ``'static'``. - :param static_host: the host to use when adding the static route. - Defaults to None. Required when using ``host_matching=True`` - with a ``static_folder`` configured. - :param host_matching: set ``url_map.host_matching`` attribute. - Defaults to False. - :param subdomain_matching: consider the subdomain relative to - :data:`SERVER_NAME` when matching routes. Defaults to False. - :param template_folder: the folder that contains the templates that should - be used by the application. Defaults to - ``'templates'`` folder in the root path of the - application. - :param instance_path: An alternative instance path for the application. - By default the folder ``'instance'`` next to the - package or module is assumed to be the instance - path. - :param instance_relative_config: if set to ``True`` relative filenames - for loading the config are assumed to - be relative to the instance path instead - of the application root. - :param root_path: The path to the root of the application files. - This should only be set manually when it can't be detected - automatically, such as for namespace packages. - """ - - #: The class of the object assigned to :attr:`aborter`, created by - #: :meth:`create_aborter`. That object is called by - #: :func:`flask.abort` to raise HTTP errors, and can be - #: called directly as well. - #: - #: Defaults to :class:`werkzeug.exceptions.Aborter`. - #: - #: .. versionadded:: 2.2 - aborter_class = Aborter - - #: The class that is used for the Jinja environment. - #: - #: .. versionadded:: 0.11 - jinja_environment = Environment - - #: The class that is used for the :data:`~flask.g` instance. - #: - #: Example use cases for a custom class: - #: - #: 1. Store arbitrary attributes on flask.g. - #: 2. Add a property for lazy per-request database connectors. - #: 3. Return None instead of AttributeError on unexpected attributes. - #: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g. - #: - #: .. versionadded:: 0.10 - #: Renamed from ``request_globals_class`. - app_ctx_globals_class = _AppCtxGlobals - - #: The class that is used for the ``config`` attribute of this app. - #: Defaults to :class:`~flask.Config`. - #: - #: Example use cases for a custom class: - #: - #: 1. Default values for certain config options. - #: 2. Access to config values through attributes in addition to keys. - #: - #: .. versionadded:: 0.11 - config_class = Config - - #: The testing flag. Set this to ``True`` to enable the test mode of - #: Flask extensions (and in the future probably also Flask itself). - #: For example this might activate test helpers that have an - #: additional runtime cost which should not be enabled by default. - #: - #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the - #: default it's implicitly enabled. - #: - #: This attribute can also be configured from the config with the - #: ``TESTING`` configuration key. Defaults to ``False``. - testing = ConfigAttribute[bool]("TESTING") - - #: If a secret key is set, cryptographic components can use this to - #: sign cookies and other things. Set this to a complex random value - #: when you want to use the secure cookie for instance. - #: - #: This attribute can also be configured from the config with the - #: :data:`SECRET_KEY` configuration key. Defaults to ``None``. - secret_key = ConfigAttribute[str | bytes | None]("SECRET_KEY") - - #: A :class:`~datetime.timedelta` which is used to set the expiration - #: date of a permanent session. The default is 31 days which makes a - #: permanent session survive for roughly one month. - #: - #: This attribute can also be configured from the config with the - #: ``PERMANENT_SESSION_LIFETIME`` configuration key. Defaults to - #: ``timedelta(days=31)`` - permanent_session_lifetime = ConfigAttribute[timedelta]( - "PERMANENT_SESSION_LIFETIME", - get_converter=_make_timedelta, # type: ignore[arg-type] - ) - - json_provider_class: type[JSONProvider] = DefaultJSONProvider - """A subclass of :class:`~flask.json.provider.JSONProvider`. An - instance is created and assigned to :attr:`app.json` when creating - the app. - - The default, :class:`~flask.json.provider.DefaultJSONProvider`, uses - Python's built-in :mod:`json` library. A different provider can use - a different JSON library. - - .. versionadded:: 2.2 - """ - - #: Options that are passed to the Jinja environment in - #: :meth:`create_jinja_environment`. Changing these options after - #: the environment is created (accessing :attr:`jinja_env`) will - #: have no effect. - #: - #: .. versionchanged:: 1.1.0 - #: This is a ``dict`` instead of an ``ImmutableDict`` to allow - #: easier configuration. - #: - jinja_options: dict[str, t.Any] = {} - - #: The rule object to use for URL rules created. This is used by - #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. - #: - #: .. versionadded:: 0.7 - url_rule_class = Rule - - #: The map object to use for storing the URL rules and routing - #: configuration parameters. Defaults to :class:`werkzeug.routing.Map`. - #: - #: .. versionadded:: 1.1.0 - url_map_class = Map - - #: The :meth:`test_client` method creates an instance of this test - #: client class. Defaults to :class:`~flask.testing.FlaskClient`. - #: - #: .. versionadded:: 0.7 - test_client_class: type[FlaskClient] | None = None - - #: The :class:`~click.testing.CliRunner` subclass, by default - #: :class:`~flask.testing.FlaskCliRunner` that is used by - #: :meth:`test_cli_runner`. Its ``__init__`` method should take a - #: Flask app object as the first argument. - #: - #: .. versionadded:: 1.0 - test_cli_runner_class: type[FlaskCliRunner] | None = None - - default_config: dict[str, t.Any] - response_class: type[Response] - - def __init__( - self, - import_name: str, - static_url_path: str | None = None, - static_folder: str | os.PathLike[str] | None = "static", - static_host: str | None = None, - host_matching: bool = False, - subdomain_matching: bool = False, - template_folder: str | os.PathLike[str] | None = "templates", - instance_path: str | None = None, - instance_relative_config: bool = False, - root_path: str | None = None, - ) -> None: - super().__init__( - import_name=import_name, - static_folder=static_folder, - static_url_path=static_url_path, - template_folder=template_folder, - root_path=root_path, - ) - - if instance_path is None: - instance_path = self.auto_find_instance_path() - elif not os.path.isabs(instance_path): - raise ValueError( - "If an instance path is provided it must be absolute." - " A relative path was given instead." - ) - - #: Holds the path to the instance folder. - #: - #: .. versionadded:: 0.8 - self.instance_path = instance_path - - #: The configuration dictionary as :class:`Config`. This behaves - #: exactly like a regular dictionary but supports additional methods - #: to load a config from files. - self.config = self.make_config(instance_relative_config) - - #: An instance of :attr:`aborter_class` created by - #: :meth:`make_aborter`. This is called by :func:`flask.abort` - #: to raise HTTP errors, and can be called directly as well. - #: - #: .. versionadded:: 2.2 - #: Moved from ``flask.abort``, which calls this object. - self.aborter = self.make_aborter() - - self.json: JSONProvider = self.json_provider_class(self) - """Provides access to JSON methods. Functions in ``flask.json`` - will call methods on this provider when the application context - is active. Used for handling JSON requests and responses. - - An instance of :attr:`json_provider_class`. Can be customized by - changing that attribute on a subclass, or by assigning to this - attribute afterwards. - - The default, :class:`~flask.json.provider.DefaultJSONProvider`, - uses Python's built-in :mod:`json` library. A different provider - can use a different JSON library. - - .. versionadded:: 2.2 - """ - - #: A list of functions that are called by - #: :meth:`handle_url_build_error` when :meth:`.url_for` raises a - #: :exc:`~werkzeug.routing.BuildError`. Each function is called - #: with ``error``, ``endpoint`` and ``values``. If a function - #: returns ``None`` or raises a ``BuildError``, it is skipped. - #: Otherwise, its return value is returned by ``url_for``. - #: - #: .. versionadded:: 0.9 - self.url_build_error_handlers: list[ - t.Callable[[Exception, str, dict[str, t.Any]], str] - ] = [] - - #: A list of functions that are called when the application context - #: is destroyed. Since the application context is also torn down - #: if the request ends this is the place to store code that disconnects - #: from databases. - #: - #: .. versionadded:: 0.9 - self.teardown_appcontext_funcs: list[ft.TeardownCallable] = [] - - #: A list of shell context processor functions that should be run - #: when a shell context is created. - #: - #: .. versionadded:: 0.11 - self.shell_context_processors: list[ft.ShellContextProcessorCallable] = [] - - #: Maps registered blueprint names to blueprint objects. The - #: dict retains the order the blueprints were registered in. - #: Blueprints can be registered multiple times, this dict does - #: not track how often they were attached. - #: - #: .. versionadded:: 0.7 - self.blueprints: dict[str, Blueprint] = {} - - #: a place where extensions can store application specific state. For - #: example this is where an extension could store database engines and - #: similar things. - #: - #: The key must match the name of the extension module. For example in - #: case of a "Flask-Foo" extension in `flask_foo`, the key would be - #: ``'foo'``. - #: - #: .. versionadded:: 0.7 - self.extensions: dict[str, t.Any] = {} - - #: The :class:`~werkzeug.routing.Map` for this instance. You can use - #: this to change the routing converters after the class was created - #: but before any routes are connected. Example:: - #: - #: from werkzeug.routing import BaseConverter - #: - #: class ListConverter(BaseConverter): - #: def to_python(self, value): - #: return value.split(',') - #: def to_url(self, values): - #: return ','.join(super(ListConverter, self).to_url(value) - #: for value in values) - #: - #: app = Flask(__name__) - #: app.url_map.converters['list'] = ListConverter - self.url_map = self.url_map_class(host_matching=host_matching) - - self.subdomain_matching = subdomain_matching - - # tracks internally if the application already handled at least one - # request. - self._got_first_request = False - - def _check_setup_finished(self, f_name: str) -> None: - if self._got_first_request: - raise AssertionError( - f"The setup method '{f_name}' can no longer be called" - " on the application. It has already handled its first" - " request, any changes will not be applied" - " consistently.\n" - "Make sure all imports, decorators, functions, etc." - " needed to set up the application are done before" - " running it." - ) - - @cached_property - def name(self) -> str: - """The name of the application. This is usually the import name - with the difference that it's guessed from the run file if the - import name is main. This name is used as a display name when - Flask needs the name of the application. It can be set and overridden - to change the value. - - .. versionadded:: 0.8 - """ - if self.import_name == "__main__": - fn: str | None = getattr(sys.modules["__main__"], "__file__", None) - if fn is None: - return "__main__" - return os.path.splitext(os.path.basename(fn))[0] - return self.import_name - - @cached_property - def logger(self) -> logging.Logger: - """A standard Python :class:`~logging.Logger` for the app, with - the same name as :attr:`name`. - - In debug mode, the logger's :attr:`~logging.Logger.level` will - be set to :data:`~logging.DEBUG`. - - If there are no handlers configured, a default handler will be - added. See :doc:`/logging` for more information. - - .. versionchanged:: 1.1.0 - The logger takes the same name as :attr:`name` rather than - hard-coding ``"flask.app"``. - - .. versionchanged:: 1.0.0 - Behavior was simplified. The logger is always named - ``"flask.app"``. The level is only set during configuration, - it doesn't check ``app.debug`` each time. Only one format is - used, not different ones depending on ``app.debug``. No - handlers are removed, and a handler is only added if no - handlers are already configured. - - .. versionadded:: 0.3 - """ - return create_logger(self) - - @cached_property - def jinja_env(self) -> Environment: - """The Jinja environment used to load templates. - - The environment is created the first time this property is - accessed. Changing :attr:`jinja_options` after that will have no - effect. - """ - return self.create_jinja_environment() - - def create_jinja_environment(self) -> Environment: - raise NotImplementedError() - - def make_config(self, instance_relative: bool = False) -> Config: - """Used to create the config attribute by the Flask constructor. - The `instance_relative` parameter is passed in from the constructor - of Flask (there named `instance_relative_config`) and indicates if - the config should be relative to the instance path or the root path - of the application. - - .. versionadded:: 0.8 - """ - root_path = self.root_path - if instance_relative: - root_path = self.instance_path - defaults = dict(self.default_config) - defaults["DEBUG"] = get_debug_flag() - return self.config_class(root_path, defaults) - - def make_aborter(self) -> Aborter: - """Create the object to assign to :attr:`aborter`. That object - is called by :func:`flask.abort` to raise HTTP errors, and can - be called directly as well. - - By default, this creates an instance of :attr:`aborter_class`, - which defaults to :class:`werkzeug.exceptions.Aborter`. - - .. versionadded:: 2.2 - """ - return self.aborter_class() - - def auto_find_instance_path(self) -> str: - """Tries to locate the instance path if it was not provided to the - constructor of the application class. It will basically calculate - the path to a folder named ``instance`` next to your main file or - the package. - - .. versionadded:: 0.8 - """ - prefix, package_path = find_package(self.import_name) - if prefix is None: - return os.path.join(package_path, "instance") - return os.path.join(prefix, "var", f"{self.name}-instance") - - def create_global_jinja_loader(self) -> DispatchingJinjaLoader: - """Creates the loader for the Jinja environment. Can be used to - override just the loader and keeping the rest unchanged. It's - discouraged to override this function. Instead one should override - the :meth:`jinja_loader` function instead. - - The global loader dispatches between the loaders of the application - and the individual blueprints. - - .. versionadded:: 0.7 - """ - return DispatchingJinjaLoader(self) - - def select_jinja_autoescape(self, filename: str | None) -> bool: - """Returns ``True`` if autoescaping should be active for the given - template name. If no template name is given, returns `True`. - - .. versionchanged:: 3.2 - Use case-insensitive comparison instead of only lower case. - - .. versionchanged:: 2.2 - Autoescaping is now enabled by default for ``.svg`` files. - - .. versionadded:: 0.5 - """ - if filename is None: - return True - return filename.lower().endswith((".html", ".htm", ".xml", ".xhtml", ".svg")) - - @property - def debug(self) -> bool: - """Whether debug mode is enabled. When using ``flask run`` to start the - development server, an interactive debugger will be shown for unhandled - exceptions, and the server will be reloaded when code changes. This maps to the - :data:`DEBUG` config key. It may not behave as expected if set late. - - **Do not enable debug mode when deploying in production.** - - Default: ``False`` - """ - return self.config["DEBUG"] # type: ignore[no-any-return] - - @debug.setter - def debug(self, value: bool) -> None: - self.config["DEBUG"] = value - - if self.config["TEMPLATES_AUTO_RELOAD"] is None: - self.jinja_env.auto_reload = value - - @setupmethod - def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: - """Register a :class:`~flask.Blueprint` on the application. Keyword - arguments passed to this method will override the defaults set on the - blueprint. - - Calls the blueprint's :meth:`~flask.Blueprint.register` method after - recording the blueprint in the application's :attr:`blueprints`. - - :param blueprint: The blueprint to register. - :param url_prefix: Blueprint routes will be prefixed with this. - :param subdomain: Blueprint routes will match on this subdomain. - :param url_defaults: Blueprint routes will use these default values for - view arguments. - :param options: Additional keyword arguments are passed to - :class:`~flask.blueprints.BlueprintSetupState`. They can be - accessed in :meth:`~flask.Blueprint.record` callbacks. - - .. versionchanged:: 2.0.1 - The ``name`` option can be used to change the (pre-dotted) - name the blueprint is registered with. This allows the same - blueprint to be registered multiple times with unique names - for ``url_for``. - - .. versionadded:: 0.7 - """ - blueprint.register(self, options) - - def iter_blueprints(self) -> t.ValuesView[Blueprint]: - """Iterates over all blueprints by the order they were registered. - - .. versionadded:: 0.11 - """ - return self.blueprints.values() - - @setupmethod - def add_url_rule( - self, - rule: str, - endpoint: str | None = None, - view_func: ft.RouteCallable | None = None, - provide_automatic_options: bool | None = None, - **options: t.Any, - ) -> None: - if endpoint is None: - endpoint = _endpoint_from_view_func(view_func) # type: ignore - options["endpoint"] = endpoint - methods = options.pop("methods", None) - - # if the methods are not given and the view_func object knows its - # methods we can use that instead. If neither exists, we go with - # a tuple of only ``GET`` as default. - if methods is None: - methods = getattr(view_func, "methods", None) or ("GET",) - if isinstance(methods, str): - raise TypeError( - "Allowed methods must be a list of strings, for" - ' example: @app.route(..., methods=["POST"])' - ) - methods = {item.upper() for item in methods} - - # Methods that should always be added - required_methods: set[str] = set(getattr(view_func, "required_methods", ())) - - if provide_automatic_options is None: - provide_automatic_options = getattr( - view_func, "provide_automatic_options", None - ) - - if provide_automatic_options is None: - provide_automatic_options = ( - "OPTIONS" not in methods - and self.config["PROVIDE_AUTOMATIC_OPTIONS"] - ) - - if provide_automatic_options: - required_methods.add("OPTIONS") - - # Add the required methods now. - methods |= required_methods - - rule_obj = self.url_rule_class(rule, methods=methods, **options) - rule_obj.provide_automatic_options = provide_automatic_options # type: ignore[attr-defined] - - self.url_map.add(rule_obj) - if view_func is not None: - old_func = self.view_functions.get(endpoint) - if old_func is not None and old_func != view_func: - raise AssertionError( - "View function mapping is overwriting an existing" - f" endpoint function: {endpoint}" - ) - self.view_functions[endpoint] = view_func - - @t.overload - def template_filter(self, name: T_template_filter) -> T_template_filter: ... - @t.overload - def template_filter( - self, name: str | None = None - ) -> t.Callable[[T_template_filter], T_template_filter]: ... - @setupmethod - def template_filter( - self, name: T_template_filter | str | None = None - ) -> T_template_filter | t.Callable[[T_template_filter], T_template_filter]: - """Decorate a function to register it as a custom Jinja filter. The name - is optional. The decorator may be used without parentheses. - - .. code-block:: python - - @app.template_filter("reverse") - def reverse_filter(s): - return reversed(s) - - The :meth:`add_template_filter` method may be used to register a - function later rather than decorating. - - :param name: The name to register the filter as. If not given, uses the - function's name. - """ - if callable(name): - self.add_template_filter(name) - return name - - def decorator(f: T_template_filter) -> T_template_filter: - self.add_template_filter(f, name=name) - return f - - return decorator - - @setupmethod - def add_template_filter( - self, f: ft.TemplateFilterCallable, name: str | None = None - ) -> None: - """Register a function to use as a custom Jinja filter. - - The :meth:`template_filter` decorator can be used to register a function - by decorating instead. - - :param f: The function to register. - :param name: The name to register the filter as. If not given, uses the - function's name. - """ - self.jinja_env.filters[name or f.__name__] = f - - @t.overload - def template_test(self, name: T_template_test) -> T_template_test: ... - @t.overload - def template_test( - self, name: str | None = None - ) -> t.Callable[[T_template_test], T_template_test]: ... - @setupmethod - def template_test( - self, name: T_template_test | str | None = None - ) -> T_template_test | t.Callable[[T_template_test], T_template_test]: - """Decorate a function to register it as a custom Jinja test. The name - is optional. The decorator may be used without parentheses. - - .. code-block:: python - - @app.template_test("prime") - def is_prime_test(n): - if n == 2: - return True - for i in range(2, int(math.ceil(math.sqrt(n))) + 1): - if n % i == 0: - return False - return True - - The :meth:`add_template_test` method may be used to register a function - later rather than decorating. - - :param name: The name to register the filter as. If not given, uses the - function's name. - - .. versionadded:: 0.10 - """ - if callable(name): - self.add_template_test(name) - return name - - def decorator(f: T_template_test) -> T_template_test: - self.add_template_test(f, name=name) - return f - - return decorator - - @setupmethod - def add_template_test( - self, f: ft.TemplateTestCallable, name: str | None = None - ) -> None: - """Register a function to use as a custom Jinja test. - - The :meth:`template_test` decorator can be used to register a function - by decorating instead. - - :param f: The function to register. - :param name: The name to register the test as. If not given, uses the - function's name. - - .. versionadded:: 0.10 - """ - self.jinja_env.tests[name or f.__name__] = f - - @t.overload - def template_global(self, name: T_template_global) -> T_template_global: ... - @t.overload - def template_global( - self, name: str | None = None - ) -> t.Callable[[T_template_global], T_template_global]: ... - @setupmethod - def template_global( - self, name: T_template_global | str | None = None - ) -> T_template_global | t.Callable[[T_template_global], T_template_global]: - """Decorate a function to register it as a custom Jinja global. The name - is optional. The decorator may be used without parentheses. - - .. code-block:: python - - @app.template_global - def double(n): - return 2 * n - - The :meth:`add_template_global` method may be used to register a - function later rather than decorating. - - :param name: The name to register the global as. If not given, uses the - function's name. - - .. versionadded:: 0.10 - """ - if callable(name): - self.add_template_global(name) - return name - - def decorator(f: T_template_global) -> T_template_global: - self.add_template_global(f, name=name) - return f - - return decorator - - @setupmethod - def add_template_global( - self, f: ft.TemplateGlobalCallable, name: str | None = None - ) -> None: - """Register a function to use as a custom Jinja global. - - The :meth:`template_global` decorator can be used to register a function - by decorating instead. - - :param f: The function to register. - :param name: The name to register the global as. If not given, uses the - function's name. - - .. versionadded:: 0.10 - """ - self.jinja_env.globals[name or f.__name__] = f - - @setupmethod - def teardown_appcontext(self, f: T_teardown) -> T_teardown: - """Registers a function to be called when the app context is popped. The - context is popped at the end of a request, CLI command, or manual ``with`` - block. - - .. code-block:: python - - with app.app_context(): - ... - - When the ``with`` block exits (or ``ctx.pop()`` is called), the - teardown functions are called just before the app context is - made inactive. - - When a teardown function was called because of an unhandled - exception it will be passed an error object. If an - :meth:`errorhandler` is registered, it will handle the exception - and the teardown will not receive it. - - Teardown functions must avoid raising exceptions. If they - execute code that might fail they must surround that code with a - ``try``/``except`` block and log any errors. - - The return values of teardown functions are ignored. - - .. versionadded:: 0.9 - """ - self.teardown_appcontext_funcs.append(f) - return f - - @setupmethod - def shell_context_processor( - self, f: T_shell_context_processor - ) -> T_shell_context_processor: - """Registers a shell context processor function. - - .. versionadded:: 0.11 - """ - self.shell_context_processors.append(f) - return f - - def _find_error_handler( - self, e: Exception, blueprints: list[str] - ) -> ft.ErrorHandlerCallable | None: - """Return a registered error handler for an exception in this order: - blueprint handler for a specific code, app handler for a specific code, - blueprint handler for an exception class, app handler for an exception - class, or ``None`` if a suitable handler is not found. - """ - exc_class, code = self._get_exc_class_and_code(type(e)) - names = (*blueprints, None) - - for c in (code, None) if code is not None else (None,): - for name in names: - handler_map = self.error_handler_spec[name][c] - - if not handler_map: - continue - - for cls in exc_class.__mro__: - handler = handler_map.get(cls) - - if handler is not None: - return handler - return None - - def trap_http_exception(self, e: Exception) -> bool: - """Checks if an HTTP exception should be trapped or not. By default - this will return ``False`` for all exceptions except for a bad request - key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to ``True``. It - also returns ``True`` if ``TRAP_HTTP_EXCEPTIONS`` is set to ``True``. - - This is called for all HTTP exceptions raised by a view function. - If it returns ``True`` for any exception the error handler for this - exception is not called and it shows up as regular exception in the - traceback. This is helpful for debugging implicitly raised HTTP - exceptions. - - .. versionchanged:: 1.0 - Bad request errors are not trapped by default in debug mode. - - .. versionadded:: 0.8 - """ - if self.config["TRAP_HTTP_EXCEPTIONS"]: - return True - - trap_bad_request = self.config["TRAP_BAD_REQUEST_ERRORS"] - - # if unset, trap key errors in debug mode - if ( - trap_bad_request is None - and self.debug - and isinstance(e, BadRequestKeyError) - ): - return True - - if trap_bad_request: - return isinstance(e, BadRequest) - - return False - - should_ignore_error: None = None - """If this method returns ``True``, the error will not be passed to - teardown handlers, and the context will not be preserved for - debugging. - - .. deprecated:: 3.2 - Handle errors as needed in teardown handlers instead. - - .. versionadded:: 0.10 - """ - - def redirect(self, location: str, code: int = 303) -> BaseResponse: - """Create a redirect response object. - - This is called by :func:`flask.redirect`, and can be called - directly as well. - - :param location: The URL to redirect to. - :param code: The status code for the redirect. - - .. versionchanged:: 3.2 - ``code`` defaults to ``303`` instead of ``302``. - - .. versionadded:: 2.2 - Moved from ``flask.redirect``, which calls this method. - """ - return _wz_redirect( - location, - code=code, - Response=self.response_class, # type: ignore[arg-type] - ) - - def inject_url_defaults(self, endpoint: str, values: dict[str, t.Any]) -> None: - """Injects the URL defaults for the given endpoint directly into - the values dictionary passed. This is used internally and - automatically called on URL building. - - .. versionadded:: 0.7 - """ - names: t.Iterable[str | None] = (None,) - - # url_for may be called outside a request context, parse the - # passed endpoint instead of using request.blueprints. - if "." in endpoint: - names = chain( - names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0])) - ) - - for name in names: - if name in self.url_default_functions: - for func in self.url_default_functions[name]: - func(endpoint, values) - - def handle_url_build_error( - self, error: BuildError, endpoint: str, values: dict[str, t.Any] - ) -> str: - """Called by :meth:`.url_for` if a - :exc:`~werkzeug.routing.BuildError` was raised. If this returns - a value, it will be returned by ``url_for``, otherwise the error - will be re-raised. - - Each function in :attr:`url_build_error_handlers` is called with - ``error``, ``endpoint`` and ``values``. If a function returns - ``None`` or raises a ``BuildError``, it is skipped. Otherwise, - its return value is returned by ``url_for``. - - :param error: The active ``BuildError`` being handled. - :param endpoint: The endpoint being built. - :param values: The keyword arguments passed to ``url_for``. - """ - for handler in self.url_build_error_handlers: - try: - rv = handler(error, endpoint, values) - except BuildError as e: - # make error available outside except block - error = e - else: - if rv is not None: - return rv - - # Re-raise if called with an active exception, otherwise raise - # the passed in exception. - if error is sys.exc_info()[1]: - raise - - raise error diff --git a/src/flask/sansio/blueprints.py b/src/flask/sansio/blueprints.py deleted file mode 100644 index 665816e5..00000000 --- a/src/flask/sansio/blueprints.py +++ /dev/null @@ -1,692 +0,0 @@ -from __future__ import annotations - -import os -import typing as t -from collections import defaultdict -from functools import update_wrapper - -from .. import typing as ft -from .scaffold import _endpoint_from_view_func -from .scaffold import _sentinel -from .scaffold import Scaffold -from .scaffold import setupmethod - -if t.TYPE_CHECKING: # pragma: no cover - from .app import App - -DeferredSetupFunction = t.Callable[["BlueprintSetupState"], None] -T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any]) -T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) -T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) -T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) -T_template_context_processor = t.TypeVar( - "T_template_context_processor", bound=ft.TemplateContextProcessorCallable -) -T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) -T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) -T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) -T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) -T_url_value_preprocessor = t.TypeVar( - "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable -) - - -class BlueprintSetupState: - """Temporary holder object for registering a blueprint with the - application. An instance of this class is created by the - :meth:`~flask.Blueprint.make_setup_state` method and later passed - to all register callback functions. - """ - - def __init__( - self, - blueprint: Blueprint, - app: App, - options: t.Any, - first_registration: bool, - ) -> None: - #: a reference to the current application - self.app = app - - #: a reference to the blueprint that created this setup state. - self.blueprint = blueprint - - #: a dictionary with all options that were passed to the - #: :meth:`~flask.Flask.register_blueprint` method. - self.options = options - - #: as blueprints can be registered multiple times with the - #: application and not everything wants to be registered - #: multiple times on it, this attribute can be used to figure - #: out if the blueprint was registered in the past already. - self.first_registration = first_registration - - subdomain = self.options.get("subdomain") - if subdomain is None: - subdomain = self.blueprint.subdomain - - #: The subdomain that the blueprint should be active for, ``None`` - #: otherwise. - self.subdomain = subdomain - - url_prefix = self.options.get("url_prefix") - if url_prefix is None: - url_prefix = self.blueprint.url_prefix - #: The prefix that should be used for all URLs defined on the - #: blueprint. - self.url_prefix = url_prefix - - self.name = self.options.get("name", blueprint.name) - self.name_prefix = self.options.get("name_prefix", "") - - #: A dictionary with URL defaults that is added to each and every - #: URL that was defined with the blueprint. - self.url_defaults = dict(self.blueprint.url_values_defaults) - self.url_defaults.update(self.options.get("url_defaults", ())) - - def add_url_rule( - self, - rule: str, - endpoint: str | None = None, - view_func: ft.RouteCallable | None = None, - **options: t.Any, - ) -> None: - """A helper method to register a rule (and optionally a view function) - to the application. The endpoint is automatically prefixed with the - blueprint's name. - """ - if self.url_prefix is not None: - if rule: - rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/"))) - else: - rule = self.url_prefix - options.setdefault("subdomain", self.subdomain) - if endpoint is None: - endpoint = _endpoint_from_view_func(view_func) # type: ignore - defaults = self.url_defaults - if "defaults" in options: - defaults = dict(defaults, **options.pop("defaults")) - - self.app.add_url_rule( - rule, - f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."), - view_func, - defaults=defaults, - **options, - ) - - -class Blueprint(Scaffold): - """Represents a blueprint, a collection of routes and other - app-related functions that can be registered on a real application - later. - - A blueprint is an object that allows defining application functions - without requiring an application object ahead of time. It uses the - same decorators as :class:`~flask.Flask`, but defers the need for an - application by recording them for later registration. - - Decorating a function with a blueprint creates a deferred function - that is called with :class:`~flask.blueprints.BlueprintSetupState` - when the blueprint is registered on an application. - - See :doc:`/blueprints` for more information. - - :param name: The name of the blueprint. Will be prepended to each - endpoint name. - :param import_name: The name of the blueprint package, usually - ``__name__``. This helps locate the ``root_path`` for the - blueprint. - :param static_folder: A folder with static files that should be - served by the blueprint's static route. The path is relative to - the blueprint's root path. Blueprint static files are disabled - by default. - :param static_url_path: The url to serve static files from. - Defaults to ``static_folder``. If the blueprint does not have - a ``url_prefix``, the app's static route will take precedence, - and the blueprint's static files won't be accessible. - :param template_folder: A folder with templates that should be added - to the app's template search path. The path is relative to the - blueprint's root path. Blueprint templates are disabled by - default. Blueprint templates have a lower precedence than those - in the app's templates folder. - :param url_prefix: A path to prepend to all of the blueprint's URLs, - to make them distinct from the rest of the app's routes. - :param subdomain: A subdomain that blueprint routes will match on by - default. - :param url_defaults: A dict of default values that blueprint routes - will receive by default. - :param root_path: By default, the blueprint will automatically set - this based on ``import_name``. In certain situations this - automatic detection can fail, so the path can be specified - manually instead. - - .. versionchanged:: 1.1.0 - Blueprints have a ``cli`` group to register nested CLI commands. - The ``cli_group`` parameter controls the name of the group under - the ``flask`` command. - - .. versionadded:: 0.7 - """ - - _got_registered_once = False - - def __init__( - self, - name: str, - import_name: str, - static_folder: str | os.PathLike[str] | None = None, - static_url_path: str | None = None, - template_folder: str | os.PathLike[str] | None = None, - url_prefix: str | None = None, - subdomain: str | None = None, - url_defaults: dict[str, t.Any] | None = None, - root_path: str | None = None, - cli_group: str | None = _sentinel, # type: ignore[assignment] - ): - super().__init__( - import_name=import_name, - static_folder=static_folder, - static_url_path=static_url_path, - template_folder=template_folder, - root_path=root_path, - ) - - if not name: - raise ValueError("'name' may not be empty.") - - if "." in name: - raise ValueError("'name' may not contain a dot '.' character.") - - self.name = name - self.url_prefix = url_prefix - self.subdomain = subdomain - self.deferred_functions: list[DeferredSetupFunction] = [] - - if url_defaults is None: - url_defaults = {} - - self.url_values_defaults = url_defaults - self.cli_group = cli_group - self._blueprints: list[tuple[Blueprint, dict[str, t.Any]]] = [] - - def _check_setup_finished(self, f_name: str) -> None: - if self._got_registered_once: - raise AssertionError( - f"The setup method '{f_name}' can no longer be called on the blueprint" - f" '{self.name}'. It has already been registered at least once, any" - " changes will not be applied consistently.\n" - "Make sure all imports, decorators, functions, etc. needed to set up" - " the blueprint are done before registering it." - ) - - @setupmethod - def record(self, func: DeferredSetupFunction) -> None: - """Registers a function that is called when the blueprint is - registered on the application. This function is called with the - state as argument as returned by the :meth:`make_setup_state` - method. - """ - self.deferred_functions.append(func) - - @setupmethod - def record_once(self, func: DeferredSetupFunction) -> None: - """Works like :meth:`record` but wraps the function in another - function that will ensure the function is only called once. If the - blueprint is registered a second time on the application, the - function passed is not called. - """ - - def wrapper(state: BlueprintSetupState) -> None: - if state.first_registration: - func(state) - - self.record(update_wrapper(wrapper, func)) - - def make_setup_state( - self, app: App, options: dict[str, t.Any], first_registration: bool = False - ) -> BlueprintSetupState: - """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` - object that is later passed to the register callback functions. - Subclasses can override this to return a subclass of the setup state. - """ - return BlueprintSetupState(self, app, options, first_registration) - - @setupmethod - def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: - """Register a :class:`~flask.Blueprint` on this blueprint. Keyword - arguments passed to this method will override the defaults set - on the blueprint. - - .. versionchanged:: 2.0.1 - The ``name`` option can be used to change the (pre-dotted) - name the blueprint is registered with. This allows the same - blueprint to be registered multiple times with unique names - for ``url_for``. - - .. versionadded:: 2.0 - """ - if blueprint is self: - raise ValueError("Cannot register a blueprint on itself") - self._blueprints.append((blueprint, options)) - - def register(self, app: App, options: dict[str, t.Any]) -> None: - """Called by :meth:`Flask.register_blueprint` to register all - views and callbacks registered on the blueprint with the - application. Creates a :class:`.BlueprintSetupState` and calls - each :meth:`record` callback with it. - - :param app: The application this blueprint is being registered - with. - :param options: Keyword arguments forwarded from - :meth:`~Flask.register_blueprint`. - - .. versionchanged:: 2.3 - Nested blueprints now correctly apply subdomains. - - .. versionchanged:: 2.1 - Registering the same blueprint with the same name multiple - times is an error. - - .. versionchanged:: 2.0.1 - Nested blueprints are registered with their dotted name. - This allows different blueprints with the same name to be - nested at different locations. - - .. versionchanged:: 2.0.1 - The ``name`` option can be used to change the (pre-dotted) - name the blueprint is registered with. This allows the same - blueprint to be registered multiple times with unique names - for ``url_for``. - """ - name_prefix = options.get("name_prefix", "") - self_name = options.get("name", self.name) - name = f"{name_prefix}.{self_name}".lstrip(".") - - if name in app.blueprints: - bp_desc = "this" if app.blueprints[name] is self else "a different" - existing_at = f" '{name}'" if self_name != name else "" - - raise ValueError( - f"The name '{self_name}' is already registered for" - f" {bp_desc} blueprint{existing_at}. Use 'name=' to" - f" provide a unique name." - ) - - first_bp_registration = not any(bp is self for bp in app.blueprints.values()) - first_name_registration = name not in app.blueprints - - app.blueprints[name] = self - self._got_registered_once = True - state = self.make_setup_state(app, options, first_bp_registration) - - if self.has_static_folder: - state.add_url_rule( - f"{self.static_url_path}/", - view_func=self.send_static_file, # type: ignore[attr-defined] - endpoint="static", - ) - - # Merge blueprint data into parent. - if first_bp_registration or first_name_registration: - self._merge_blueprint_funcs(app, name) - - for deferred in self.deferred_functions: - deferred(state) - - cli_resolved_group = options.get("cli_group", self.cli_group) - - if self.cli.commands: - if cli_resolved_group is None: - app.cli.commands.update(self.cli.commands) - elif cli_resolved_group is _sentinel: - self.cli.name = name - app.cli.add_command(self.cli) - else: - self.cli.name = cli_resolved_group - app.cli.add_command(self.cli) - - for blueprint, bp_options in self._blueprints: - bp_options = bp_options.copy() - bp_url_prefix = bp_options.get("url_prefix") - bp_subdomain = bp_options.get("subdomain") - - if bp_subdomain is None: - bp_subdomain = blueprint.subdomain - - if state.subdomain is not None and bp_subdomain is not None: - bp_options["subdomain"] = bp_subdomain + "." + state.subdomain - elif bp_subdomain is not None: - bp_options["subdomain"] = bp_subdomain - elif state.subdomain is not None: - bp_options["subdomain"] = state.subdomain - - if bp_url_prefix is None: - bp_url_prefix = blueprint.url_prefix - - if state.url_prefix is not None and bp_url_prefix is not None: - bp_options["url_prefix"] = ( - state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") - ) - elif bp_url_prefix is not None: - bp_options["url_prefix"] = bp_url_prefix - elif state.url_prefix is not None: - bp_options["url_prefix"] = state.url_prefix - - bp_options["name_prefix"] = name - blueprint.register(app, bp_options) - - def _merge_blueprint_funcs(self, app: App, name: str) -> None: - def extend( - bp_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], - parent_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], - ) -> None: - for key, values in bp_dict.items(): - key = name if key is None else f"{name}.{key}" - parent_dict[key].extend(values) - - for key, value in self.error_handler_spec.items(): - key = name if key is None else f"{name}.{key}" - value = defaultdict( - dict, - { - code: {exc_class: func for exc_class, func in code_values.items()} - for code, code_values in value.items() - }, - ) - app.error_handler_spec[key] = value - - for endpoint, func in self.view_functions.items(): - app.view_functions[endpoint] = func - - extend(self.before_request_funcs, app.before_request_funcs) - extend(self.after_request_funcs, app.after_request_funcs) - extend( - self.teardown_request_funcs, - app.teardown_request_funcs, - ) - extend(self.url_default_functions, app.url_default_functions) - extend(self.url_value_preprocessors, app.url_value_preprocessors) - extend(self.template_context_processors, app.template_context_processors) - - @setupmethod - def add_url_rule( - self, - rule: str, - endpoint: str | None = None, - view_func: ft.RouteCallable | None = None, - provide_automatic_options: bool | None = None, - **options: t.Any, - ) -> None: - """Register a URL rule with the blueprint. See :meth:`.Flask.add_url_rule` for - full documentation. - - The URL rule is prefixed with the blueprint's URL prefix. The endpoint name, - used with :func:`url_for`, is prefixed with the blueprint's name. - """ - if endpoint and "." in endpoint: - raise ValueError("'endpoint' may not contain a dot '.' character.") - - if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: - raise ValueError("'view_func' name may not contain a dot '.' character.") - - self.record( - lambda s: s.add_url_rule( - rule, - endpoint, - view_func, - provide_automatic_options=provide_automatic_options, - **options, - ) - ) - - @t.overload - def app_template_filter(self, name: T_template_filter) -> T_template_filter: ... - @t.overload - def app_template_filter( - self, name: str | None = None - ) -> t.Callable[[T_template_filter], T_template_filter]: ... - @setupmethod - def app_template_filter( - self, name: T_template_filter | str | None = None - ) -> T_template_filter | t.Callable[[T_template_filter], T_template_filter]: - """Decorate a function to register it as a custom Jinja filter. The name - is optional. The decorator may be used without parentheses. - - The :meth:`add_app_template_filter` method may be used to register a - function later rather than decorating. - - The filter is available in all templates, not only those under this - blueprint. Equivalent to :meth:`.Flask.template_filter`. - - :param name: The name to register the filter as. If not given, uses the - function's name. - """ - if callable(name): - self.add_app_template_filter(name) - return name - - def decorator(f: T_template_filter) -> T_template_filter: - self.add_app_template_filter(f, name=name) - return f - - return decorator - - @setupmethod - def add_app_template_filter( - self, f: ft.TemplateFilterCallable, name: str | None = None - ) -> None: - """Register a function to use as a custom Jinja filter. - - The :meth:`app_template_filter` decorator can be used to register a - function by decorating instead. - - The filter is available in all templates, not only those under this - blueprint. Equivalent to :meth:`.Flask.add_template_filter`. - - :param f: The function to register. - :param name: The name to register the filter as. If not given, uses the - function's name. - """ - - def register_template_filter(state: BlueprintSetupState) -> None: - state.app.add_template_filter(f, name=name) - - self.record_once(register_template_filter) - - @t.overload - def app_template_test(self, name: T_template_test) -> T_template_test: ... - @t.overload - def app_template_test( - self, name: str | None = None - ) -> t.Callable[[T_template_test], T_template_test]: ... - @setupmethod - def app_template_test( - self, name: T_template_test | str | None = None - ) -> T_template_test | t.Callable[[T_template_test], T_template_test]: - """Decorate a function to register it as a custom Jinja test. The name - is optional. The decorator may be used without parentheses. - - The :meth:`add_app_template_test` method may be used to register a - function later rather than decorating. - - The test is available in all templates, not only those under this - blueprint. Equivalent to :meth:`.Flask.template_test`. - - :param name: The name to register the filter as. If not given, uses the - function's name. - - .. versionadded:: 0.10 - """ - if callable(name): - self.add_app_template_test(name) - return name - - def decorator(f: T_template_test) -> T_template_test: - self.add_app_template_test(f, name=name) - return f - - return decorator - - @setupmethod - def add_app_template_test( - self, f: ft.TemplateTestCallable, name: str | None = None - ) -> None: - """Register a function to use as a custom Jinja test. - - The :meth:`app_template_test` decorator can be used to register a - function by decorating instead. - - The test is available in all templates, not only those under this - blueprint. Equivalent to :meth:`.Flask.add_template_test`. - - :param f: The function to register. - :param name: The name to register the test as. If not given, uses the - function's name. - - .. versionadded:: 0.10 - """ - - def register_template_test(state: BlueprintSetupState) -> None: - state.app.add_template_test(f, name=name) - - self.record_once(register_template_test) - - @t.overload - def app_template_global(self, name: T_template_global) -> T_template_global: ... - @t.overload - def app_template_global( - self, name: str | None = None - ) -> t.Callable[[T_template_global], T_template_global]: ... - @setupmethod - def app_template_global( - self, name: T_template_global | str | None = None - ) -> T_template_global | t.Callable[[T_template_global], T_template_global]: - """Decorate a function to register it as a custom Jinja global. The name - is optional. The decorator may be used without parentheses. - - The :meth:`add_app_template_global` method may be used to register a - function later rather than decorating. - - The global is available in all templates, not only those under this - blueprint. Equivalent to :meth:`.Flask.template_global`. - - :param name: The name to register the global as. If not given, uses the - function's name. - - .. versionadded:: 0.10 - """ - if callable(name): - self.add_app_template_global(name) - return name - - def decorator(f: T_template_global) -> T_template_global: - self.add_app_template_global(f, name=name) - return f - - return decorator - - @setupmethod - def add_app_template_global( - self, f: ft.TemplateGlobalCallable, name: str | None = None - ) -> None: - """Register a function to use as a custom Jinja global. - - The :meth:`app_template_global` decorator can be used to register a function - by decorating instead. - - The global is available in all templates, not only those under this - blueprint. Equivalent to :meth:`.Flask.add_template_global`. - - :param f: The function to register. - :param name: The name to register the global as. If not given, uses the - function's name. - - .. versionadded:: 0.10 - """ - - def register_template_global(state: BlueprintSetupState) -> None: - state.app.add_template_global(f, name=name) - - self.record_once(register_template_global) - - @setupmethod - def before_app_request(self, f: T_before_request) -> T_before_request: - """Like :meth:`before_request`, but before every request, not only those handled - by the blueprint. Equivalent to :meth:`.Flask.before_request`. - """ - self.record_once( - lambda s: s.app.before_request_funcs.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def after_app_request(self, f: T_after_request) -> T_after_request: - """Like :meth:`after_request`, but after every request, not only those handled - by the blueprint. Equivalent to :meth:`.Flask.after_request`. - """ - self.record_once( - lambda s: s.app.after_request_funcs.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def teardown_app_request(self, f: T_teardown) -> T_teardown: - """Like :meth:`teardown_request`, but after every request, not only those - handled by the blueprint. Equivalent to :meth:`.Flask.teardown_request`. - """ - self.record_once( - lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def app_context_processor( - self, f: T_template_context_processor - ) -> T_template_context_processor: - """Like :meth:`context_processor`, but for templates rendered by every view, not - only by the blueprint. Equivalent to :meth:`.Flask.context_processor`. - """ - self.record_once( - lambda s: s.app.template_context_processors.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def app_errorhandler( - self, code: type[Exception] | int - ) -> t.Callable[[T_error_handler], T_error_handler]: - """Like :meth:`errorhandler`, but for every request, not only those handled by - the blueprint. Equivalent to :meth:`.Flask.errorhandler`. - """ - - def decorator(f: T_error_handler) -> T_error_handler: - def from_blueprint(state: BlueprintSetupState) -> None: - state.app.errorhandler(code)(f) - - self.record_once(from_blueprint) - return f - - return decorator - - @setupmethod - def app_url_value_preprocessor( - self, f: T_url_value_preprocessor - ) -> T_url_value_preprocessor: - """Like :meth:`url_value_preprocessor`, but for every request, not only those - handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`. - """ - self.record_once( - lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults: - """Like :meth:`url_defaults`, but for every request, not only those handled by - the blueprint. Equivalent to :meth:`.Flask.url_defaults`. - """ - self.record_once( - lambda s: s.app.url_default_functions.setdefault(None, []).append(f) - ) - return f diff --git a/src/flask/sansio/scaffold.py b/src/flask/sansio/scaffold.py deleted file mode 100644 index a04c38ad..00000000 --- a/src/flask/sansio/scaffold.py +++ /dev/null @@ -1,792 +0,0 @@ -from __future__ import annotations - -import importlib.util -import os -import pathlib -import sys -import typing as t -from collections import defaultdict -from functools import update_wrapper - -from jinja2 import BaseLoader -from jinja2 import FileSystemLoader -from werkzeug.exceptions import default_exceptions -from werkzeug.exceptions import HTTPException -from werkzeug.utils import cached_property - -from .. import typing as ft -from ..helpers import get_root_path -from ..templating import _default_template_ctx_processor - -if t.TYPE_CHECKING: # pragma: no cover - from click import Group - -# a singleton sentinel value for parameter defaults -_sentinel = object() - -F = t.TypeVar("F", bound=t.Callable[..., t.Any]) -T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any]) -T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) -T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) -T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) -T_template_context_processor = t.TypeVar( - "T_template_context_processor", bound=ft.TemplateContextProcessorCallable -) -T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) -T_url_value_preprocessor = t.TypeVar( - "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable -) -T_route = t.TypeVar("T_route", bound=ft.RouteCallable) - - -def setupmethod(f: F) -> F: - f_name = f.__name__ - - def wrapper_func(self: Scaffold, *args: t.Any, **kwargs: t.Any) -> t.Any: - self._check_setup_finished(f_name) - return f(self, *args, **kwargs) - - return t.cast(F, update_wrapper(wrapper_func, f)) - - -class Scaffold: - """Common behavior shared between :class:`~flask.Flask` and - :class:`~flask.blueprints.Blueprint`. - - :param import_name: The import name of the module where this object - is defined. Usually :attr:`__name__` should be used. - :param static_folder: Path to a folder of static files to serve. - If this is set, a static route will be added. - :param static_url_path: URL prefix for the static route. - :param template_folder: Path to a folder containing template files. - for rendering. If this is set, a Jinja loader will be added. - :param root_path: The path that static, template, and resource files - are relative to. Typically not set, it is discovered based on - the ``import_name``. - - .. versionadded:: 2.0 - """ - - cli: Group - name: str - _static_folder: str | None = None - _static_url_path: str | None = None - - def __init__( - self, - import_name: str, - static_folder: str | os.PathLike[str] | None = None, - static_url_path: str | None = None, - template_folder: str | os.PathLike[str] | None = None, - root_path: str | None = None, - ): - #: The name of the package or module that this object belongs - #: to. Do not change this once it is set by the constructor. - self.import_name = import_name - - self.static_folder = static_folder - self.static_url_path = static_url_path - - #: The path to the templates folder, relative to - #: :attr:`root_path`, to add to the template loader. ``None`` if - #: templates should not be added. - self.template_folder = template_folder - - if root_path is None: - root_path = get_root_path(self.import_name) - - #: Absolute path to the package on the filesystem. Used to look - #: up resources contained in the package. - self.root_path = root_path - - #: A dictionary mapping endpoint names to view functions. - #: - #: To register a view function, use the :meth:`route` decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.view_functions: dict[str, ft.RouteCallable] = {} - - #: A data structure of registered error handlers, in the format - #: ``{scope: {code: {class: handler}}}``. The ``scope`` key is - #: the name of a blueprint the handlers are active for, or - #: ``None`` for all requests. The ``code`` key is the HTTP - #: status code for ``HTTPException``, or ``None`` for - #: other exceptions. The innermost dictionary maps exception - #: classes to handler functions. - #: - #: To register an error handler, use the :meth:`errorhandler` - #: decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.error_handler_spec: dict[ - ft.AppOrBlueprintKey, - dict[int | None, dict[type[Exception], ft.ErrorHandlerCallable]], - ] = defaultdict(lambda: defaultdict(dict)) - - #: A data structure of functions to call at the beginning of - #: each request, in the format ``{scope: [functions]}``. The - #: ``scope`` key is the name of a blueprint the functions are - #: active for, or ``None`` for all requests. - #: - #: To register a function, use the :meth:`before_request` - #: decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.before_request_funcs: dict[ - ft.AppOrBlueprintKey, list[ft.BeforeRequestCallable] - ] = defaultdict(list) - - #: A data structure of functions to call at the end of each - #: request, in the format ``{scope: [functions]}``. The - #: ``scope`` key is the name of a blueprint the functions are - #: active for, or ``None`` for all requests. - #: - #: To register a function, use the :meth:`after_request` - #: decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.after_request_funcs: dict[ - ft.AppOrBlueprintKey, list[ft.AfterRequestCallable[t.Any]] - ] = defaultdict(list) - - #: A data structure of functions to call at the end of each - #: request even if an exception is raised, in the format - #: ``{scope: [functions]}``. The ``scope`` key is the name of a - #: blueprint the functions are active for, or ``None`` for all - #: requests. - #: - #: To register a function, use the :meth:`teardown_request` - #: decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.teardown_request_funcs: dict[ - ft.AppOrBlueprintKey, list[ft.TeardownCallable] - ] = defaultdict(list) - - #: A data structure of functions to call to pass extra context - #: values when rendering templates, in the format - #: ``{scope: [functions]}``. The ``scope`` key is the name of a - #: blueprint the functions are active for, or ``None`` for all - #: requests. - #: - #: To register a function, use the :meth:`context_processor` - #: decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.template_context_processors: dict[ - ft.AppOrBlueprintKey, list[ft.TemplateContextProcessorCallable] - ] = defaultdict(list, {None: [_default_template_ctx_processor]}) - - #: A data structure of functions to call to modify the keyword - #: arguments passed to the view function, in the format - #: ``{scope: [functions]}``. The ``scope`` key is the name of a - #: blueprint the functions are active for, or ``None`` for all - #: requests. - #: - #: To register a function, use the - #: :meth:`url_value_preprocessor` decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.url_value_preprocessors: dict[ - ft.AppOrBlueprintKey, - list[ft.URLValuePreprocessorCallable], - ] = defaultdict(list) - - #: A data structure of functions to call to modify the keyword - #: arguments when generating URLs, in the format - #: ``{scope: [functions]}``. The ``scope`` key is the name of a - #: blueprint the functions are active for, or ``None`` for all - #: requests. - #: - #: To register a function, use the :meth:`url_defaults` - #: decorator. - #: - #: This data structure is internal. It should not be modified - #: directly and its format may change at any time. - self.url_default_functions: dict[ - ft.AppOrBlueprintKey, list[ft.URLDefaultCallable] - ] = defaultdict(list) - - def __repr__(self) -> str: - return f"<{type(self).__name__} {self.name!r}>" - - def _check_setup_finished(self, f_name: str) -> None: - raise NotImplementedError - - @property - def static_folder(self) -> str | None: - """The absolute path to the configured static folder. ``None`` - if no static folder is set. - """ - if self._static_folder is not None: - return os.path.join(self.root_path, self._static_folder) - else: - return None - - @static_folder.setter - def static_folder(self, value: str | os.PathLike[str] | None) -> None: - if value is not None: - value = os.fspath(value).rstrip(r"\/") - - self._static_folder = value - - @property - def has_static_folder(self) -> bool: - """``True`` if :attr:`static_folder` is set. - - .. versionadded:: 0.5 - """ - return self.static_folder is not None - - @property - def static_url_path(self) -> str | None: - """The URL prefix that the static route will be accessible from. - - If it was not configured during init, it is derived from - :attr:`static_folder`. - """ - if self._static_url_path is not None: - return self._static_url_path - - if self.static_folder is not None: - basename = os.path.basename(self.static_folder) - return f"/{basename}".rstrip("/") - - return None - - @static_url_path.setter - def static_url_path(self, value: str | None) -> None: - if value is not None: - value = value.rstrip("/") - - self._static_url_path = value - - @cached_property - def jinja_loader(self) -> BaseLoader | None: - """The Jinja loader for this object's templates. By default this - is a class :class:`jinja2.loaders.FileSystemLoader` to - :attr:`template_folder` if it is set. - - .. versionadded:: 0.5 - """ - if self.template_folder is not None: - return FileSystemLoader(os.path.join(self.root_path, self.template_folder)) - else: - return None - - def _method_route( - self, - method: str, - rule: str, - options: dict[str, t.Any], - ) -> t.Callable[[T_route], T_route]: - if "methods" in options: - raise TypeError("Use the 'route' decorator to use the 'methods' argument.") - - return self.route(rule, methods=[method], **options) - - @setupmethod - def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: - """Shortcut for :meth:`route` with ``methods=["GET"]``. - - .. versionadded:: 2.0 - """ - return self._method_route("GET", rule, options) - - @setupmethod - def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: - """Shortcut for :meth:`route` with ``methods=["POST"]``. - - .. versionadded:: 2.0 - """ - return self._method_route("POST", rule, options) - - @setupmethod - def put(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: - """Shortcut for :meth:`route` with ``methods=["PUT"]``. - - .. versionadded:: 2.0 - """ - return self._method_route("PUT", rule, options) - - @setupmethod - def delete(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: - """Shortcut for :meth:`route` with ``methods=["DELETE"]``. - - .. versionadded:: 2.0 - """ - return self._method_route("DELETE", rule, options) - - @setupmethod - def patch(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: - """Shortcut for :meth:`route` with ``methods=["PATCH"]``. - - .. versionadded:: 2.0 - """ - return self._method_route("PATCH", rule, options) - - @setupmethod - def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: - """Decorate a view function to register it with the given URL - rule and options. Calls :meth:`add_url_rule`, which has more - details about the implementation. - - .. code-block:: python - - @app.route("/") - def index(): - return "Hello, World!" - - See :ref:`url-route-registrations`. - - The endpoint name for the route defaults to the name of the view - function if the ``endpoint`` parameter isn't passed. - - The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` and - ``OPTIONS`` are added automatically. - - :param rule: The URL rule string. - :param options: Extra options passed to the - :class:`~werkzeug.routing.Rule` object. - """ - - def decorator(f: T_route) -> T_route: - endpoint = options.pop("endpoint", None) - self.add_url_rule(rule, endpoint, f, **options) - return f - - return decorator - - @setupmethod - def add_url_rule( - self, - rule: str, - endpoint: str | None = None, - view_func: ft.RouteCallable | None = None, - provide_automatic_options: bool | None = None, - **options: t.Any, - ) -> None: - """Register a rule for routing incoming requests and building - URLs. The :meth:`route` decorator is a shortcut to call this - with the ``view_func`` argument. These are equivalent: - - .. code-block:: python - - @app.route("/") - def index(): - ... - - .. code-block:: python - - def index(): - ... - - app.add_url_rule("/", view_func=index) - - See :ref:`url-route-registrations`. - - The endpoint name for the route defaults to the name of the view - function if the ``endpoint`` parameter isn't passed. An error - will be raised if a function has already been registered for the - endpoint. - - The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` is - always added automatically, and ``OPTIONS`` is added - automatically by default. - - ``view_func`` does not necessarily need to be passed, but if the - rule should participate in routing an endpoint name must be - associated with a view function at some point with the - :meth:`endpoint` decorator. - - .. code-block:: python - - app.add_url_rule("/", endpoint="index") - - @app.endpoint("index") - def index(): - ... - - If ``view_func`` has a ``required_methods`` attribute, those - methods are added to the passed and automatic methods. If it - has a ``provide_automatic_methods`` attribute, it is used as the - default if the parameter is not passed. - - :param rule: The URL rule string. - :param endpoint: The endpoint name to associate with the rule - and view function. Used when routing and building URLs. - Defaults to ``view_func.__name__``. - :param view_func: The view function to associate with the - endpoint name. - :param provide_automatic_options: Add the ``OPTIONS`` method and - respond to ``OPTIONS`` requests automatically. - :param options: Extra options passed to the - :class:`~werkzeug.routing.Rule` object. - """ - raise NotImplementedError - - @setupmethod - def endpoint(self, endpoint: str) -> t.Callable[[F], F]: - """Decorate a view function to register it for the given - endpoint. Used if a rule is added without a ``view_func`` with - :meth:`add_url_rule`. - - .. code-block:: python - - app.add_url_rule("/ex", endpoint="example") - - @app.endpoint("example") - def example(): - ... - - :param endpoint: The endpoint name to associate with the view - function. - """ - - def decorator(f: F) -> F: - self.view_functions[endpoint] = f - return f - - return decorator - - @setupmethod - def before_request(self, f: T_before_request) -> T_before_request: - """Register a function to run before each request. - - For example, this can be used to open a database connection, or - to load the logged in user from the session. - - .. code-block:: python - - @app.before_request - def load_user(): - if "user_id" in session: - g.user = db.session.get(session["user_id"]) - - The function will be called without any arguments. If it returns - a non-``None`` value, the value is handled as if it was the - return value from the view, and further request handling is - stopped. - - This is available on both app and blueprint objects. When used on an app, this - executes before every request. When used on a blueprint, this executes before - every request that the blueprint handles. To register with a blueprint and - execute before every request, use :meth:`.Blueprint.before_app_request`. - """ - self.before_request_funcs.setdefault(None, []).append(f) - return f - - @setupmethod - def after_request(self, f: T_after_request) -> T_after_request: - """Register a function to run after each request to this object. - - The function is called with the response object, and must return - a response object. This allows the functions to modify or - replace the response before it is sent. - - If a function raises an exception, any remaining - ``after_request`` functions will not be called. Therefore, this - should not be used for actions that must execute, such as to - close resources. Use :meth:`teardown_request` for that. - - This is available on both app and blueprint objects. When used on an app, this - executes after every request. When used on a blueprint, this executes after - every request that the blueprint handles. To register with a blueprint and - execute after every request, use :meth:`.Blueprint.after_app_request`. - """ - self.after_request_funcs.setdefault(None, []).append(f) - return f - - @setupmethod - def teardown_request(self, f: T_teardown) -> T_teardown: - """Register a function to be called when the request context is - popped. Typically, this happens at the end of each request, but - contexts may be pushed manually during testing. - - .. code-block:: python - - with app.test_request_context(): - ... - - When the ``with`` block exits (or ``ctx.pop()`` is called), the - teardown functions are called just before the request context is - made inactive. - - When a teardown function was called because of an unhandled - exception it will be passed an error object. If an - :meth:`errorhandler` is registered, it will handle the exception - and the teardown will not receive it. - - Teardown functions must avoid raising exceptions. If they - execute code that might fail they must surround that code with a - ``try``/``except`` block and log any errors. - - The return values of teardown functions are ignored. - - This is available on both app and blueprint objects. When used on an app, this - executes after every request. When used on a blueprint, this executes after - every request that the blueprint handles. To register with a blueprint and - execute after every request, use :meth:`.Blueprint.teardown_app_request`. - """ - self.teardown_request_funcs.setdefault(None, []).append(f) - return f - - @setupmethod - def context_processor( - self, - f: T_template_context_processor, - ) -> T_template_context_processor: - """Registers a template context processor function. These functions run before - rendering a template. The keys of the returned dict are added as variables - available in the template. - - This is available on both app and blueprint objects. When used on an app, this - is called for every rendered template. When used on a blueprint, this is called - for templates rendered from the blueprint's views. To register with a blueprint - and affect every template, use :meth:`.Blueprint.app_context_processor`. - """ - self.template_context_processors[None].append(f) - return f - - @setupmethod - def url_value_preprocessor( - self, - f: T_url_value_preprocessor, - ) -> T_url_value_preprocessor: - """Register a URL value preprocessor function for all view - functions in the application. These functions will be called before the - :meth:`before_request` functions. - - The function can modify the values captured from the matched url before - they are passed to the view. For example, this can be used to pop a - common language code value and place it in ``g`` rather than pass it to - every view. - - The function is passed the endpoint name and values dict. The return - value is ignored. - - This is available on both app and blueprint objects. When used on an app, this - is called for every request. When used on a blueprint, this is called for - requests that the blueprint handles. To register with a blueprint and affect - every request, use :meth:`.Blueprint.app_url_value_preprocessor`. - """ - self.url_value_preprocessors[None].append(f) - return f - - @setupmethod - def url_defaults(self, f: T_url_defaults) -> T_url_defaults: - """Callback function for URL defaults for all view functions of the - application. It's called with the endpoint and values and should - update the values passed in place. - - This is available on both app and blueprint objects. When used on an app, this - is called for every request. When used on a blueprint, this is called for - requests that the blueprint handles. To register with a blueprint and affect - every request, use :meth:`.Blueprint.app_url_defaults`. - """ - self.url_default_functions[None].append(f) - return f - - @setupmethod - def errorhandler( - self, code_or_exception: type[Exception] | int - ) -> t.Callable[[T_error_handler], T_error_handler]: - """Register a function to handle errors by code or exception class. - - A decorator that is used to register a function given an - error code. Example:: - - @app.errorhandler(404) - def page_not_found(error): - return 'This page does not exist', 404 - - You can also register handlers for arbitrary exceptions:: - - @app.errorhandler(DatabaseError) - def special_exception_handler(error): - return 'Database connection failed', 500 - - This is available on both app and blueprint objects. When used on an app, this - can handle errors from every request. When used on a blueprint, this can handle - errors from requests that the blueprint handles. To register with a blueprint - and affect every request, use :meth:`.Blueprint.app_errorhandler`. - - .. versionadded:: 0.7 - Use :meth:`register_error_handler` instead of modifying - :attr:`error_handler_spec` directly, for application wide error - handlers. - - .. versionadded:: 0.7 - One can now additionally also register custom exception types - that do not necessarily have to be a subclass of the - :class:`~werkzeug.exceptions.HTTPException` class. - - :param code_or_exception: the code as integer for the handler, or - an arbitrary exception - """ - - def decorator(f: T_error_handler) -> T_error_handler: - self.register_error_handler(code_or_exception, f) - return f - - return decorator - - @setupmethod - def register_error_handler( - self, - code_or_exception: type[Exception] | int, - f: ft.ErrorHandlerCallable, - ) -> None: - """Alternative error attach function to the :meth:`errorhandler` - decorator that is more straightforward to use for non decorator - usage. - - .. versionadded:: 0.7 - """ - exc_class, code = self._get_exc_class_and_code(code_or_exception) - self.error_handler_spec[None][code][exc_class] = f - - @staticmethod - def _get_exc_class_and_code( - exc_class_or_code: type[Exception] | int, - ) -> tuple[type[Exception], int | None]: - """Get the exception class being handled. For HTTP status codes - or ``HTTPException`` subclasses, return both the exception and - status code. - - :param exc_class_or_code: Any exception class, or an HTTP status - code as an integer. - """ - exc_class: type[Exception] - - if isinstance(exc_class_or_code, int): - try: - exc_class = default_exceptions[exc_class_or_code] - except KeyError: - raise ValueError( - f"'{exc_class_or_code}' is not a recognized HTTP" - " error code. Use a subclass of HTTPException with" - " that code instead." - ) from None - else: - exc_class = exc_class_or_code - - if isinstance(exc_class, Exception): - raise TypeError( - f"{exc_class!r} is an instance, not a class. Handlers" - " can only be registered for Exception classes or HTTP" - " error codes." - ) - - if not issubclass(exc_class, Exception): - raise ValueError( - f"'{exc_class.__name__}' is not a subclass of Exception." - " Handlers can only be registered for Exception classes" - " or HTTP error codes." - ) - - if issubclass(exc_class, HTTPException): - return exc_class, exc_class.code - else: - return exc_class, None - - -def _endpoint_from_view_func(view_func: ft.RouteCallable) -> str: - """Internal helper that returns the default endpoint for a given - function. This always is the function name. - """ - assert view_func is not None, "expected view func if endpoint is not provided." - return view_func.__name__ - - -def _find_package_path(import_name: str) -> str: - """Find the path that contains the package or module.""" - root_mod_name, _, _ = import_name.partition(".") - - try: - root_spec = importlib.util.find_spec(root_mod_name) - - if root_spec is None: - raise ValueError("not found") - except (ImportError, ValueError): - # ImportError: the machinery told us it does not exist - # ValueError: - # - the module name was invalid - # - the module name is __main__ - # - we raised `ValueError` due to `root_spec` being `None` - return os.getcwd() - - if root_spec.submodule_search_locations: - if root_spec.origin is None or root_spec.origin == "namespace": - # namespace package - package_spec = importlib.util.find_spec(import_name) - - if package_spec is not None and package_spec.submodule_search_locations: - # Pick the path in the namespace that contains the submodule. - package_path = pathlib.Path( - os.path.commonpath(package_spec.submodule_search_locations) - ) - search_location = next( - location - for location in root_spec.submodule_search_locations - if package_path.is_relative_to(location) - ) - else: - # Pick the first path. - search_location = root_spec.submodule_search_locations[0] - - return os.path.dirname(search_location) - else: - # package with __init__.py - return os.path.dirname(os.path.dirname(root_spec.origin)) - else: - # module - return os.path.dirname(root_spec.origin) # type: ignore[type-var, return-value] - - -def find_package(import_name: str) -> tuple[str | None, str]: - """Find the prefix that a package is installed under, and the path - that it would be imported from. - - The prefix is the directory containing the standard directory - hierarchy (lib, bin, etc.). If the package is not installed to the - system (:attr:`sys.prefix`) or a virtualenv (``site-packages``), - ``None`` is returned. - - The path is the entry in :attr:`sys.path` that contains the package - for import. If the package is not installed, it's assumed that the - package was imported from the current working directory. - """ - package_path = _find_package_path(import_name) - py_prefix = os.path.abspath(sys.prefix) - - # installed to the system - if pathlib.PurePath(package_path).is_relative_to(py_prefix): - return py_prefix, package_path - - site_parent, site_folder = os.path.split(package_path) - - # installed to a virtualenv - if site_folder.lower() == "site-packages": - parent, folder = os.path.split(site_parent) - - # Windows (prefix/lib/site-packages) - if folder.lower() == "lib": - return parent, package_path - - # Unix (prefix/lib/pythonX.Y/site-packages) - if os.path.basename(parent).lower() == "lib": - return os.path.dirname(parent), package_path - - # something else (prefix/site-packages) - return site_parent, package_path - - # not installed - return None, package_path diff --git a/src/flask/sessions.py b/src/flask/sessions.py deleted file mode 100644 index ad357706..00000000 --- a/src/flask/sessions.py +++ /dev/null @@ -1,385 +0,0 @@ -from __future__ import annotations - -import collections.abc as c -import hashlib -import typing as t -from collections.abc import MutableMapping -from datetime import datetime -from datetime import timezone - -from itsdangerous import BadSignature -from itsdangerous import URLSafeTimedSerializer -from werkzeug.datastructures import CallbackDict - -from .json.tag import TaggedJSONSerializer - -if t.TYPE_CHECKING: # pragma: no cover - import typing_extensions as te - - from .app import Flask - from .wrappers import Request - from .wrappers import Response - - -class SessionMixin(MutableMapping[str, t.Any]): - """Expands a basic dictionary with session attributes.""" - - @property - def permanent(self) -> bool: - """This reflects the ``'_permanent'`` key in the dict.""" - return self.get("_permanent", False) # type: ignore[no-any-return] - - @permanent.setter - def permanent(self, value: bool) -> None: - self["_permanent"] = bool(value) - - #: Some implementations can detect whether a session is newly - #: created, but that is not guaranteed. Use with caution. The mixin - # default is hard-coded ``False``. - new = False - - #: Some implementations can detect changes to the session and set - #: this when that happens. The mixin default is hard coded to - #: ``True``. - modified = True - - accessed = False - """Indicates if the session was accessed, even if it was not modified. This - is set when the session object is accessed through the request context, - including the global :data:`.session` proxy. A ``Vary: cookie`` header will - be added if this is ``True``. - - .. versionchanged:: 3.1.3 - This is tracked by the request context. - """ - - -class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin): - """Base class for sessions based on signed cookies. - - This session backend will set the :attr:`modified` and - :attr:`accessed` attributes. It cannot reliably track whether a - session is new (vs. empty), so :attr:`new` remains hard coded to - ``False``. - """ - - #: When data is changed, this is set to ``True``. Only the session - #: dictionary itself is tracked; if the session contains mutable - #: data (for example a nested dict) then this must be set to - #: ``True`` manually when modifying that data. The session cookie - #: will only be written to the response if this is ``True``. - modified = False - - def __init__( - self, - initial: c.Mapping[str, t.Any] | None = None, - ) -> None: - def on_update(self: te.Self) -> None: - self.modified = True - - super().__init__(initial, on_update) - - -class NullSession(SecureCookieSession): - """Class used to generate nicer error messages if sessions are not - available. Will still allow read-only access to the empty session - but fail on setting. - """ - - def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: - raise RuntimeError( - "The session is unavailable because no secret " - "key was set. Set the secret_key on the " - "application to something unique and secret." - ) - - __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail - del _fail - - -class SessionInterface: - """The basic interface you have to implement in order to replace the - default session interface which uses werkzeug's securecookie - implementation. The only methods you have to implement are - :meth:`open_session` and :meth:`save_session`, the others have - useful defaults which you don't need to change. - - The session object returned by the :meth:`open_session` method has to - provide a dictionary like interface plus the properties and methods - from the :class:`SessionMixin`. We recommend just subclassing a dict - and adding that mixin:: - - class Session(dict, SessionMixin): - pass - - If :meth:`open_session` returns ``None`` Flask will call into - :meth:`make_null_session` to create a session that acts as replacement - if the session support cannot work because some requirement is not - fulfilled. The default :class:`NullSession` class that is created - will complain that the secret key was not set. - - To replace the session interface on an application all you have to do - is to assign :attr:`flask.Flask.session_interface`:: - - app = Flask(__name__) - app.session_interface = MySessionInterface() - - Multiple requests with the same session may be sent and handled - concurrently. When implementing a new session interface, consider - whether reads or writes to the backing store must be synchronized. - There is no guarantee on the order in which the session for each - request is opened or saved, it will occur in the order that requests - begin and end processing. - - .. versionadded:: 0.8 - """ - - #: :meth:`make_null_session` will look here for the class that should - #: be created when a null session is requested. Likewise the - #: :meth:`is_null_session` method will perform a typecheck against - #: this type. - null_session_class = NullSession - - #: A flag that indicates if the session interface is pickle based. - #: This can be used by Flask extensions to make a decision in regards - #: to how to deal with the session object. - #: - #: .. versionadded:: 0.10 - pickle_based = False - - def make_null_session(self, app: Flask) -> NullSession: - """Creates a null session which acts as a replacement object if the - real session support could not be loaded due to a configuration - error. This mainly aids the user experience because the job of the - null session is to still support lookup without complaining but - modifications are answered with a helpful error message of what - failed. - - This creates an instance of :attr:`null_session_class` by default. - """ - return self.null_session_class() - - def is_null_session(self, obj: object) -> bool: - """Checks if a given object is a null session. Null sessions are - not asked to be saved. - - This checks if the object is an instance of :attr:`null_session_class` - by default. - """ - return isinstance(obj, self.null_session_class) - - def get_cookie_name(self, app: Flask) -> str: - """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``.""" - return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return] - - def get_cookie_domain(self, app: Flask) -> str | None: - """The value of the ``Domain`` parameter on the session cookie. If not set, - browsers will only send the cookie to the exact domain it was set from. - Otherwise, they will send it to any subdomain of the given value as well. - - Uses the :data:`SESSION_COOKIE_DOMAIN` config. - - .. versionchanged:: 2.3 - Not set by default, does not fall back to ``SERVER_NAME``. - """ - return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return] - - def get_cookie_path(self, app: Flask) -> str: - """Returns the path for which the cookie should be valid. The - default implementation uses the value from the ``SESSION_COOKIE_PATH`` - config var if it's set, and falls back to ``APPLICATION_ROOT`` or - uses ``/`` if it's ``None``. - """ - return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return] - - def get_cookie_httponly(self, app: Flask) -> bool: - """Returns True if the session cookie should be httponly. This - currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` - config var. - """ - return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return] - - def get_cookie_secure(self, app: Flask) -> bool: - """Returns True if the cookie should be secure. This currently - just returns the value of the ``SESSION_COOKIE_SECURE`` setting. - """ - return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return] - - def get_cookie_samesite(self, app: Flask) -> str | None: - """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the - ``SameSite`` attribute. This currently just returns the value of - the :data:`SESSION_COOKIE_SAMESITE` setting. - """ - return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return] - - def get_cookie_partitioned(self, app: Flask) -> bool: - """Returns True if the cookie should be partitioned. By default, uses - the value of :data:`SESSION_COOKIE_PARTITIONED`. - - .. versionadded:: 3.1 - """ - return app.config["SESSION_COOKIE_PARTITIONED"] # type: ignore[no-any-return] - - def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None: - """A helper method that returns an expiration date for the session - or ``None`` if the session is linked to the browser session. The - default implementation returns now + the permanent session - lifetime configured on the application. - """ - if session.permanent: - return datetime.now(timezone.utc) + app.permanent_session_lifetime - return None - - def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool: - """Used by session backends to determine if a ``Set-Cookie`` header - should be set for this session cookie for this response. If the session - has been modified, the cookie is set. If the session is permanent and - the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is - always set. - - This check is usually skipped if the session was deleted. - - .. versionadded:: 0.11 - """ - - return session.modified or ( - session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"] - ) - - def open_session(self, app: Flask, request: Request) -> SessionMixin | None: - """This is called at the beginning of each request, after - pushing the request context, before matching the URL. - - This must return an object which implements a dictionary-like - interface as well as the :class:`SessionMixin` interface. - - This will return ``None`` to indicate that loading failed in - some way that is not immediately an error. The request - context will fall back to using :meth:`make_null_session` - in this case. - """ - raise NotImplementedError() - - def save_session( - self, app: Flask, session: SessionMixin, response: Response - ) -> None: - """This is called at the end of each request, after generating - a response, before removing the request context. It is skipped - if :meth:`is_null_session` returns ``True``. - """ - raise NotImplementedError() - - -session_json_serializer = TaggedJSONSerializer() - - -def _lazy_sha1(string: bytes = b"") -> t.Any: - """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include - SHA-1, in which case the import and use as a default would fail before the - developer can configure something else. - """ - return hashlib.sha1(string) - - -class SecureCookieSessionInterface(SessionInterface): - """The default session interface that stores sessions in signed cookies - through the :mod:`itsdangerous` module. - """ - - #: the salt that should be applied on top of the secret key for the - #: signing of cookie based sessions. - salt = "cookie-session" - #: the hash function to use for the signature. The default is sha1 - digest_method = staticmethod(_lazy_sha1) - #: the name of the itsdangerous supported key derivation. The default - #: is hmac. - key_derivation = "hmac" - #: A python serializer for the payload. The default is a compact - #: JSON derived serializer with support for some extra Python types - #: such as datetime objects or tuples. - serializer = session_json_serializer - session_class = SecureCookieSession - - def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None: - if not app.secret_key: - return None - - keys: list[str | bytes] = [] - - if fallbacks := app.config["SECRET_KEY_FALLBACKS"]: - keys.extend(fallbacks) - - keys.append(app.secret_key) # itsdangerous expects current key at top - return URLSafeTimedSerializer( - keys, # type: ignore[arg-type] - salt=self.salt, - serializer=self.serializer, - signer_kwargs={ - "key_derivation": self.key_derivation, - "digest_method": self.digest_method, - }, - ) - - def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None: - s = self.get_signing_serializer(app) - if s is None: - return None - val = request.cookies.get(self.get_cookie_name(app)) - if not val: - return self.session_class() - max_age = int(app.permanent_session_lifetime.total_seconds()) - try: - data = s.loads(val, max_age=max_age) - return self.session_class(data) - except BadSignature: - return self.session_class() - - def save_session( - self, app: Flask, session: SessionMixin, response: Response - ) -> None: - name = self.get_cookie_name(app) - domain = self.get_cookie_domain(app) - path = self.get_cookie_path(app) - secure = self.get_cookie_secure(app) - partitioned = self.get_cookie_partitioned(app) - samesite = self.get_cookie_samesite(app) - httponly = self.get_cookie_httponly(app) - - # Add a "Vary: Cookie" header if the session was accessed at all. - if session.accessed: - response.vary.add("Cookie") - - # If the session is modified to be empty, remove the cookie. - # If the session is empty, return without setting the cookie. - if not session: - if session.modified: - response.delete_cookie( - name, - domain=domain, - path=path, - secure=secure, - partitioned=partitioned, - samesite=samesite, - httponly=httponly, - ) - response.vary.add("Cookie") - - return - - if not self.should_set_cookie(app, session): - return - - expires = self.get_expiration_time(app, session) - val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore[union-attr] - response.set_cookie( - name, - val, - expires=expires, - httponly=httponly, - domain=domain, - path=path, - secure=secure, - partitioned=partitioned, - samesite=samesite, - ) - response.vary.add("Cookie") diff --git a/src/flask/signals.py b/src/flask/signals.py deleted file mode 100644 index 444fda99..00000000 --- a/src/flask/signals.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -from blinker import Namespace - -# This namespace is only for signals provided by Flask itself. -_signals = Namespace() - -template_rendered = _signals.signal("template-rendered") -before_render_template = _signals.signal("before-render-template") -request_started = _signals.signal("request-started") -request_finished = _signals.signal("request-finished") -request_tearing_down = _signals.signal("request-tearing-down") -got_request_exception = _signals.signal("got-request-exception") -appcontext_tearing_down = _signals.signal("appcontext-tearing-down") -appcontext_pushed = _signals.signal("appcontext-pushed") -appcontext_popped = _signals.signal("appcontext-popped") -message_flashed = _signals.signal("message-flashed") diff --git a/src/flask/templating.py b/src/flask/templating.py deleted file mode 100644 index 005108cc..00000000 --- a/src/flask/templating.py +++ /dev/null @@ -1,212 +0,0 @@ -from __future__ import annotations - -import typing as t - -from jinja2 import BaseLoader -from jinja2 import Environment as BaseEnvironment -from jinja2 import Template -from jinja2 import TemplateNotFound - -from .ctx import AppContext -from .globals import app_ctx -from .helpers import stream_with_context -from .signals import before_render_template -from .signals import template_rendered - -if t.TYPE_CHECKING: # pragma: no cover - from .sansio.app import App - from .sansio.scaffold import Scaffold - - -def _default_template_ctx_processor() -> dict[str, t.Any]: - """Default template context processor. Replaces the ``request`` and ``g`` - proxies with their concrete objects for faster access. - """ - ctx = app_ctx._get_current_object() - rv: dict[str, t.Any] = {"g": ctx.g} - - if ctx.has_request: - rv["request"] = ctx.request - # The session proxy cannot be replaced, accessing it gets - # RequestContext.session, which sets session.accessed. - - return rv - - -class Environment(BaseEnvironment): - """Works like a regular Jinja environment but has some additional - knowledge of how Flask's blueprint works so that it can prepend the - name of the blueprint to referenced templates if necessary. - """ - - def __init__(self, app: App, **options: t.Any) -> None: - if "loader" not in options: - options["loader"] = app.create_global_jinja_loader() - BaseEnvironment.__init__(self, **options) - self.app = app - - -class DispatchingJinjaLoader(BaseLoader): - """A loader that looks for templates in the application and all - the blueprint folders. - """ - - def __init__(self, app: App) -> None: - self.app = app - - def get_source( - self, environment: BaseEnvironment, template: str - ) -> tuple[str, str | None, t.Callable[[], bool] | None]: - if self.app.config["EXPLAIN_TEMPLATE_LOADING"]: - return self._get_source_explained(environment, template) - return self._get_source_fast(environment, template) - - def _get_source_explained( - self, environment: BaseEnvironment, template: str - ) -> tuple[str, str | None, t.Callable[[], bool] | None]: - attempts = [] - rv: tuple[str, str | None, t.Callable[[], bool] | None] | None - trv: None | (tuple[str, str | None, t.Callable[[], bool] | None]) = None - - for srcobj, loader in self._iter_loaders(template): - try: - rv = loader.get_source(environment, template) - if trv is None: - trv = rv - except TemplateNotFound: - rv = None - attempts.append((loader, srcobj, rv)) - - from .debughelpers import explain_template_loading_attempts - - explain_template_loading_attempts(self.app, template, attempts) - - if trv is not None: - return trv - raise TemplateNotFound(template) - - def _get_source_fast( - self, environment: BaseEnvironment, template: str - ) -> tuple[str, str | None, t.Callable[[], bool] | None]: - for _srcobj, loader in self._iter_loaders(template): - try: - return loader.get_source(environment, template) - except TemplateNotFound: - continue - raise TemplateNotFound(template) - - def _iter_loaders(self, template: str) -> t.Iterator[tuple[Scaffold, BaseLoader]]: - loader = self.app.jinja_loader - if loader is not None: - yield self.app, loader - - for blueprint in self.app.iter_blueprints(): - loader = blueprint.jinja_loader - if loader is not None: - yield blueprint, loader - - def list_templates(self) -> list[str]: - result = set() - loader = self.app.jinja_loader - if loader is not None: - result.update(loader.list_templates()) - - for blueprint in self.app.iter_blueprints(): - loader = blueprint.jinja_loader - if loader is not None: - for template in loader.list_templates(): - result.add(template) - - return list(result) - - -def _render(ctx: AppContext, template: Template, context: dict[str, t.Any]) -> str: - app = ctx.app - app.update_template_context(ctx, context) - before_render_template.send( - app, _async_wrapper=app.ensure_sync, template=template, context=context - ) - rv = template.render(context) - template_rendered.send( - app, _async_wrapper=app.ensure_sync, template=template, context=context - ) - return rv - - -def render_template( - template_name_or_list: str | Template | list[str | Template], - **context: t.Any, -) -> str: - """Render a template by name with the given context. - - :param template_name_or_list: The name of the template to render. If - a list is given, the first name to exist will be rendered. - :param context: The variables to make available in the template. - """ - ctx = app_ctx._get_current_object() - template = ctx.app.jinja_env.get_or_select_template(template_name_or_list) - return _render(ctx, template, context) - - -def render_template_string(source: str, **context: t.Any) -> str: - """Render a template from the given source string with the given - context. - - :param source: The source code of the template to render. - :param context: The variables to make available in the template. - """ - ctx = app_ctx._get_current_object() - template = ctx.app.jinja_env.from_string(source) - return _render(ctx, template, context) - - -def _stream( - ctx: AppContext, template: Template, context: dict[str, t.Any] -) -> t.Iterator[str]: - app = ctx.app - app.update_template_context(ctx, context) - before_render_template.send( - app, _async_wrapper=app.ensure_sync, template=template, context=context - ) - - def generate() -> t.Iterator[str]: - yield from template.generate(context) - template_rendered.send( - app, _async_wrapper=app.ensure_sync, template=template, context=context - ) - - return stream_with_context(generate()) - - -def stream_template( - template_name_or_list: str | Template | list[str | Template], - **context: t.Any, -) -> t.Iterator[str]: - """Render a template by name with the given context as a stream. - This returns an iterator of strings, which can be used as a - streaming response from a view. - - :param template_name_or_list: The name of the template to render. If - a list is given, the first name to exist will be rendered. - :param context: The variables to make available in the template. - - .. versionadded:: 2.2 - """ - ctx = app_ctx._get_current_object() - template = ctx.app.jinja_env.get_or_select_template(template_name_or_list) - return _stream(ctx, template, context) - - -def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]: - """Render a template from the given source string with the given - context as a stream. This returns an iterator of strings, which can - be used as a streaming response from a view. - - :param source: The source code of the template to render. - :param context: The variables to make available in the template. - - .. versionadded:: 2.2 - """ - ctx = app_ctx._get_current_object() - template = ctx.app.jinja_env.from_string(source) - return _stream(ctx, template, context) diff --git a/src/flask/testing.py b/src/flask/testing.py deleted file mode 100644 index 68b1ab48..00000000 --- a/src/flask/testing.py +++ /dev/null @@ -1,298 +0,0 @@ -from __future__ import annotations - -import importlib.metadata -import typing as t -from contextlib import contextmanager -from contextlib import ExitStack -from copy import copy -from types import TracebackType -from urllib.parse import urlsplit - -import werkzeug.test -from click.testing import CliRunner -from click.testing import Result -from werkzeug.test import Client -from werkzeug.wrappers import Request as BaseRequest - -from .cli import ScriptInfo -from .sessions import SessionMixin - -if t.TYPE_CHECKING: # pragma: no cover - from _typeshed.wsgi import WSGIEnvironment - from werkzeug.test import TestResponse - - from .app import Flask - - -class EnvironBuilder(werkzeug.test.EnvironBuilder): - """An :class:`~werkzeug.test.EnvironBuilder`, that takes defaults from the - application. - - :param app: The Flask application to configure the environment from. - :param path: URL path being requested. - :param base_url: Base URL where the app is being served, which - ``path`` is relative to. If not given, built from - :data:`PREFERRED_URL_SCHEME`, ``subdomain``, - :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. - :param subdomain: Subdomain name to append to :data:`SERVER_NAME`. - :param url_scheme: Scheme to use instead of - :data:`PREFERRED_URL_SCHEME`. - :param json: If given, this is serialized as JSON and passed as - ``data``. Also defaults ``content_type`` to - ``application/json``. - :param args: other positional arguments passed to - :class:`~werkzeug.test.EnvironBuilder`. - :param kwargs: other keyword arguments passed to - :class:`~werkzeug.test.EnvironBuilder`. - """ - - def __init__( - self, - app: Flask, - path: str = "/", - base_url: str | None = None, - subdomain: str | None = None, - url_scheme: str | None = None, - *args: t.Any, - **kwargs: t.Any, - ) -> None: - assert not (base_url or subdomain or url_scheme) or ( - base_url is not None - ) != bool(subdomain or url_scheme), ( - 'Cannot pass "subdomain" or "url_scheme" with "base_url".' - ) - - if base_url is None: - http_host = app.config.get("SERVER_NAME") or "localhost" - app_root = app.config["APPLICATION_ROOT"] - - if subdomain: - http_host = f"{subdomain}.{http_host}" - - if url_scheme is None: - url_scheme = app.config["PREFERRED_URL_SCHEME"] - - url = urlsplit(path) - base_url = ( - f"{url.scheme or url_scheme}://{url.netloc or http_host}" - f"/{app_root.lstrip('/')}" - ) - path = url.path - - if url.query: - path = f"{path}?{url.query}" - - self.app = app - super().__init__(path, base_url, *args, **kwargs) - - def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: - """Serialize ``obj`` to a JSON-formatted string. - - The serialization will be configured according to the config associated - with this EnvironBuilder's ``app``. - """ - return self.app.json.dumps(obj, **kwargs) - - -_werkzeug_version = "" - - -def _get_werkzeug_version() -> str: - global _werkzeug_version - - if not _werkzeug_version: - _werkzeug_version = importlib.metadata.version("werkzeug") - - return _werkzeug_version - - -class FlaskClient(Client): - """Works like a regular Werkzeug test client, with additional behavior for - Flask. Can defer the cleanup of the request context until the end of a - ``with`` block. For general information about how to use this class refer to - :class:`werkzeug.test.Client`. - - .. versionchanged:: 0.12 - `app.test_client()` includes preset default environment, which can be - set after instantiation of the `app.test_client()` object in - `client.environ_base`. - - Basic usage is outlined in the :doc:`/testing` chapter. - """ - - application: Flask - - def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: - super().__init__(*args, **kwargs) - self.preserve_context = False - self._new_contexts: list[t.ContextManager[t.Any]] = [] - self._context_stack = ExitStack() - self.environ_base = { - "REMOTE_ADDR": "127.0.0.1", - "HTTP_USER_AGENT": f"Werkzeug/{_get_werkzeug_version()}", - } - - @contextmanager - def session_transaction( - self, *args: t.Any, **kwargs: t.Any - ) -> t.Iterator[SessionMixin]: - """When used in combination with a ``with`` statement this opens a - session transaction. This can be used to modify the session that - the test client uses. Once the ``with`` block is left the session is - stored back. - - :: - - with client.session_transaction() as session: - session['value'] = 42 - - Internally this is implemented by going through a temporary test - request context and since session handling could depend on - request variables this function accepts the same arguments as - :meth:`~flask.Flask.test_request_context` which are directly - passed through. - """ - if self._cookies is None: - raise TypeError( - "Cookies are disabled. Create a client with 'use_cookies=True'." - ) - - app = self.application - ctx = app.test_request_context(*args, **kwargs) - self._add_cookies_to_wsgi(ctx.request.environ) - - with ctx: - sess = app.session_interface.open_session(app, ctx.request) - - if sess is None: - raise RuntimeError("Session backend did not open a session.") - - yield sess - resp = app.response_class() - - if app.session_interface.is_null_session(sess): - return - - with ctx: - app.session_interface.save_session(app, sess, resp) - - self._update_cookies_from_response( - ctx.request.host.partition(":")[0], - ctx.request.path, - resp.headers.getlist("Set-Cookie"), - ) - - def _copy_environ(self, other: WSGIEnvironment) -> WSGIEnvironment: - out = {**self.environ_base, **other} - - if self.preserve_context: - out["werkzeug.debug.preserve_context"] = self._new_contexts.append - - return out - - def _request_from_builder_args( - self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] - ) -> BaseRequest: - kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {})) - builder = EnvironBuilder(self.application, *args, **kwargs) - - try: - return builder.get_request() - finally: - builder.close() - - def open( - self, - *args: t.Any, - buffered: bool = False, - follow_redirects: bool = False, - **kwargs: t.Any, - ) -> TestResponse: - if args and isinstance( - args[0], (werkzeug.test.EnvironBuilder, dict, BaseRequest) - ): - if isinstance(args[0], werkzeug.test.EnvironBuilder): - builder = copy(args[0]) - builder.environ_base = self._copy_environ(builder.environ_base or {}) # type: ignore[arg-type] - request = builder.get_request() - elif isinstance(args[0], dict): - request = EnvironBuilder.from_environ( - args[0], app=self.application, environ_base=self._copy_environ({}) - ).get_request() - else: - # isinstance(args[0], BaseRequest) - request = copy(args[0]) - request.environ = self._copy_environ(request.environ) - else: - # request is None - request = self._request_from_builder_args(args, kwargs) - - # Pop any previously preserved contexts. This prevents contexts - # from being preserved across redirects or multiple requests - # within a single block. - self._context_stack.close() - - response = super().open( - request, - buffered=buffered, - follow_redirects=follow_redirects, - ) - response.json_module = self.application.json # type: ignore[assignment] - - # Re-push contexts that were preserved during the request. - for cm in self._new_contexts: - self._context_stack.enter_context(cm) - - self._new_contexts.clear() - return response - - def __enter__(self) -> FlaskClient: - if self.preserve_context: - raise RuntimeError("Cannot nest client invocations") - self.preserve_context = True - return self - - def __exit__( - self, - exc_type: type | None, - exc_value: BaseException | None, - tb: TracebackType | None, - ) -> None: - self.preserve_context = False - self._context_stack.close() - - -class FlaskCliRunner(CliRunner): - """A :class:`~click.testing.CliRunner` for testing a Flask app's - CLI commands. Typically created using - :meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`. - """ - - def __init__(self, app: Flask, **kwargs: t.Any) -> None: - self.app = app - super().__init__(**kwargs) - - def invoke( # type: ignore - self, cli: t.Any = None, args: t.Any = None, **kwargs: t.Any - ) -> Result: - """Invokes a CLI command in an isolated environment. See - :meth:`CliRunner.invoke ` for - full method documentation. See :ref:`testing-cli` for examples. - - If the ``obj`` argument is not given, passes an instance of - :class:`~flask.cli.ScriptInfo` that knows how to load the Flask - app being tested. - - :param cli: Command object to invoke. Default is the app's - :attr:`~flask.app.Flask.cli` group. - :param args: List of strings to invoke the command with. - - :return: a :class:`~click.testing.Result` object. - """ - if cli is None: - cli = self.app.cli - - if "obj" not in kwargs: - kwargs["obj"] = ScriptInfo(create_app=lambda: self.app) - - return super().invoke(cli, args, **kwargs) diff --git a/src/flask/typing.py b/src/flask/typing.py deleted file mode 100644 index 54950616..00000000 --- a/src/flask/typing.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import annotations - -import collections.abc as cabc -import typing as t - -if t.TYPE_CHECKING: # pragma: no cover - from _typeshed.wsgi import WSGIApplication # noqa: F401 - from werkzeug.datastructures import Headers # noqa: F401 - from werkzeug.sansio.response import Response # noqa: F401 - -# The possible types that are directly convertible or are a Response object. -ResponseValue = t.Union[ - "Response", - str, - bytes, - list[t.Any], - # Only dict is actually accepted, but Mapping allows for TypedDict. - t.Mapping[str, t.Any], - t.Iterator[str], - t.Iterator[bytes], - cabc.AsyncIterable[str], # for Quart, until App is generic. - cabc.AsyncIterable[bytes], -] - -# the possible types for an individual HTTP header -HeaderValue = str | list[str] | tuple[str, ...] - -# the possible types for HTTP headers -HeadersValue = t.Union[ - "Headers", - t.Mapping[str, HeaderValue], - t.Sequence[tuple[str, HeaderValue]], -] - -# The possible types returned by a route function. -ResponseReturnValue = t.Union[ - ResponseValue, - tuple[ResponseValue, HeadersValue], - tuple[ResponseValue, int], - tuple[ResponseValue, int, HeadersValue], - "WSGIApplication", -] - -# Allow any subclass of werkzeug.Response, such as the one from Flask, -# as a callback argument. Using werkzeug.Response directly makes a -# callback annotated with flask.Response fail type checking. -ResponseClass = t.TypeVar("ResponseClass", bound="Response") - -AppOrBlueprintKey = str | None # The App key is None, whereas blueprints are named -AfterRequestCallable = ( - t.Callable[[ResponseClass], ResponseClass] - | t.Callable[[ResponseClass], t.Awaitable[ResponseClass]] -) -BeforeFirstRequestCallable = t.Callable[[], None] | t.Callable[[], t.Awaitable[None]] -BeforeRequestCallable = ( - t.Callable[[], ResponseReturnValue | None] - | t.Callable[[], t.Awaitable[ResponseReturnValue | None]] -) -ShellContextProcessorCallable = t.Callable[[], dict[str, t.Any]] -TeardownCallable = ( - t.Callable[[BaseException | None], None] - | t.Callable[[BaseException | None], t.Awaitable[None]] -) -TemplateContextProcessorCallable = ( - t.Callable[[], dict[str, t.Any]] | t.Callable[[], t.Awaitable[dict[str, t.Any]]] -) -TemplateFilterCallable = t.Callable[..., t.Any] -TemplateGlobalCallable = t.Callable[..., t.Any] -TemplateTestCallable = t.Callable[..., bool] -URLDefaultCallable = t.Callable[[str, dict[str, t.Any]], None] -URLValuePreprocessorCallable = t.Callable[[str | None, dict[str, t.Any] | None], None] - -# This should take Exception, but that either breaks typing the argument -# with a specific exception, or decorating multiple times with different -# exceptions (and using a union type on the argument). -# https://github.com/pallets/flask/issues/4095 -# https://github.com/pallets/flask/issues/4295 -# https://github.com/pallets/flask/issues/4297 -ErrorHandlerCallable = ( - t.Callable[[t.Any], ResponseReturnValue] - | t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]] -) - -RouteCallable = ( - t.Callable[..., ResponseReturnValue] - | t.Callable[..., t.Awaitable[ResponseReturnValue]] -) diff --git a/src/flask/views.py b/src/flask/views.py deleted file mode 100644 index 53fe976d..00000000 --- a/src/flask/views.py +++ /dev/null @@ -1,191 +0,0 @@ -from __future__ import annotations - -import typing as t - -from . import typing as ft -from .globals import current_app -from .globals import request - -F = t.TypeVar("F", bound=t.Callable[..., t.Any]) - -http_method_funcs = frozenset( - ["get", "post", "head", "options", "delete", "put", "trace", "patch"] -) - - -class View: - """Subclass this class and override :meth:`dispatch_request` to - create a generic class-based view. Call :meth:`as_view` to create a - view function that creates an instance of the class with the given - arguments and calls its ``dispatch_request`` method with any URL - variables. - - See :doc:`views` for a detailed guide. - - .. code-block:: python - - class Hello(View): - init_every_request = False - - def dispatch_request(self, name): - return f"Hello, {name}!" - - app.add_url_rule( - "/hello/", view_func=Hello.as_view("hello") - ) - - Set :attr:`methods` on the class to change what methods the view - accepts. - - Set :attr:`decorators` on the class to apply a list of decorators to - the generated view function. Decorators applied to the class itself - will not be applied to the generated view function! - - Set :attr:`init_every_request` to ``False`` for efficiency, unless - you need to store request-global data on ``self``. - """ - - #: The methods this view is registered for. Uses the same default - #: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and - #: ``add_url_rule`` by default. - methods: t.ClassVar[t.Collection[str] | None] = None - - #: Control whether the ``OPTIONS`` method is handled automatically. - #: Uses the same default (``True``) as ``route`` and - #: ``add_url_rule`` by default. - provide_automatic_options: t.ClassVar[bool | None] = None - - #: A list of decorators to apply, in order, to the generated view - #: function. Remember that ``@decorator`` syntax is applied bottom - #: to top, so the first decorator in the list would be the bottom - #: decorator. - #: - #: .. versionadded:: 0.8 - decorators: t.ClassVar[list[t.Callable[..., t.Any]]] = [] - - #: Create a new instance of this view class for every request by - #: default. If a view subclass sets this to ``False``, the same - #: instance is used for every request. - #: - #: A single instance is more efficient, especially if complex setup - #: is done during init. However, storing data on ``self`` is no - #: longer safe across requests, and :data:`~flask.g` should be used - #: instead. - #: - #: .. versionadded:: 2.2 - init_every_request: t.ClassVar[bool] = True - - def dispatch_request(self) -> ft.ResponseReturnValue: - """The actual view function behavior. Subclasses must override - this and return a valid response. Any variables from the URL - rule are passed as keyword arguments. - """ - raise NotImplementedError() - - @classmethod - def as_view( - cls, name: str, *class_args: t.Any, **class_kwargs: t.Any - ) -> ft.RouteCallable: - """Convert the class into a view function that can be registered - for a route. - - By default, the generated view will create a new instance of the - view class for every request and call its - :meth:`dispatch_request` method. If the view class sets - :attr:`init_every_request` to ``False``, the same instance will - be used for every request. - - Except for ``name``, all other arguments passed to this method - are forwarded to the view class ``__init__`` method. - - .. versionchanged:: 2.2 - Added the ``init_every_request`` class attribute. - """ - if cls.init_every_request: - - def view(**kwargs: t.Any) -> ft.ResponseReturnValue: - self = view.view_class( # type: ignore[attr-defined] - *class_args, **class_kwargs - ) - return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return] - - else: - self = cls(*class_args, **class_kwargs) # pyright: ignore - - def view(**kwargs: t.Any) -> ft.ResponseReturnValue: - return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return] - - if cls.decorators: - view.__name__ = name - view.__module__ = cls.__module__ - for decorator in cls.decorators: - view = decorator(view) - - # We attach the view class to the view function for two reasons: - # first of all it allows us to easily figure out what class-based - # view this thing came from, secondly it's also used for instantiating - # the view class so you can actually replace it with something else - # for testing purposes and debugging. - view.view_class = cls # type: ignore - view.__name__ = name - view.__doc__ = cls.__doc__ - view.__module__ = cls.__module__ - view.methods = cls.methods # type: ignore - view.provide_automatic_options = cls.provide_automatic_options # type: ignore - return view - - -class MethodView(View): - """Dispatches request methods to the corresponding instance methods. - For example, if you implement a ``get`` method, it will be used to - handle ``GET`` requests. - - This can be useful for defining a REST API. - - :attr:`methods` is automatically set based on the methods defined on - the class. - - See :doc:`views` for a detailed guide. - - .. code-block:: python - - class CounterAPI(MethodView): - def get(self): - return str(session.get("counter", 0)) - - def post(self): - session["counter"] = session.get("counter", 0) + 1 - return redirect(url_for("counter")) - - app.add_url_rule( - "/counter", view_func=CounterAPI.as_view("counter") - ) - """ - - def __init_subclass__(cls, **kwargs: t.Any) -> None: - super().__init_subclass__(**kwargs) - - if "methods" not in cls.__dict__: - methods = set() - - for base in cls.__bases__: - if getattr(base, "methods", None): - methods.update(base.methods) # type: ignore[attr-defined] - - for key in http_method_funcs: - if hasattr(cls, key): - methods.add(key.upper()) - - if methods: - cls.methods = methods - - def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue: - meth = getattr(self, request.method.lower(), None) - - # If the request method is HEAD and we don't have a handler for it - # retry with GET. - if meth is None and request.method == "HEAD": - meth = getattr(self, "get", None) - - assert meth is not None, f"Unimplemented method {request.method!r}" - return current_app.ensure_sync(meth)(**kwargs) # type: ignore[no-any-return] diff --git a/src/flask/wrappers.py b/src/flask/wrappers.py deleted file mode 100644 index bab61029..00000000 --- a/src/flask/wrappers.py +++ /dev/null @@ -1,257 +0,0 @@ -from __future__ import annotations - -import typing as t - -from werkzeug.exceptions import BadRequest -from werkzeug.exceptions import HTTPException -from werkzeug.wrappers import Request as RequestBase -from werkzeug.wrappers import Response as ResponseBase - -from . import json -from .globals import current_app -from .helpers import _split_blueprint_path - -if t.TYPE_CHECKING: # pragma: no cover - from werkzeug.routing import Rule - - -class Request(RequestBase): - """The request object used by default in Flask. Remembers the - matched endpoint and view arguments. - - It is what ends up as :class:`~flask.request`. If you want to replace - the request object used you can subclass this and set - :attr:`~flask.Flask.request_class` to your subclass. - - The request object is a :class:`~werkzeug.wrappers.Request` subclass and - provides all of the attributes Werkzeug defines plus a few Flask - specific ones. - """ - - json_module: t.Any = json - - #: The internal URL rule that matched the request. This can be - #: useful to inspect which methods are allowed for the URL from - #: a before/after handler (``request.url_rule.methods``) etc. - #: Though if the request's method was invalid for the URL rule, - #: the valid list is available in ``routing_exception.valid_methods`` - #: instead (an attribute of the Werkzeug exception - #: :exc:`~werkzeug.exceptions.MethodNotAllowed`) - #: because the request was never internally bound. - #: - #: .. versionadded:: 0.6 - url_rule: Rule | None = None - - #: A dict of view arguments that matched the request. If an exception - #: happened when matching, this will be ``None``. - view_args: dict[str, t.Any] | None = None - - #: If matching the URL failed, this is the exception that will be - #: raised / was raised as part of the request handling. This is - #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or - #: something similar. - routing_exception: HTTPException | None = None - - _max_content_length: int | None = None - _max_form_memory_size: int | None = None - _max_form_parts: int | None = None - - @property - def max_content_length(self) -> int | None: - """The maximum number of bytes that will be read during this request. If - this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge` - error is raised. If it is set to ``None``, no limit is enforced at the - Flask application level. However, if it is ``None`` and the request has - no ``Content-Length`` header and the WSGI server does not indicate that - it terminates the stream, then no data is read to avoid an infinite - stream. - - Each request defaults to the :data:`MAX_CONTENT_LENGTH` config, which - defaults to ``None``. It can be set on a specific ``request`` to apply - the limit to that specific view. This should be set appropriately based - on an application's or view's specific needs. - - .. versionchanged:: 3.1 - This can be set per-request. - - .. versionchanged:: 0.6 - This is configurable through Flask config. - """ - if self._max_content_length is not None: - return self._max_content_length - - if not current_app: - return super().max_content_length - - return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return] - - @max_content_length.setter - def max_content_length(self, value: int | None) -> None: - self._max_content_length = value - - @property - def max_form_memory_size(self) -> int | None: - """The maximum size in bytes any non-file form field may be in a - ``multipart/form-data`` body. If this limit is exceeded, a 413 - :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it - is set to ``None``, no limit is enforced at the Flask application level. - - Each request defaults to the :data:`MAX_FORM_MEMORY_SIZE` config, which - defaults to ``500_000``. It can be set on a specific ``request`` to - apply the limit to that specific view. This should be set appropriately - based on an application's or view's specific needs. - - .. versionchanged:: 3.1 - This is configurable through Flask config. - """ - if self._max_form_memory_size is not None: - return self._max_form_memory_size - - if not current_app: - return super().max_form_memory_size - - return current_app.config["MAX_FORM_MEMORY_SIZE"] # type: ignore[no-any-return] - - @max_form_memory_size.setter - def max_form_memory_size(self, value: int | None) -> None: - self._max_form_memory_size = value - - @property # type: ignore[override] - def max_form_parts(self) -> int | None: - """The maximum number of fields that may be present in a - ``multipart/form-data`` body. If this limit is exceeded, a 413 - :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it - is set to ``None``, no limit is enforced at the Flask application level. - - Each request defaults to the :data:`MAX_FORM_PARTS` config, which - defaults to ``1_000``. It can be set on a specific ``request`` to apply - the limit to that specific view. This should be set appropriately based - on an application's or view's specific needs. - - .. versionchanged:: 3.1 - This is configurable through Flask config. - """ - if self._max_form_parts is not None: - return self._max_form_parts - - if not current_app: - return super().max_form_parts - - return current_app.config["MAX_FORM_PARTS"] # type: ignore[no-any-return] - - @max_form_parts.setter - def max_form_parts(self, value: int | None) -> None: - self._max_form_parts = value - - @property - def endpoint(self) -> str | None: - """The endpoint that matched the request URL. - - This will be ``None`` if matching failed or has not been - performed yet. - - This in combination with :attr:`view_args` can be used to - reconstruct the same URL or a modified URL. - """ - if self.url_rule is not None: - return self.url_rule.endpoint # type: ignore[no-any-return] - - return None - - @property - def blueprint(self) -> str | None: - """The registered name of the current blueprint. - - This will be ``None`` if the endpoint is not part of a - blueprint, or if URL matching failed or has not been performed - yet. - - This does not necessarily match the name the blueprint was - created with. It may have been nested, or registered with a - different name. - """ - endpoint = self.endpoint - - if endpoint is not None and "." in endpoint: - return endpoint.rpartition(".")[0] - - return None - - @property - def blueprints(self) -> list[str]: - """The registered names of the current blueprint upwards through - parent blueprints. - - This will be an empty list if there is no current blueprint, or - if URL matching failed. - - .. versionadded:: 2.0.1 - """ - name = self.blueprint - - if name is None: - return [] - - return _split_blueprint_path(name) - - def _load_form_data(self) -> None: - super()._load_form_data() - - # In debug mode we're replacing the files multidict with an ad-hoc - # subclass that raises a different error for key errors. - if ( - current_app - and current_app.debug - and self.mimetype != "multipart/form-data" - and not self.files - ): - from .debughelpers import attach_enctype_error_multidict - - attach_enctype_error_multidict(self) - - def on_json_loading_failed(self, e: ValueError | None) -> t.Any: - try: - return super().on_json_loading_failed(e) - except BadRequest as ebr: - if current_app and current_app.debug: - raise - - raise BadRequest() from ebr - - -class Response(ResponseBase): - """The response object that is used by default in Flask. Works like the - response object from Werkzeug but is set to have an HTML mimetype by - default. Quite often you don't have to create this object yourself because - :meth:`~flask.Flask.make_response` will take care of that for you. - - If you want to replace the response object used you can subclass this and - set :attr:`~flask.Flask.response_class` to your subclass. - - .. versionchanged:: 1.0 - JSON support is added to the response, like the request. This is useful - when testing to get the test client response data as JSON. - - .. versionchanged:: 1.0 - - Added :attr:`max_cookie_size`. - """ - - default_mimetype: str | None = "text/html" - - json_module = json - - autocorrect_location_header = False - - @property - def max_cookie_size(self) -> int: # type: ignore - """Read-only view of the :data:`MAX_COOKIE_SIZE` config key. - - See :attr:`~werkzeug.wrappers.Response.max_cookie_size` in - Werkzeug's docs. - """ - if current_app: - return current_app.config["MAX_COOKIE_SIZE"] # type: ignore[no-any-return] - - # return Werkzeug's default when not in an app context - return super().max_cookie_size diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 0414b9e2..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,129 +0,0 @@ -import os -import sys - -import pytest -from _pytest import monkeypatch - -from flask import Flask -from flask.globals import app_ctx as _app_ctx - - -@pytest.fixture(scope="session", autouse=True) -def _standard_os_environ(): - """Set up ``os.environ`` at the start of the test session to have - standard values. Returns a list of operations that is used by - :func:`._reset_os_environ` after each test. - """ - mp = monkeypatch.MonkeyPatch() - out = ( - (os.environ, "FLASK_ENV_FILE", monkeypatch.notset), - (os.environ, "FLASK_APP", monkeypatch.notset), - (os.environ, "FLASK_DEBUG", monkeypatch.notset), - (os.environ, "FLASK_RUN_FROM_CLI", monkeypatch.notset), - (os.environ, "WERKZEUG_RUN_MAIN", monkeypatch.notset), - ) - - for _, key, value in out: - if value is monkeypatch.notset: - mp.delenv(key, False) - else: - mp.setenv(key, value) - - yield out - mp.undo() - - -@pytest.fixture(autouse=True) -def _reset_os_environ(monkeypatch, _standard_os_environ): - """Reset ``os.environ`` to the standard environ after each test, - in case a test changed something without cleaning up. - """ - monkeypatch._setitem.extend(_standard_os_environ) - - -@pytest.fixture -def app(): - app = Flask("flask_test", root_path=os.path.dirname(__file__)) - app.config.update( - TESTING=True, - SECRET_KEY="test key", - ) - return app - - -@pytest.fixture -def app_ctx(app): - with app.app_context() as ctx: - yield ctx - - -@pytest.fixture -def req_ctx(app): - with app.test_request_context() as ctx: - yield ctx - - -@pytest.fixture -def client(app): - return app.test_client() - - -@pytest.fixture -def test_apps(monkeypatch): - monkeypatch.syspath_prepend(os.path.join(os.path.dirname(__file__), "test_apps")) - original_modules = set(sys.modules.keys()) - - yield - - # Remove any imports cached during the test. Otherwise "import app" - # will work in the next test even though it's no longer on the path. - for key in sys.modules.keys() - original_modules: - sys.modules.pop(key) - - -@pytest.fixture(autouse=True) -def leak_detector(): - """Fails if any app contexts are still pushed when a test ends. Pops all - contexts so subsequent tests are not affected. - """ - yield - leaks = [] - - while _app_ctx: - leaks.append(_app_ctx._get_current_object()) - _app_ctx.pop() - - assert not leaks - - -@pytest.fixture -def modules_tmp_path(tmp_path, monkeypatch): - """A temporary directory added to sys.path.""" - rv = tmp_path / "modules_tmp" - rv.mkdir() - monkeypatch.syspath_prepend(os.fspath(rv)) - return rv - - -@pytest.fixture -def modules_tmp_path_prefix(modules_tmp_path, monkeypatch): - monkeypatch.setattr(sys, "prefix", os.fspath(modules_tmp_path)) - return modules_tmp_path - - -@pytest.fixture -def site_packages(modules_tmp_path, monkeypatch): - """Create a fake site-packages.""" - py_dir = f"python{sys.version_info.major}.{sys.version_info.minor}" - rv = modules_tmp_path / "lib" / py_dir / "site-packages" - rv.mkdir(parents=True) - monkeypatch.syspath_prepend(os.fspath(rv)) - return rv - - -@pytest.fixture -def purge_module(request): - def inner(name): - request.addfinalizer(lambda: sys.modules.pop(name, None)) - - return inner diff --git a/tests/static/config.json b/tests/static/config.json deleted file mode 100644 index 4eedab12..00000000 --- a/tests/static/config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "TEST_KEY": "foo", - "SECRET_KEY": "config" -} diff --git a/tests/static/config.toml b/tests/static/config.toml deleted file mode 100644 index 64acdbdd..00000000 --- a/tests/static/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -TEST_KEY="foo" -SECRET_KEY="config" diff --git a/tests/templates/non_escaping_template.txt b/tests/templates/non_escaping_template.txt deleted file mode 100644 index 542864e8..00000000 --- a/tests/templates/non_escaping_template.txt +++ /dev/null @@ -1,8 +0,0 @@ -{{ text }} -{{ html }} -{% autoescape false %}{{ text }} -{{ html }}{% endautoescape %} -{% autoescape true %}{{ text }} -{{ html }}{% endautoescape %} -{{ text }} -{{ html }} diff --git a/tests/templates/template_filter.html b/tests/templates/template_filter.html deleted file mode 100644 index d51506a3..00000000 --- a/tests/templates/template_filter.html +++ /dev/null @@ -1 +0,0 @@ -{{ value|super_reverse }} diff --git a/tests/templates/template_test.html b/tests/templates/template_test.html deleted file mode 100644 index 92d5561b..00000000 --- a/tests/templates/template_test.html +++ /dev/null @@ -1,3 +0,0 @@ -{% if value is boolean %} - Success! -{% endif %} diff --git a/tests/test_appctx.py b/tests/test_appctx.py deleted file mode 100644 index 2d4eecc5..00000000 --- a/tests/test_appctx.py +++ /dev/null @@ -1,265 +0,0 @@ -import sys - -import pytest - -import flask -from flask.globals import app_ctx -from flask.testing import FlaskClient - - -def test_basic_url_generation(app): - app.config["SERVER_NAME"] = "localhost" - app.config["PREFERRED_URL_SCHEME"] = "https" - - @app.route("/") - def index(): - pass - - with app.app_context(): - rv = flask.url_for("index") - assert rv == "https://localhost/" - - -def test_url_generation_requires_server_name(app): - with app.app_context(): - with pytest.raises(RuntimeError): - flask.url_for("index") - - -def test_url_generation_without_context_fails(): - with pytest.raises(RuntimeError): - flask.url_for("index") - - -def test_request_context_means_app_context(app): - with app.test_request_context(): - assert flask.current_app._get_current_object() is app - assert not flask.current_app - - -def test_app_context_provides_current_app(app): - with app.app_context(): - assert flask.current_app._get_current_object() is app - assert not flask.current_app - - -def test_app_tearing_down(app): - cleanup_stuff = [] - - @app.teardown_appcontext - def cleanup(exception): - cleanup_stuff.append(exception) - - with app.app_context(): - pass - - assert cleanup_stuff == [None] - - -def test_app_tearing_down_with_previous_exception(app): - cleanup_stuff = [] - - @app.teardown_appcontext - def cleanup(exception): - cleanup_stuff.append(exception) - - try: - raise Exception("dummy") - except Exception: - pass - - with app.app_context(): - pass - - assert cleanup_stuff == [None] - - -def test_app_tearing_down_with_handled_exception_by_except_block(app): - cleanup_stuff = [] - - @app.teardown_appcontext - def cleanup(exception): - cleanup_stuff.append(exception) - - with app.app_context(): - try: - raise Exception("dummy") - except Exception: - pass - - assert cleanup_stuff == [None] - - -def test_app_tearing_down_with_handled_exception_by_app_handler(app, client): - app.config["PROPAGATE_EXCEPTIONS"] = True - cleanup_stuff = [] - - @app.teardown_appcontext - def cleanup(exception): - cleanup_stuff.append(exception) - - @app.route("/") - def index(): - raise Exception("dummy") - - @app.errorhandler(Exception) - def handler(f): - return flask.jsonify(str(f)) - - with app.app_context(): - client.get("/") - - # teardown request context, and with block context - assert cleanup_stuff == [None, None] - - -def test_app_tearing_down_with_unhandled_exception(app, client): - app.config["PROPAGATE_EXCEPTIONS"] = True - cleanup_stuff = [] - - @app.teardown_appcontext - def cleanup(exception): - cleanup_stuff.append(exception) - - @app.route("/") - def index(): - raise ValueError("dummy") - - with pytest.raises(ValueError, match="dummy"): - with app.app_context(): - client.get("/") - - assert len(cleanup_stuff) == 2 - assert isinstance(cleanup_stuff[0], ValueError) - assert str(cleanup_stuff[0]) == "dummy" - # exception propagated, seen by request context and with block context - assert cleanup_stuff[0] is cleanup_stuff[1] - - -def test_app_ctx_globals_methods(app, app_ctx): - # get - assert flask.g.get("foo") is None - assert flask.g.get("foo", "bar") == "bar" - # __contains__ - assert "foo" not in flask.g - flask.g.foo = "bar" - assert "foo" in flask.g - # setdefault - flask.g.setdefault("bar", "the cake is a lie") - flask.g.setdefault("bar", "hello world") - assert flask.g.bar == "the cake is a lie" - # pop - assert flask.g.pop("bar") == "the cake is a lie" - with pytest.raises(KeyError): - flask.g.pop("bar") - assert flask.g.pop("bar", "more cake") == "more cake" - # __iter__ - assert list(flask.g) == ["foo"] - # __repr__ - assert repr(flask.g) == "" - - -def test_custom_app_ctx_globals_class(app): - class CustomRequestGlobals: - def __init__(self): - self.spam = "eggs" - - app.app_ctx_globals_class = CustomRequestGlobals - with app.app_context(): - assert flask.render_template_string("{{ g.spam }}") == "eggs" - - -def test_context_refcounts(app, client): - called = [] - - @app.teardown_request - def teardown_req(error=None): - called.append("request") - - @app.teardown_appcontext - def teardown_app(error=None): - called.append("app") - - @app.route("/") - def index(): - with app_ctx: - pass - - assert flask.request.environ["werkzeug.request"] is not None - return "" - - res = client.get("/") - assert res.status_code == 200 - assert res.data == b"" - assert called == ["request", "app"] - - -def test_clean_pop(app): - app.testing = False - called = [] - - @app.teardown_request - def teardown_req(error=None): - raise ZeroDivisionError - - @app.teardown_appcontext - def teardown_app(error=None): - called.append("TEARDOWN") - - with app.app_context(): - called.append(flask.current_app.name) - - assert called == ["flask_test", "TEARDOWN"] - assert not flask.current_app - - -def test_robust_teardown(app: flask.Flask, client: FlaskClient) -> None: - count = 0 - - @app.teardown_request - def request_teardown(e: Exception | None) -> None: - nonlocal count - count += 1 - raise ValueError("request_teardown") - - @app.teardown_appcontext - def app_teardown(e: Exception | None) -> None: - nonlocal count - count += 1 - raise ValueError("app_teardown") - - @app.get("/") - def index() -> str: - return "" - - def request_signal(sender: flask.Flask, exc: Exception | None) -> None: - nonlocal count - count += 1 - raise ValueError("request_signal") - - def app_signal(sender: flask.Flask, exc: Exception | None) -> None: - nonlocal count - count += 1 - raise ValueError("app_signal") - - with ( - flask.request_tearing_down.connected_to(request_signal, app), - flask.appcontext_tearing_down.connected_to(app_signal, app), - ): - if sys.version_info >= (3, 11): - with pytest.raises(ExceptionGroup, match="context teardown") as exc_info: # noqa: F821 - client.get() - - assert len(exc_info.value.exceptions) == 2 - eg1, eg2 = exc_info.value.exceptions - assert isinstance(eg1, ExceptionGroup) # noqa: F821 - assert "request teardown" in eg1.message - assert len(eg1.exceptions) == 2 - assert isinstance(eg2, ExceptionGroup) # noqa: F821 - assert "app teardown" in eg2.message - assert len(eg2.exceptions) == 2 - else: - with pytest.raises(ValueError, match="request_teardown"): - client.get() - - assert count == 4 diff --git a/tests/test_apps/.env b/tests/test_apps/.env deleted file mode 100644 index 0890b615..00000000 --- a/tests/test_apps/.env +++ /dev/null @@ -1,4 +0,0 @@ -FOO=env -SPAM=1 -EGGS=2 -HAM=火腿 diff --git a/tests/test_apps/.flaskenv b/tests/test_apps/.flaskenv deleted file mode 100644 index 59f96af7..00000000 --- a/tests/test_apps/.flaskenv +++ /dev/null @@ -1,3 +0,0 @@ -FOO=flaskenv -BAR=bar -EGGS=0 diff --git a/tests/test_apps/blueprintapp/__init__.py b/tests/test_apps/blueprintapp/__init__.py deleted file mode 100644 index ad594cf1..00000000 --- a/tests/test_apps/blueprintapp/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask import Flask - -app = Flask(__name__) -app.config["DEBUG"] = True -from blueprintapp.apps.admin import admin # noqa: E402 -from blueprintapp.apps.frontend import frontend # noqa: E402 - -app.register_blueprint(admin) -app.register_blueprint(frontend) diff --git a/tests/test_apps/blueprintapp/apps/admin/__init__.py b/tests/test_apps/blueprintapp/apps/admin/__init__.py deleted file mode 100644 index b197fad0..00000000 --- a/tests/test_apps/blueprintapp/apps/admin/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from flask import Blueprint -from flask import render_template - -admin = Blueprint( - "admin", - __name__, - url_prefix="/admin", - template_folder="templates", - static_folder="static", -) - - -@admin.route("/") -def index(): - return render_template("admin/index.html") - - -@admin.route("/index2") -def index2(): - return render_template("./admin/index.html") diff --git a/tests/test_apps/blueprintapp/apps/frontend/__init__.py b/tests/test_apps/blueprintapp/apps/frontend/__init__.py deleted file mode 100644 index 7cc5cd82..00000000 --- a/tests/test_apps/blueprintapp/apps/frontend/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from flask import Blueprint -from flask import render_template - -frontend = Blueprint("frontend", __name__, template_folder="templates") - - -@frontend.route("/") -def index(): - return render_template("frontend/index.html") - - -@frontend.route("/missing") -def missing_template(): - return render_template("missing_template.html") diff --git a/tests/test_apps/cliapp/app.py b/tests/test_apps/cliapp/app.py deleted file mode 100644 index 017ce280..00000000 --- a/tests/test_apps/cliapp/app.py +++ /dev/null @@ -1,3 +0,0 @@ -from flask import Flask - -testapp = Flask("testapp") diff --git a/tests/test_apps/cliapp/factory.py b/tests/test_apps/cliapp/factory.py deleted file mode 100644 index 1d27396d..00000000 --- a/tests/test_apps/cliapp/factory.py +++ /dev/null @@ -1,13 +0,0 @@ -from flask import Flask - - -def create_app(): - return Flask("app") - - -def create_app2(foo, bar): - return Flask("_".join(["app2", foo, bar])) - - -def no_app(): - pass diff --git a/tests/test_apps/cliapp/importerrorapp.py b/tests/test_apps/cliapp/importerrorapp.py deleted file mode 100644 index 2c96c9b4..00000000 --- a/tests/test_apps/cliapp/importerrorapp.py +++ /dev/null @@ -1,5 +0,0 @@ -from flask import Flask - -raise ImportError() - -testapp = Flask("testapp") diff --git a/tests/test_apps/cliapp/inner1/__init__.py b/tests/test_apps/cliapp/inner1/__init__.py deleted file mode 100644 index 8330f6e0..00000000 --- a/tests/test_apps/cliapp/inner1/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from flask import Flask - -application = Flask(__name__) diff --git a/tests/test_apps/cliapp/inner1/inner2/flask.py b/tests/test_apps/cliapp/inner1/inner2/flask.py deleted file mode 100644 index d7562aac..00000000 --- a/tests/test_apps/cliapp/inner1/inner2/flask.py +++ /dev/null @@ -1,3 +0,0 @@ -from flask import Flask - -app = Flask(__name__) diff --git a/tests/test_apps/cliapp/message.txt b/tests/test_apps/cliapp/message.txt deleted file mode 100644 index fc2b2cf0..00000000 --- a/tests/test_apps/cliapp/message.txt +++ /dev/null @@ -1 +0,0 @@ -So long, and thanks for all the fish. diff --git a/tests/test_apps/cliapp/multiapp.py b/tests/test_apps/cliapp/multiapp.py deleted file mode 100644 index 4ed0f328..00000000 --- a/tests/test_apps/cliapp/multiapp.py +++ /dev/null @@ -1,4 +0,0 @@ -from flask import Flask - -app1 = Flask("app1") -app2 = Flask("app2") diff --git a/tests/test_apps/helloworld/hello.py b/tests/test_apps/helloworld/hello.py deleted file mode 100644 index 71a2f90c..00000000 --- a/tests/test_apps/helloworld/hello.py +++ /dev/null @@ -1,8 +0,0 @@ -from flask import Flask - -app = Flask(__name__) - - -@app.route("/") -def hello(): - return "Hello World!" diff --git a/tests/test_apps/helloworld/wsgi.py b/tests/test_apps/helloworld/wsgi.py deleted file mode 100644 index ab2d6e9f..00000000 --- a/tests/test_apps/helloworld/wsgi.py +++ /dev/null @@ -1 +0,0 @@ -from hello import app # noqa: F401 diff --git a/tests/test_apps/subdomaintestmodule/__init__.py b/tests/test_apps/subdomaintestmodule/__init__.py deleted file mode 100644 index b4ce4b16..00000000 --- a/tests/test_apps/subdomaintestmodule/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from flask import Module - -mod = Module(__name__, "foo", subdomain="foo") diff --git a/tests/test_async.py b/tests/test_async.py deleted file mode 100644 index f52b0492..00000000 --- a/tests/test_async.py +++ /dev/null @@ -1,145 +0,0 @@ -import asyncio - -import pytest - -from flask import Blueprint -from flask import Flask -from flask import request -from flask.views import MethodView -from flask.views import View - -pytest.importorskip("asgiref") - - -class AppError(Exception): - pass - - -class BlueprintError(Exception): - pass - - -class AsyncView(View): - methods = ["GET", "POST"] - - async def dispatch_request(self): - await asyncio.sleep(0) - return request.method - - -class AsyncMethodView(MethodView): - async def get(self): - await asyncio.sleep(0) - return "GET" - - async def post(self): - await asyncio.sleep(0) - return "POST" - - -@pytest.fixture(name="async_app") -def _async_app(): - app = Flask(__name__) - - @app.route("/", methods=["GET", "POST"]) - @app.route("/home", methods=["GET", "POST"]) - async def index(): - await asyncio.sleep(0) - return request.method - - @app.errorhandler(AppError) - async def handle(_): - return "", 412 - - @app.route("/error") - async def error(): - raise AppError() - - blueprint = Blueprint("bp", __name__) - - @blueprint.route("/", methods=["GET", "POST"]) - async def bp_index(): - await asyncio.sleep(0) - return request.method - - @blueprint.errorhandler(BlueprintError) - async def bp_handle(_): - return "", 412 - - @blueprint.route("/error") - async def bp_error(): - raise BlueprintError() - - app.register_blueprint(blueprint, url_prefix="/bp") - - app.add_url_rule("/view", view_func=AsyncView.as_view("view")) - app.add_url_rule("/methodview", view_func=AsyncMethodView.as_view("methodview")) - - return app - - -@pytest.mark.parametrize("path", ["/", "/home", "/bp/", "/view", "/methodview"]) -def test_async_route(path, async_app): - test_client = async_app.test_client() - response = test_client.get(path) - assert b"GET" in response.get_data() - response = test_client.post(path) - assert b"POST" in response.get_data() - - -@pytest.mark.parametrize("path", ["/error", "/bp/error"]) -def test_async_error_handler(path, async_app): - test_client = async_app.test_client() - response = test_client.get(path) - assert response.status_code == 412 - - -def test_async_before_after_request(): - app_before_called = False - app_after_called = False - bp_before_called = False - bp_after_called = False - - app = Flask(__name__) - - @app.route("/") - def index(): - return "" - - @app.before_request - async def before(): - nonlocal app_before_called - app_before_called = True - - @app.after_request - async def after(response): - nonlocal app_after_called - app_after_called = True - return response - - blueprint = Blueprint("bp", __name__) - - @blueprint.route("/") - def bp_index(): - return "" - - @blueprint.before_request - async def bp_before(): - nonlocal bp_before_called - bp_before_called = True - - @blueprint.after_request - async def bp_after(response): - nonlocal bp_after_called - bp_after_called = True - return response - - app.register_blueprint(blueprint, url_prefix="/bp") - - test_client = app.test_client() - test_client.get("/") - assert app_before_called - assert app_after_called - test_client.get("/bp/") - assert bp_before_called - assert bp_after_called diff --git a/tests/test_basic.py b/tests/test_basic.py deleted file mode 100644 index 1d9d83f8..00000000 --- a/tests/test_basic.py +++ /dev/null @@ -1,1970 +0,0 @@ -import gc -import importlib.metadata -import re -import typing as t -import uuid -import weakref -from contextlib import nullcontext -from datetime import datetime -from datetime import timezone -from platform import python_implementation - -import pytest -import werkzeug.serving -from markupsafe import Markup -from werkzeug.exceptions import BadRequest -from werkzeug.exceptions import Forbidden -from werkzeug.exceptions import NotFound -from werkzeug.http import parse_date -from werkzeug.routing import BuildError -from werkzeug.routing import RequestRedirect - -import flask -from flask.globals import app_ctx -from flask.testing import FlaskClient - -require_cpython_gc = pytest.mark.skipif( - python_implementation() != "CPython", - reason="Requires CPython GC behavior", -) - - -def test_options_work(app, client): - @app.route("/", methods=["GET", "POST"]) - def index(): - return "Hello World" - - rv = client.open("/", method="OPTIONS") - assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST"] - assert rv.data == b"" - - -def test_options_on_multiple_rules(app, client): - @app.route("/", methods=["GET", "POST"]) - def index(): - return "Hello World" - - @app.route("/", methods=["PUT"]) - def index_put(): - return "Aha!" - - rv = client.open("/", method="OPTIONS") - assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST", "PUT"] - - -@pytest.mark.parametrize("method", ["get", "post", "put", "delete", "patch"]) -def test_method_route(app, client, method): - method_route = getattr(app, method) - client_method = getattr(client, method) - - @method_route("/") - def hello(): - return "Hello" - - assert client_method("/").data == b"Hello" - - -def test_method_route_no_methods(app): - with pytest.raises(TypeError): - app.get("/", methods=["GET", "POST"]) - - -def test_provide_automatic_options_attr_disable( - app: flask.Flask, client: FlaskClient -) -> None: - """Automatic options can be disabled by the view func attribute.""" - - def index(): - return "Hello World!" - - index.provide_automatic_options = False - app.add_url_rule("/", view_func=index) - rv = client.options() - assert rv.status_code == 405 - - -def test_provide_automatic_options_attr_enable( - app: flask.Flask, client: FlaskClient -) -> None: - """When default automatic options is disabled in config, it can still be - enabled by the view function attribute. - """ - app.config["PROVIDE_AUTOMATIC_OPTIONS"] = False - - def index(): - return "Hello World!" - - index.provide_automatic_options = True - app.add_url_rule("/", view_func=index) - rv = client.options() - assert rv.allow == {"GET", "HEAD", "OPTIONS"} - - -def test_provide_automatic_options_arg_disable( - app: flask.Flask, client: FlaskClient -) -> None: - """Automatic options can be disabled by the route argument.""" - - @app.get("/", provide_automatic_options=False) - def index(): - return "Hello World!" - - rv = client.options() - assert rv.status_code == 405 - - -def test_provide_automatic_options_method_disable( - app: flask.Flask, client: FlaskClient -) -> None: - """Automatic options is ignored if the route handles options.""" - - @app.route("/", methods=["OPTIONS"]) - def index(): - return "", {"X-Test": "test"} - - rv = client.options() - assert rv.headers["X-Test"] == "test" - - -def test_request_dispatching(app, client): - @app.route("/") - def index(): - return flask.request.method - - @app.route("/more", methods=["GET", "POST"]) - def more(): - return flask.request.method - - assert client.get("/").data == b"GET" - rv = client.post("/") - assert rv.status_code == 405 - assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS"] - rv = client.head("/") - assert rv.status_code == 200 - assert not rv.data # head truncates - assert client.post("/more").data == b"POST" - assert client.get("/more").data == b"GET" - rv = client.delete("/more") - assert rv.status_code == 405 - assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST"] - - -def test_disallow_string_for_allowed_methods(app): - with pytest.raises(TypeError): - app.add_url_rule("/", methods="GET POST", endpoint="test") - - -def test_url_mapping(app, client): - random_uuid4 = "7eb41166-9ebf-4d26-b771-ea3f54f8b383" - - def index(): - return flask.request.method - - def more(): - return flask.request.method - - def options(): - return random_uuid4 - - app.add_url_rule("/", "index", index) - app.add_url_rule("/more", "more", more, methods=["GET", "POST"]) - - # Issue 1288: Test that automatic options are not added - # when non-uppercase 'options' in methods - app.add_url_rule("/options", "options", options, methods=["options"]) - - assert client.get("/").data == b"GET" - rv = client.post("/") - assert rv.status_code == 405 - assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS"] - rv = client.head("/") - assert rv.status_code == 200 - assert not rv.data # head truncates - assert client.post("/more").data == b"POST" - assert client.get("/more").data == b"GET" - rv = client.delete("/more") - assert rv.status_code == 405 - assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST"] - rv = client.open("/options", method="OPTIONS") - assert rv.status_code == 200 - assert random_uuid4 in rv.data.decode("utf-8") - - -def test_werkzeug_routing(app, client): - from werkzeug.routing import Rule - from werkzeug.routing import Submount - - app.url_map.add( - Submount("/foo", [Rule("/bar", endpoint="bar"), Rule("/", endpoint="index")]) - ) - - def bar(): - return "bar" - - def index(): - return "index" - - app.view_functions["bar"] = bar - app.view_functions["index"] = index - - assert client.get("/foo/").data == b"index" - assert client.get("/foo/bar").data == b"bar" - - -def test_endpoint_decorator(app, client): - from werkzeug.routing import Rule - from werkzeug.routing import Submount - - app.url_map.add( - Submount("/foo", [Rule("/bar", endpoint="bar"), Rule("/", endpoint="index")]) - ) - - @app.endpoint("bar") - def bar(): - return "bar" - - @app.endpoint("index") - def index(): - return "index" - - assert client.get("/foo/").data == b"index" - assert client.get("/foo/bar").data == b"bar" - - -def test_session_accessed(app: flask.Flask, client: FlaskClient) -> None: - @app.post("/") - def do_set(): - flask.session["value"] = flask.request.form["value"] - return "value set" - - @app.get("/") - def do_get(): - return flask.session.get("value", "None") - - @app.get("/nothing") - def do_nothing() -> str: - return "" - - with client: - rv = client.get("/nothing") - assert "cookie" not in rv.vary - assert not app_ctx._session.accessed - assert not app_ctx._session.modified - - with client: - rv = client.post(data={"value": "42"}) - assert rv.text == "value set" - assert "cookie" in rv.vary - assert app_ctx._session.accessed - assert app_ctx._session.modified - - with client: - rv = client.get() - assert rv.text == "42" - assert "cookie" in rv.vary - assert app_ctx._session.accessed - assert not app_ctx._session.modified - - with client: - rv = client.get("/nothing") - assert rv.text == "" - assert "cookie" not in rv.vary - assert not app_ctx._session.accessed - assert not app_ctx._session.modified - - -def test_session_path(app, client): - app.config.update(APPLICATION_ROOT="/foo") - - @app.route("/") - def index(): - flask.session["testing"] = 42 - return "Hello World" - - rv = client.get("/", "http://example.com:8080/foo") - assert "path=/foo" in rv.headers["set-cookie"].lower() - - -def test_session_using_application_root(app, client): - class PrefixPathMiddleware: - def __init__(self, app, prefix): - self.app = app - self.prefix = prefix - - def __call__(self, environ, start_response): - environ["SCRIPT_NAME"] = self.prefix - return self.app(environ, start_response) - - app.wsgi_app = PrefixPathMiddleware(app.wsgi_app, "/bar") - app.config.update(APPLICATION_ROOT="/bar") - - @app.route("/") - def index(): - flask.session["testing"] = 42 - return "Hello World" - - rv = client.get("/", "http://example.com:8080/") - assert "path=/bar" in rv.headers["set-cookie"].lower() - - -def test_session_using_session_settings(app, client): - app.config.update( - SERVER_NAME="www.example.com:8080", - APPLICATION_ROOT="/test", - SESSION_COOKIE_DOMAIN=".example.com", - SESSION_COOKIE_HTTPONLY=False, - SESSION_COOKIE_SECURE=True, - SESSION_COOKIE_PARTITIONED=True, - SESSION_COOKIE_SAMESITE="Lax", - SESSION_COOKIE_PATH="/", - ) - - @app.route("/") - def index(): - flask.session["testing"] = 42 - return "Hello World" - - @app.route("/clear") - def clear(): - flask.session.pop("testing", None) - return "Goodbye World" - - rv = client.get("/", "http://www.example.com:8080/test/") - cookie = rv.headers["set-cookie"].lower() - # or condition for Werkzeug < 2.3 - assert "domain=example.com" in cookie or "domain=.example.com" in cookie - assert "path=/" in cookie - assert "secure" in cookie - assert "httponly" not in cookie - assert "samesite" in cookie - assert "partitioned" in cookie - - rv = client.get("/clear", "http://www.example.com:8080/test/") - cookie = rv.headers["set-cookie"].lower() - assert "session=;" in cookie - # or condition for Werkzeug < 2.3 - assert "domain=example.com" in cookie or "domain=.example.com" in cookie - assert "path=/" in cookie - assert "secure" in cookie - assert "samesite" in cookie - assert "partitioned" in cookie - - -def test_session_using_samesite_attribute(app, client): - @app.route("/") - def index(): - flask.session["testing"] = 42 - return "Hello World" - - app.config.update(SESSION_COOKIE_SAMESITE="invalid") - - with pytest.raises(ValueError): - client.get("/") - - app.config.update(SESSION_COOKIE_SAMESITE=None) - rv = client.get("/") - cookie = rv.headers["set-cookie"].lower() - assert "samesite" not in cookie - - app.config.update(SESSION_COOKIE_SAMESITE="Strict") - rv = client.get("/") - cookie = rv.headers["set-cookie"].lower() - assert "samesite=strict" in cookie - - app.config.update(SESSION_COOKIE_SAMESITE="Lax") - rv = client.get("/") - cookie = rv.headers["set-cookie"].lower() - assert "samesite=lax" in cookie - - -def test_missing_session(app): - app.secret_key = None - - def expect_exception(f, *args, **kwargs): - e = pytest.raises(RuntimeError, f, *args, **kwargs) - assert e.value.args and "session is unavailable" in e.value.args[0] - - with app.test_request_context(): - assert flask.session.get("missing_key") is None - expect_exception(flask.session.__setitem__, "foo", 42) - expect_exception(flask.session.pop, "foo") - - -def test_session_secret_key_fallbacks(app, client) -> None: - @app.post("/") - def set_session() -> str: - flask.session["a"] = 1 - return "" - - @app.get("/") - def get_session() -> dict[str, t.Any]: - return dict(flask.session) - - # Set session with initial secret key, and two valid expiring keys - app.secret_key, app.config["SECRET_KEY_FALLBACKS"] = ( - "0 key", - ["-1 key", "-2 key"], - ) - client.post() - assert client.get().json == {"a": 1} - # Change secret key, session can't be loaded and appears empty - app.secret_key = "? key" - assert client.get().json == {} - # Rotate the valid keys, session can be loaded - app.secret_key, app.config["SECRET_KEY_FALLBACKS"] = ( - "+1 key", - ["0 key", "-1 key"], - ) - assert client.get().json == {"a": 1} - - -def test_session_expiration(app, client): - permanent = True - - @app.route("/") - def index(): - flask.session["test"] = 42 - flask.session.permanent = permanent - return "" - - @app.route("/test") - def test(): - return str(flask.session.permanent) - - rv = client.get("/") - assert "set-cookie" in rv.headers - match = re.search(r"(?i)\bexpires=([^;]+)", rv.headers["set-cookie"]) - expires = parse_date(match.group()) - expected = datetime.now(timezone.utc) + app.permanent_session_lifetime - assert expires.year == expected.year - assert expires.month == expected.month - assert expires.day == expected.day - - rv = client.get("/test") - assert rv.data == b"True" - - permanent = False - rv = client.get("/") - assert "set-cookie" in rv.headers - match = re.search(r"\bexpires=([^;]+)", rv.headers["set-cookie"]) - assert match is None - - -def test_session_stored_last(app, client): - @app.after_request - def modify_session(response): - flask.session["foo"] = 42 - return response - - @app.route("/") - def dump_session_contents(): - return repr(flask.session.get("foo")) - - assert client.get("/").data == b"None" - assert client.get("/").data == b"42" - - -def test_session_special_types(app, client): - now = datetime.now(timezone.utc).replace(microsecond=0) - the_uuid = uuid.uuid4() - - @app.route("/") - def dump_session_contents(): - flask.session["t"] = (1, 2, 3) - flask.session["b"] = b"\xff" - flask.session["m"] = Markup("") - flask.session["u"] = the_uuid - flask.session["d"] = now - flask.session["t_tag"] = {" t": "not-a-tuple"} - flask.session["di_t_tag"] = {" t__": "not-a-tuple"} - flask.session["di_tag"] = {" di": "not-a-dict"} - return "", 204 - - with client: - client.get("/") - s = flask.session - assert s["t"] == (1, 2, 3) - assert type(s["b"]) is bytes # noqa: E721 - assert s["b"] == b"\xff" - assert type(s["m"]) is Markup # noqa: E721 - assert s["m"] == Markup("") - assert s["u"] == the_uuid - assert s["d"] == now - assert s["t_tag"] == {" t": "not-a-tuple"} - assert s["di_t_tag"] == {" t__": "not-a-tuple"} - assert s["di_tag"] == {" di": "not-a-dict"} - - -def test_session_cookie_setting(app): - is_permanent = True - - @app.route("/bump") - def bump(): - rv = flask.session["foo"] = flask.session.get("foo", 0) + 1 - flask.session.permanent = is_permanent - return str(rv) - - @app.route("/read") - def read(): - return str(flask.session.get("foo", 0)) - - def run_test(expect_header): - with app.test_client() as c: - assert c.get("/bump").data == b"1" - assert c.get("/bump").data == b"2" - assert c.get("/bump").data == b"3" - - rv = c.get("/read") - set_cookie = rv.headers.get("set-cookie") - assert (set_cookie is not None) == expect_header - assert rv.data == b"3" - - is_permanent = True - app.config["SESSION_REFRESH_EACH_REQUEST"] = True - run_test(expect_header=True) - - is_permanent = True - app.config["SESSION_REFRESH_EACH_REQUEST"] = False - run_test(expect_header=False) - - is_permanent = False - app.config["SESSION_REFRESH_EACH_REQUEST"] = True - run_test(expect_header=False) - - is_permanent = False - app.config["SESSION_REFRESH_EACH_REQUEST"] = False - run_test(expect_header=False) - - -def test_session_vary_cookie(app, client): - @app.route("/set") - def set_session(): - flask.session["test"] = "test" - return "" - - @app.route("/get") - def get(): - return flask.session.get("test") - - @app.route("/getitem") - def getitem(): - return flask.session["test"] - - @app.route("/setdefault") - def setdefault(): - return flask.session.setdefault("test", "default") - - @app.route("/clear") - def clear(): - flask.session.clear() - return "" - - @app.route("/vary-cookie-header-set") - def vary_cookie_header_set(): - response = flask.Response() - response.vary.add("Cookie") - flask.session["test"] = "test" - return response - - @app.route("/vary-header-set") - def vary_header_set(): - response = flask.Response() - response.vary.update(("Accept-Encoding", "Accept-Language")) - flask.session["test"] = "test" - return response - - @app.route("/no-vary-header") - def no_vary_header(): - return "" - - def expect(path, header_value="Cookie"): - rv = client.get(path) - - if header_value: - # The 'Vary' key should exist in the headers only once. - assert len(rv.headers.get_all("Vary")) == 1 - assert rv.headers["Vary"] == header_value - else: - assert "Vary" not in rv.headers - - expect("/set") - expect("/get") - expect("/getitem") - expect("/setdefault") - expect("/clear") - expect("/vary-cookie-header-set") - expect("/vary-header-set", "Accept-Encoding, Accept-Language, Cookie") - expect("/no-vary-header", None) - - -def test_session_refresh_vary(app, client): - @app.get("/login") - def login(): - flask.session["user_id"] = 1 - flask.session.permanent = True - return "" - - @app.get("/ignored") - def ignored(): - return "" - - rv = client.get("/login") - assert rv.headers["Vary"] == "Cookie" - rv = client.get("/ignored") - assert rv.headers["Vary"] == "Cookie" - - -def test_flashes(app, req_ctx): - assert not flask.session.modified - flask.flash("Zap") - flask.session.modified = False - flask.flash("Zip") - assert flask.session.modified - assert list(flask.get_flashed_messages()) == ["Zap", "Zip"] - - -def test_extended_flashing(app): - # Be sure app.testing=True below, else tests can fail silently. - # - # Specifically, if app.testing is not set to True, the AssertionErrors - # in the view functions will cause a 500 response to the test client - # instead of propagating exceptions. - - @app.route("/") - def index(): - flask.flash("Hello World") - flask.flash("Hello World", "error") - flask.flash(Markup("Testing"), "warning") - return "" - - @app.route("/test/") - def test(): - messages = flask.get_flashed_messages() - assert list(messages) == [ - "Hello World", - "Hello World", - Markup("Testing"), - ] - return "" - - @app.route("/test_with_categories/") - def test_with_categories(): - messages = flask.get_flashed_messages(with_categories=True) - assert len(messages) == 3 - assert list(messages) == [ - ("message", "Hello World"), - ("error", "Hello World"), - ("warning", Markup("Testing")), - ] - return "" - - @app.route("/test_filter/") - def test_filter(): - messages = flask.get_flashed_messages( - category_filter=["message"], with_categories=True - ) - assert list(messages) == [("message", "Hello World")] - return "" - - @app.route("/test_filters/") - def test_filters(): - messages = flask.get_flashed_messages( - category_filter=["message", "warning"], with_categories=True - ) - assert list(messages) == [ - ("message", "Hello World"), - ("warning", Markup("Testing")), - ] - return "" - - @app.route("/test_filters_without_returning_categories/") - def test_filters2(): - messages = flask.get_flashed_messages(category_filter=["message", "warning"]) - assert len(messages) == 2 - assert messages[0] == "Hello World" - assert messages[1] == Markup("Testing") - return "" - - # Create new test client on each test to clean flashed messages. - - client = app.test_client() - client.get("/") - client.get("/test_with_categories/") - - client = app.test_client() - client.get("/") - client.get("/test_filter/") - - client = app.test_client() - client.get("/") - client.get("/test_filters/") - - client = app.test_client() - client.get("/") - client.get("/test_filters_without_returning_categories/") - - -def test_request_processing(app, client): - evts = [] - - @app.before_request - def before_request(): - evts.append("before") - - @app.after_request - def after_request(response): - response.data += b"|after" - evts.append("after") - return response - - @app.route("/") - def index(): - assert "before" in evts - assert "after" not in evts - return "request" - - assert "after" not in evts - rv = client.get("/").data - assert "after" in evts - assert rv == b"request|after" - - -def test_request_preprocessing_early_return(app, client): - evts = [] - - @app.before_request - def before_request1(): - evts.append(1) - - @app.before_request - def before_request2(): - evts.append(2) - return "hello" - - @app.before_request - def before_request3(): - evts.append(3) - return "bye" - - @app.route("/") - def index(): - evts.append("index") - return "damnit" - - rv = client.get("/").data.strip() - assert rv == b"hello" - assert evts == [1, 2] - - -def test_after_request_processing(app, client): - @app.route("/") - def index(): - @flask.after_this_request - def foo(response): - response.headers["X-Foo"] = "a header" - return response - - return "Test" - - resp = client.get("/") - assert resp.status_code == 200 - assert resp.headers["X-Foo"] == "a header" - - -def test_teardown_request_handler(app, client): - called = [] - - @app.teardown_request - def teardown_request(exc): - called.append(True) - return "Ignored" - - @app.route("/") - def root(): - return "Response" - - rv = client.get("/") - assert rv.status_code == 200 - assert b"Response" in rv.data - assert len(called) == 1 - - -def test_teardown_request_handler_debug_mode(app, client): - called = [] - - @app.teardown_request - def teardown_request(exc): - called.append(True) - return "Ignored" - - @app.route("/") - def root(): - return "Response" - - rv = client.get("/") - assert rv.status_code == 200 - assert b"Response" in rv.data - assert len(called) == 1 - - -def test_teardown_request_handler_error(app, client): - called = [] - app.testing = False - - @app.teardown_request - def teardown_request1(exc): - assert type(exc) is ZeroDivisionError - called.append(True) - # This raises a new error and blows away sys.exc_info(), so we can - # test that all teardown_requests get passed the same original - # exception. - try: - raise TypeError() - except Exception: - pass - - @app.teardown_request - def teardown_request2(exc): - assert type(exc) is ZeroDivisionError - called.append(True) - # This raises a new error and blows away sys.exc_info(), so we can - # test that all teardown_requests get passed the same original - # exception. - try: - raise TypeError() - except Exception: - pass - - @app.route("/") - def fails(): - raise ZeroDivisionError - - rv = client.get("/") - assert rv.status_code == 500 - assert b"Internal Server Error" in rv.data - assert len(called) == 2 - - -def test_before_after_request_order(app, client): - called = [] - - @app.before_request - def before1(): - called.append(1) - - @app.before_request - def before2(): - called.append(2) - - @app.after_request - def after1(response): - called.append(4) - return response - - @app.after_request - def after2(response): - called.append(3) - return response - - @app.teardown_request - def finish1(exc): - called.append(6) - - @app.teardown_request - def finish2(exc): - called.append(5) - - @app.route("/") - def index(): - return "42" - - rv = client.get("/") - assert rv.data == b"42" - assert called == [1, 2, 3, 4, 5, 6] - - -def test_error_handling(app, client): - app.testing = False - - @app.errorhandler(404) - def not_found(e): - return "not found", 404 - - @app.errorhandler(500) - def internal_server_error(e): - return "internal server error", 500 - - @app.errorhandler(Forbidden) - def forbidden(e): - return "forbidden", 403 - - @app.route("/") - def index(): - flask.abort(404) - - @app.route("/error") - def error(): - raise ZeroDivisionError - - @app.route("/forbidden") - def error2(): - flask.abort(403) - - rv = client.get("/") - assert rv.status_code == 404 - assert rv.data == b"not found" - rv = client.get("/error") - assert rv.status_code == 500 - assert b"internal server error" == rv.data - rv = client.get("/forbidden") - assert rv.status_code == 403 - assert b"forbidden" == rv.data - - -def test_error_handling_processing(app, client): - app.testing = False - - @app.errorhandler(500) - def internal_server_error(e): - return "internal server error", 500 - - @app.route("/") - def broken_func(): - raise ZeroDivisionError - - @app.after_request - def after_request(resp): - resp.mimetype = "text/x-special" - return resp - - resp = client.get("/") - assert resp.mimetype == "text/x-special" - assert resp.data == b"internal server error" - - -def test_baseexception_error_handling(app, client): - app.testing = False - - @app.route("/") - def broken_func(): - raise KeyboardInterrupt() - - with pytest.raises(KeyboardInterrupt): - client.get("/") - - -def test_before_request_and_routing_errors(app, client): - @app.before_request - def attach_something(): - flask.g.something = "value" - - @app.errorhandler(404) - def return_something(error): - return flask.g.something, 404 - - rv = client.get("/") - assert rv.status_code == 404 - assert rv.data == b"value" - - -def test_user_error_handling(app, client): - class MyException(Exception): - pass - - @app.errorhandler(MyException) - def handle_my_exception(e): - assert isinstance(e, MyException) - return "42" - - @app.route("/") - def index(): - raise MyException() - - assert client.get("/").data == b"42" - - -def test_http_error_subclass_handling(app, client): - class ForbiddenSubclass(Forbidden): - pass - - @app.errorhandler(ForbiddenSubclass) - def handle_forbidden_subclass(e): - assert isinstance(e, ForbiddenSubclass) - return "banana" - - @app.errorhandler(403) - def handle_403(e): - assert not isinstance(e, ForbiddenSubclass) - assert isinstance(e, Forbidden) - return "apple" - - @app.route("/1") - def index1(): - raise ForbiddenSubclass() - - @app.route("/2") - def index2(): - flask.abort(403) - - @app.route("/3") - def index3(): - raise Forbidden() - - assert client.get("/1").data == b"banana" - assert client.get("/2").data == b"apple" - assert client.get("/3").data == b"apple" - - -def test_errorhandler_precedence(app, client): - class E1(Exception): - pass - - class E2(Exception): - pass - - class E3(E1, E2): - pass - - @app.errorhandler(E2) - def handle_e2(e): - return "E2" - - @app.errorhandler(Exception) - def handle_exception(e): - return "Exception" - - @app.route("/E1") - def raise_e1(): - raise E1 - - @app.route("/E3") - def raise_e3(): - raise E3 - - rv = client.get("/E1") - assert rv.data == b"Exception" - - rv = client.get("/E3") - assert rv.data == b"E2" - - -@pytest.mark.parametrize( - ("debug", "trap", "expect_key", "expect_abort"), - [(False, None, True, True), (True, None, False, True), (False, True, False, False)], -) -def test_trap_bad_request_key_error(app, client, debug, trap, expect_key, expect_abort): - app.config["DEBUG"] = debug - app.config["TRAP_BAD_REQUEST_ERRORS"] = trap - - @app.route("/key") - def fail(): - flask.request.form["missing_key"] - - @app.route("/abort") - def allow_abort(): - flask.abort(400) - - if expect_key: - rv = client.get("/key") - assert rv.status_code == 400 - assert b"missing_key" not in rv.data - else: - with pytest.raises(KeyError) as exc_info: - client.get("/key") - - assert exc_info.errisinstance(BadRequest) - assert "missing_key" in exc_info.value.get_description() - - if expect_abort: - rv = client.get("/abort") - assert rv.status_code == 400 - else: - with pytest.raises(BadRequest): - client.get("/abort") - - -def test_trapping_of_all_http_exceptions(app, client): - app.config["TRAP_HTTP_EXCEPTIONS"] = True - - @app.route("/fail") - def fail(): - flask.abort(404) - - with pytest.raises(NotFound): - client.get("/fail") - - -def test_error_handler_after_processor_error(app, client): - app.testing = False - - @app.before_request - def before_request(): - if _trigger == "before": - raise ZeroDivisionError - - @app.after_request - def after_request(response): - if _trigger == "after": - raise ZeroDivisionError - - return response - - @app.route("/") - def index(): - return "Foo" - - @app.errorhandler(500) - def internal_server_error(e): - return "Hello Server Error", 500 - - for _trigger in "before", "after": - rv = client.get("/") - assert rv.status_code == 500 - assert rv.data == b"Hello Server Error" - - -def test_enctype_debug_helper(app, client): - from flask.debughelpers import DebugFilesKeyError - - app.debug = True - - @app.route("/fail", methods=["POST"]) - def index(): - return flask.request.files["foo"].filename - - with pytest.raises(DebugFilesKeyError) as e: - client.post("/fail", data={"foo": "index.txt"}) - assert "no file contents were transmitted" in str(e.value) - assert "This was submitted: 'index.txt'" in str(e.value) - - -def test_response_types(app, client): - @app.route("/text") - def from_text(): - return "Hällo Wörld" - - @app.route("/bytes") - def from_bytes(): - return "Hällo Wörld".encode() - - @app.route("/full_tuple") - def from_full_tuple(): - return ( - "Meh", - 400, - {"X-Foo": "Testing", "Content-Type": "text/plain; charset=utf-8"}, - ) - - @app.route("/text_headers") - def from_text_headers(): - return "Hello", {"X-Foo": "Test", "Content-Type": "text/plain; charset=utf-8"} - - @app.route("/text_status") - def from_text_status(): - return "Hi, status!", 400 - - @app.route("/response_headers") - def from_response_headers(): - return ( - flask.Response( - "Hello world", 404, {"Content-Type": "text/html", "X-Foo": "Baz"} - ), - {"Content-Type": "text/plain", "X-Foo": "Bar", "X-Bar": "Foo"}, - ) - - @app.route("/response_status") - def from_response_status(): - return app.response_class("Hello world", 400), 500 - - @app.route("/wsgi") - def from_wsgi(): - return NotFound() - - @app.route("/dict") - def from_dict(): - return {"foo": "bar"}, 201 - - @app.route("/list") - def from_list(): - return ["foo", "bar"], 201 - - assert client.get("/text").data == "Hällo Wörld".encode() - assert client.get("/bytes").data == "Hällo Wörld".encode() - - rv = client.get("/full_tuple") - assert rv.data == b"Meh" - assert rv.headers["X-Foo"] == "Testing" - assert rv.status_code == 400 - assert rv.mimetype == "text/plain" - - rv = client.get("/text_headers") - assert rv.data == b"Hello" - assert rv.headers["X-Foo"] == "Test" - assert rv.status_code == 200 - assert rv.mimetype == "text/plain" - - rv = client.get("/text_status") - assert rv.data == b"Hi, status!" - assert rv.status_code == 400 - assert rv.mimetype == "text/html" - - rv = client.get("/response_headers") - assert rv.data == b"Hello world" - assert rv.content_type == "text/plain" - assert rv.headers.getlist("X-Foo") == ["Bar"] - assert rv.headers["X-Bar"] == "Foo" - assert rv.status_code == 404 - - rv = client.get("/response_status") - assert rv.data == b"Hello world" - assert rv.status_code == 500 - - rv = client.get("/wsgi") - assert b"Not Found" in rv.data - assert rv.status_code == 404 - - rv = client.get("/dict") - assert rv.json == {"foo": "bar"} - assert rv.status_code == 201 - - rv = client.get("/list") - assert rv.json == ["foo", "bar"] - assert rv.status_code == 201 - - -def test_response_type_errors(): - app = flask.Flask(__name__) - app.testing = True - - @app.route("/none") - def from_none(): - pass - - @app.route("/small_tuple") - def from_small_tuple(): - return ("Hello",) - - @app.route("/large_tuple") - def from_large_tuple(): - return "Hello", 234, {"X-Foo": "Bar"}, "???" - - @app.route("/bad_type") - def from_bad_type(): - return True - - @app.route("/bad_wsgi") - def from_bad_wsgi(): - return lambda: None - - c = app.test_client() - - with pytest.raises(TypeError) as e: - c.get("/none") - - assert "returned None" in str(e.value) - assert "from_none" in str(e.value) - - with pytest.raises(TypeError) as e: - c.get("/small_tuple") - - assert "tuple must have the form" in str(e.value) - - with pytest.raises(TypeError): - c.get("/large_tuple") - - with pytest.raises(TypeError) as e: - c.get("/bad_type") - - assert "it was a bool" in str(e.value) - - with pytest.raises(TypeError): - c.get("/bad_wsgi") - - -def test_make_response(app, req_ctx): - rv = flask.make_response() - assert rv.status_code == 200 - assert rv.data == b"" - assert rv.mimetype == "text/html" - - rv = flask.make_response("Awesome") - assert rv.status_code == 200 - assert rv.data == b"Awesome" - assert rv.mimetype == "text/html" - - rv = flask.make_response("W00t", 404) - assert rv.status_code == 404 - assert rv.data == b"W00t" - assert rv.mimetype == "text/html" - - rv = flask.make_response(c for c in "Hello") - assert rv.status_code == 200 - assert rv.data == b"Hello" - assert rv.mimetype == "text/html" - - -def test_make_response_with_response_instance(app, req_ctx): - rv = flask.make_response(flask.jsonify({"msg": "W00t"}), 400) - assert rv.status_code == 400 - assert rv.data == b'{"msg":"W00t"}\n' - assert rv.mimetype == "application/json" - - rv = flask.make_response(flask.Response(""), 400) - assert rv.status_code == 400 - assert rv.data == b"" - assert rv.mimetype == "text/html" - - rv = flask.make_response( - flask.Response("", headers={"Content-Type": "text/html"}), - 400, - [("X-Foo", "bar")], - ) - assert rv.status_code == 400 - assert rv.headers["Content-Type"] == "text/html" - assert rv.headers["X-Foo"] == "bar" - - -@pytest.mark.parametrize("compact", [True, False]) -def test_jsonify_no_prettyprint(app, compact): - app.json.compact = compact - rv = app.json.response({"msg": {"submsg": "W00t"}, "msg2": "foobar"}) - data = rv.data.strip() - assert (b" " not in data) is compact - assert (b"\n" not in data) is compact - - -def test_jsonify_mimetype(app, req_ctx): - app.json.mimetype = "application/vnd.api+json" - msg = {"msg": {"submsg": "W00t"}} - rv = flask.make_response(flask.jsonify(msg), 200) - assert rv.mimetype == "application/vnd.api+json" - - -def test_json_dump_dataclass(app, req_ctx): - from dataclasses import make_dataclass - - Data = make_dataclass("Data", [("name", str)]) - value = app.json.dumps(Data("Flask")) - value = app.json.loads(value) - assert value == {"name": "Flask"} - - -def test_jsonify_args_and_kwargs_check(app, req_ctx): - with pytest.raises(TypeError) as e: - flask.jsonify("fake args", kwargs="fake") - assert "args or kwargs" in str(e.value) - - -def test_url_generation(app, req_ctx): - @app.route("/hello/", methods=["POST"]) - def hello(): - pass - - assert flask.url_for("hello", name="test x") == "/hello/test%20x" - assert ( - flask.url_for("hello", name="test x", _external=True) - == "http://localhost/hello/test%20x" - ) - - -def test_build_error_handler(app): - # Test base case, a URL which results in a BuildError. - with app.test_request_context(): - pytest.raises(BuildError, flask.url_for, "spam") - - # Verify the error is re-raised if not the current exception. - try: - with app.test_request_context(): - flask.url_for("spam") - except BuildError as err: - error = err - try: - raise RuntimeError("Test case where BuildError is not current.") - except RuntimeError: - pytest.raises(BuildError, app.handle_url_build_error, error, "spam", {}) - - # Test a custom handler. - def handler(error, endpoint, values): - # Just a test. - return "/test_handler/" - - app.url_build_error_handlers.append(handler) - with app.test_request_context(): - assert flask.url_for("spam") == "/test_handler/" - - -def test_build_error_handler_reraise(app): - # Test a custom handler which reraises the BuildError - def handler_raises_build_error(error, endpoint, values): - raise error - - app.url_build_error_handlers.append(handler_raises_build_error) - - with app.test_request_context(): - pytest.raises(BuildError, flask.url_for, "not.existing") - - -def test_url_for_passes_special_values_to_build_error_handler(app): - @app.url_build_error_handlers.append - def handler(error, endpoint, values): - assert values == { - "_external": False, - "_anchor": None, - "_method": None, - "_scheme": None, - } - return "handled" - - with app.test_request_context(): - flask.url_for("/") - - -def test_static_files(app, client): - with client.get("/static/index.html") as rv: - assert rv.status_code == 200 - assert rv.data.strip() == b"

Hello World!

" - with app.test_request_context(): - assert ( - flask.url_for("static", filename="index.html") == "/static/index.html" - ) - - -def test_static_url_path(): - app = flask.Flask(__name__, static_url_path="/foo") - app.testing = True - - with app.test_client().get("/foo/index.html") as rv: - assert rv.status_code == 200 - - with app.test_request_context(): - assert flask.url_for("static", filename="index.html") == "/foo/index.html" - - -def test_static_url_path_with_ending_slash(): - app = flask.Flask(__name__, static_url_path="/foo/") - app.testing = True - - with app.test_client().get("/foo/index.html") as rv: - assert rv.status_code == 200 - - with app.test_request_context(): - assert flask.url_for("static", filename="index.html") == "/foo/index.html" - - -def test_static_url_empty_path(app): - app = flask.Flask(__name__, static_folder="", static_url_path="") - - with app.test_client().open("/static/index.html", method="GET") as rv: - assert rv.status_code == 200 - - -def test_static_url_empty_path_default(app): - app = flask.Flask(__name__, static_folder="") - - with app.test_client().open("/static/index.html", method="GET") as rv: - assert rv.status_code == 200 - - -def test_static_folder_with_pathlib_path(app): - from pathlib import Path - - app = flask.Flask(__name__, static_folder=Path("static")) - - with app.test_client().open("/static/index.html", method="GET") as rv: - assert rv.status_code == 200 - - -def test_static_folder_with_ending_slash(): - app = flask.Flask(__name__, static_folder="static/") - - @app.route("/") - def catch_all(path): - return path - - rv = app.test_client().get("/catch/all") - assert rv.data == b"catch/all" - - -def test_static_route_with_host_matching(): - app = flask.Flask(__name__, host_matching=True, static_host="example.com") - c = app.test_client() - - with c.get("http://example.com/static/index.html") as rv: - assert rv.status_code == 200 - - with app.test_request_context(): - rv = flask.url_for("static", filename="index.html", _external=True) - assert rv == "http://example.com/static/index.html" - # Providing static_host without host_matching=True should error. - with pytest.raises(AssertionError): - flask.Flask(__name__, static_host="example.com") - # Providing host_matching=True with static_folder - # but without static_host should error. - with pytest.raises(AssertionError): - flask.Flask(__name__, host_matching=True) - # Providing host_matching=True without static_host - # but with static_folder=None should not error. - flask.Flask(__name__, host_matching=True, static_folder=None) - - -def test_request_locals(): - assert repr(flask.g) == "" - assert not flask.g - - -werkzeug_3_2 = importlib.metadata.version("werkzeug") >= "3.2." - - -@pytest.mark.parametrize( - ("subdomain_matching", "host_matching", "expect_subdomain", "expect_host"), - [ - (False, False, "default", "default"), - (True, False, "abc", ""), - (False, True, "abc", "default"), - ], -) -def test_server_name_matching( - subdomain_matching: bool, - host_matching: bool, - expect_subdomain: str, - expect_host: str, -) -> None: - app = flask.Flask( - __name__, - subdomain_matching=subdomain_matching, - host_matching=host_matching, - static_host="example.test" if host_matching else None, - ) - app.config["SERVER_NAME"] = "example.test" - - @app.route("/", defaults={"name": "default"}, host="") - @app.route("/", subdomain="", host=".example.test") - def index(name: str) -> str: - return name - - client = app.test_client() - - r = client.get(base_url="http://example.test") - assert r.text == "default" - - r = client.get(base_url="http://abc.example.test") - assert r.text == expect_subdomain - - with pytest.warns() if subdomain_matching else nullcontext(): - r = client.get(base_url="http://xyz.other.test") - - if werkzeug_3_2: - assert r.text == "default" - else: - assert r.text == expect_host - - -def test_server_name_subdomain(): - app = flask.Flask(__name__, subdomain_matching=True) - client = app.test_client() - - @app.route("/") - def index(): - return "default" - - @app.route("/", subdomain="foo") - def subdomain(): - return "subdomain" - - app.config["SERVER_NAME"] = "dev.local:5000" - rv = client.get("/") - assert rv.data == b"default" - - rv = client.get("/", "http://dev.local:5000") - assert rv.data == b"default" - - rv = client.get("/", "https://dev.local:5000") - assert rv.data == b"default" - - app.config["SERVER_NAME"] = "dev.local:443" - rv = client.get("/", "https://dev.local") - - # Werkzeug 1.0 fixes matching https scheme with 443 port - if rv.status_code != 404: - assert rv.data == b"default" - - app.config["SERVER_NAME"] = "dev.local" - rv = client.get("/", "https://dev.local") - assert rv.data == b"default" - - with pytest.warns(match="Current server name"): - rv = client.get("/", "http://foo.localhost") - - if werkzeug_3_2: - assert rv.status_code == 200 - else: - assert rv.status_code == 404 - - rv = client.get("/", "http://foo.dev.local") - assert rv.data == b"subdomain" - - -@pytest.mark.parametrize("key", ["TESTING", "PROPAGATE_EXCEPTIONS", "DEBUG", None]) -def test_exception_propagation(app, client, key): - app.testing = False - - @app.route("/") - def index(): - raise ZeroDivisionError - - if key is not None: - app.config[key] = True - - with pytest.raises(ZeroDivisionError): - client.get("/") - else: - assert client.get("/").status_code == 500 - - -@pytest.mark.parametrize("debug", [True, False]) -@pytest.mark.parametrize("use_debugger", [True, False]) -@pytest.mark.parametrize("use_reloader", [True, False]) -@pytest.mark.parametrize("propagate_exceptions", [None, True, False]) -def test_werkzeug_passthrough_errors( - monkeypatch, debug, use_debugger, use_reloader, propagate_exceptions, app -): - rv = {} - - # Mocks werkzeug.serving.run_simple method - def run_simple_mock(*args, **kwargs): - rv["passthrough_errors"] = kwargs.get("passthrough_errors") - - monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) - app.config["PROPAGATE_EXCEPTIONS"] = propagate_exceptions - app.run(debug=debug, use_debugger=use_debugger, use_reloader=use_reloader) - - -def test_url_processors(app, client): - @app.url_defaults - def add_language_code(endpoint, values): - if flask.g.lang_code is not None and app.url_map.is_endpoint_expecting( - endpoint, "lang_code" - ): - values.setdefault("lang_code", flask.g.lang_code) - - @app.url_value_preprocessor - def pull_lang_code(endpoint, values): - flask.g.lang_code = values.pop("lang_code", None) - - @app.route("//") - def index(): - return flask.url_for("about") - - @app.route("//about") - def about(): - return flask.url_for("something_else") - - @app.route("/foo") - def something_else(): - return flask.url_for("about", lang_code="en") - - assert client.get("/de/").data == b"/de/about" - assert client.get("/de/about").data == b"/foo" - assert client.get("/foo").data == b"/en/about" - - -def test_inject_blueprint_url_defaults(app): - bp = flask.Blueprint("foo", __name__, template_folder="template") - - @bp.url_defaults - def bp_defaults(endpoint, values): - values["page"] = "login" - - @bp.route("/") - def view(page): - pass - - app.register_blueprint(bp) - - values = dict() - app.inject_url_defaults("foo.view", values) - expected = dict(page="login") - assert values == expected - - with app.test_request_context("/somepage"): - url = flask.url_for("foo.view") - expected = "/login" - assert url == expected - - -def test_nonascii_pathinfo(app, client): - @app.route("/киртест") - def index(): - return "Hello World!" - - rv = client.get("/киртест") - assert rv.data == b"Hello World!" - - -def test_no_setup_after_first_request(app, client): - app.debug = True - - @app.route("/") - def index(): - return "Awesome" - - assert client.get("/").data == b"Awesome" - - with pytest.raises(AssertionError) as exc_info: - app.add_url_rule("/foo", endpoint="late") - - assert "setup method 'add_url_rule'" in str(exc_info.value) - - -def test_routing_redirect_debugging(monkeypatch, app, client): - app.config["DEBUG"] = True - - @app.route("/user/", methods=["GET", "POST"]) - def user(): - return flask.request.form["status"] - - # default redirect code preserves form data - rv = client.post("/user", data={"status": "success"}, follow_redirects=True) - assert rv.data == b"success" - - # 301 and 302 raise error - monkeypatch.setattr(RequestRedirect, "code", 301) - - with client, pytest.raises(AssertionError) as exc_info: - client.post("/user", data={"status": "error"}, follow_redirects=True) - - assert "canonical URL 'http://localhost/user/'" in str(exc_info.value) - - -def test_route_decorator_custom_endpoint(app, client): - app.debug = True - - @app.route("/foo/") - def foo(): - return flask.request.endpoint - - @app.route("/bar/", endpoint="bar") - def for_bar(): - return flask.request.endpoint - - @app.route("/bar/123", endpoint="123") - def for_bar_foo(): - return flask.request.endpoint - - with app.test_request_context(): - assert flask.url_for("foo") == "/foo/" - assert flask.url_for("bar") == "/bar/" - assert flask.url_for("123") == "/bar/123" - - assert client.get("/foo/").data == b"foo" - assert client.get("/bar/").data == b"bar" - assert client.get("/bar/123").data == b"123" - - -def test_get_method_on_g(app_ctx): - assert flask.g.get("x") is None - assert flask.g.get("x", 11) == 11 - flask.g.x = 42 - assert flask.g.get("x") == 42 - assert flask.g.x == 42 - - -def test_g_iteration_protocol(app_ctx): - flask.g.foo = 23 - flask.g.bar = 42 - assert "foo" in flask.g - assert "foos" not in flask.g - assert sorted(flask.g) == ["bar", "foo"] - - -def test_subdomain_basic_support(): - app = flask.Flask(__name__, subdomain_matching=True) - app.config["SERVER_NAME"] = "localhost.localdomain" - client = app.test_client() - - @app.route("/") - def normal_index(): - return "normal index" - - @app.route("/", subdomain="test") - def test_index(): - return "test index" - - rv = client.get("/", "http://localhost.localdomain/") - assert rv.data == b"normal index" - - rv = client.get("/", "http://test.localhost.localdomain/") - assert rv.data == b"test index" - - -def test_subdomain_matching(): - app = flask.Flask(__name__, subdomain_matching=True) - client = app.test_client() - app.config["SERVER_NAME"] = "localhost.localdomain" - - @app.route("/", subdomain="") - def index(user): - return f"index for {user}" - - rv = client.get("/", "http://mitsuhiko.localhost.localdomain/") - assert rv.data == b"index for mitsuhiko" - - -def test_subdomain_matching_with_ports(): - app = flask.Flask(__name__, subdomain_matching=True) - app.config["SERVER_NAME"] = "localhost.localdomain:3000" - client = app.test_client() - - @app.route("/", subdomain="") - def index(user): - return f"index for {user}" - - rv = client.get("/", "http://mitsuhiko.localhost.localdomain:3000/") - assert rv.data == b"index for mitsuhiko" - - -@pytest.mark.parametrize("matching", (False, True)) -def test_subdomain_matching_other_name(matching): - app = flask.Flask(__name__, subdomain_matching=matching) - app.config["SERVER_NAME"] = "localhost.localdomain:3000" - client = app.test_client() - - @app.route("/") - def index(): - return "", 204 - - with pytest.warns(match="Current server name") if matching else nullcontext(): - # ip address can't match name, but will fall back to default - rv = client.get("/", "http://127.0.0.1:3000/") - - if werkzeug_3_2: - assert rv.status_code == 204 - else: - assert rv.status_code == 404 if matching else 204 - - # allow all subdomains if matching is disabled - rv = client.get("/", "http://www.localhost.localdomain:3000/") - assert rv.status_code == 404 if matching else 204 - - -def test_multi_route_rules(app, client): - @app.route("/") - @app.route("//") - def index(test="a"): - return test - - rv = client.open("/") - assert rv.data == b"a" - rv = client.open("/b/") - assert rv.data == b"b" - - -def test_multi_route_class_views(app, client): - class View: - def __init__(self, app): - app.add_url_rule("/", "index", self.index) - app.add_url_rule("//", "index", self.index) - - def index(self, test="a"): - return test - - _ = View(app) - rv = client.open("/") - assert rv.data == b"a" - rv = client.open("/b/") - assert rv.data == b"b" - - -def test_run_defaults(monkeypatch, app): - rv = {} - - # Mocks werkzeug.serving.run_simple method - def run_simple_mock(*args, **kwargs): - rv["result"] = "running..." - - monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) - app.run() - assert rv["result"] == "running..." - - -def test_run_server_port(monkeypatch, app): - rv = {} - - # Mocks werkzeug.serving.run_simple method - def run_simple_mock(hostname, port, application, *args, **kwargs): - rv["result"] = f"running on {hostname}:{port} ..." - - monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) - hostname, port = "localhost", 8000 - app.run(hostname, port, debug=True) - assert rv["result"] == f"running on {hostname}:{port} ..." - - -@pytest.mark.parametrize( - "host,port,server_name,expect_host,expect_port", - ( - (None, None, "pocoo.org:8080", "pocoo.org", 8080), - ("localhost", None, "pocoo.org:8080", "localhost", 8080), - (None, 80, "pocoo.org:8080", "pocoo.org", 80), - ("localhost", 80, "pocoo.org:8080", "localhost", 80), - ("localhost", 0, "localhost:8080", "localhost", 0), - (None, None, "localhost:8080", "localhost", 8080), - (None, None, "localhost:0", "localhost", 0), - ), -) -def test_run_from_config( - monkeypatch, host, port, server_name, expect_host, expect_port, app -): - def run_simple_mock(hostname, port, *args, **kwargs): - assert hostname == expect_host - assert port == expect_port - - monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) - app.config["SERVER_NAME"] = server_name - app.run(host, port) - - -def test_max_cookie_size(app, client, recwarn): - app.config["MAX_COOKIE_SIZE"] = 100 - - # outside app context, default to Werkzeug static value, - # which is also the default config - response = flask.Response() - default = flask.Flask.default_config["MAX_COOKIE_SIZE"] - assert response.max_cookie_size == default - - # inside app context, use app config - with app.app_context(): - assert flask.Response().max_cookie_size == 100 - - @app.route("/") - def index(): - r = flask.Response("", status=204) - r.set_cookie("foo", "bar" * 100) - return r - - client.get("/") - assert len(recwarn) == 1 - w = recwarn.pop() - assert "cookie is too large" in str(w.message) - - app.config["MAX_COOKIE_SIZE"] = 0 - - client.get("/") - assert len(recwarn) == 0 - - -@require_cpython_gc -def test_app_freed_on_zero_refcount(): - # A Flask instance should not create a reference cycle that prevents CPython - # from freeing it when all external references to it are released (see #3761). - gc.disable() - try: - app = flask.Flask(__name__) - assert app.view_functions["static"] - weak = weakref.ref(app) - assert weak() is not None - del app - assert weak() is None - finally: - gc.enable() diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py deleted file mode 100644 index 6eb7d1d5..00000000 --- a/tests/test_blueprints.py +++ /dev/null @@ -1,1118 +0,0 @@ -import pytest -from jinja2 import TemplateNotFound -from werkzeug.http import parse_cache_control_header - -import flask - - -def test_blueprint_specific_error_handling(app, client): - frontend = flask.Blueprint("frontend", __name__) - backend = flask.Blueprint("backend", __name__) - sideend = flask.Blueprint("sideend", __name__) - - @frontend.errorhandler(403) - def frontend_forbidden(e): - return "frontend says no", 403 - - @frontend.route("/frontend-no") - def frontend_no(): - flask.abort(403) - - @backend.errorhandler(403) - def backend_forbidden(e): - return "backend says no", 403 - - @backend.route("/backend-no") - def backend_no(): - flask.abort(403) - - @sideend.route("/what-is-a-sideend") - def sideend_no(): - flask.abort(403) - - app.register_blueprint(frontend) - app.register_blueprint(backend) - app.register_blueprint(sideend) - - @app.errorhandler(403) - def app_forbidden(e): - return "application itself says no", 403 - - assert client.get("/frontend-no").data == b"frontend says no" - assert client.get("/backend-no").data == b"backend says no" - assert client.get("/what-is-a-sideend").data == b"application itself says no" - - -def test_blueprint_specific_user_error_handling(app, client): - class MyDecoratorException(Exception): - pass - - class MyFunctionException(Exception): - pass - - blue = flask.Blueprint("blue", __name__) - - @blue.errorhandler(MyDecoratorException) - def my_decorator_exception_handler(e): - assert isinstance(e, MyDecoratorException) - return "boom" - - def my_function_exception_handler(e): - assert isinstance(e, MyFunctionException) - return "bam" - - blue.register_error_handler(MyFunctionException, my_function_exception_handler) - - @blue.route("/decorator") - def blue_deco_test(): - raise MyDecoratorException() - - @blue.route("/function") - def blue_func_test(): - raise MyFunctionException() - - app.register_blueprint(blue) - - assert client.get("/decorator").data == b"boom" - assert client.get("/function").data == b"bam" - - -def test_blueprint_app_error_handling(app, client): - errors = flask.Blueprint("errors", __name__) - - @errors.app_errorhandler(403) - def forbidden_handler(e): - return "you shall not pass", 403 - - @app.route("/forbidden") - def app_forbidden(): - flask.abort(403) - - forbidden_bp = flask.Blueprint("forbidden_bp", __name__) - - @forbidden_bp.route("/nope") - def bp_forbidden(): - flask.abort(403) - - app.register_blueprint(errors) - app.register_blueprint(forbidden_bp) - - assert client.get("/forbidden").data == b"you shall not pass" - assert client.get("/nope").data == b"you shall not pass" - - -@pytest.mark.parametrize( - ("prefix", "rule", "url"), - ( - ("", "/", "/"), - ("/", "", "/"), - ("/", "/", "/"), - ("/foo", "", "/foo"), - ("/foo/", "", "/foo/"), - ("", "/bar", "/bar"), - ("/foo/", "/bar", "/foo/bar"), - ("/foo/", "bar", "/foo/bar"), - ("/foo", "/bar", "/foo/bar"), - ("/foo/", "//bar", "/foo/bar"), - ("/foo//", "/bar", "/foo/bar"), - ), -) -def test_blueprint_prefix_slash(app, client, prefix, rule, url): - bp = flask.Blueprint("test", __name__, url_prefix=prefix) - - @bp.route(rule) - def index(): - return "", 204 - - app.register_blueprint(bp) - assert client.get(url).status_code == 204 - - -def test_blueprint_url_defaults(app, client): - bp = flask.Blueprint("test", __name__) - - @bp.route("/foo", defaults={"baz": 42}) - def foo(bar, baz): - return f"{bar}/{baz:d}" - - @bp.route("/bar") - def bar(bar): - return str(bar) - - app.register_blueprint(bp, url_prefix="/1", url_defaults={"bar": 23}) - app.register_blueprint(bp, name="test2", url_prefix="/2", url_defaults={"bar": 19}) - - assert client.get("/1/foo").data == b"23/42" - assert client.get("/2/foo").data == b"19/42" - assert client.get("/1/bar").data == b"23" - assert client.get("/2/bar").data == b"19" - - -def test_blueprint_url_processors(app, client): - bp = flask.Blueprint("frontend", __name__, url_prefix="/") - - @bp.url_defaults - def add_language_code(endpoint, values): - values.setdefault("lang_code", flask.g.lang_code) - - @bp.url_value_preprocessor - def pull_lang_code(endpoint, values): - flask.g.lang_code = values.pop("lang_code") - - @bp.route("/") - def index(): - return flask.url_for(".about") - - @bp.route("/about") - def about(): - return flask.url_for(".index") - - app.register_blueprint(bp) - - assert client.get("/de/").data == b"/de/about" - assert client.get("/de/about").data == b"/de/" - - -def test_templates_and_static(test_apps): - from blueprintapp import app - - client = app.test_client() - - rv = client.get("/") - assert rv.data == b"Hello from the Frontend" - rv = client.get("/admin/") - assert rv.data == b"Hello from the Admin" - rv = client.get("/admin/index2") - assert rv.data == b"Hello from the Admin" - - with client.get("/admin/static/test.txt") as rv: - assert rv.data.strip() == b"Admin File" - - with client.get("/admin/static/css/test.css") as rv: - assert rv.data.strip() == b"/* nested file */" - - # try/finally, in case other tests use this app for Blueprint tests. - max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"] - try: - expected_max_age = 3600 - if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == expected_max_age: - expected_max_age = 7200 - app.config["SEND_FILE_MAX_AGE_DEFAULT"] = expected_max_age - - with client.get("/admin/static/css/test.css") as rv: - cc = parse_cache_control_header(rv.headers["Cache-Control"]) - assert cc.max_age == expected_max_age - finally: - app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default - - with app.test_request_context(): - assert ( - flask.url_for("admin.static", filename="test.txt") - == "/admin/static/test.txt" - ) - - with app.test_request_context(): - with pytest.raises(TemplateNotFound) as e: - flask.render_template("missing.html") - assert e.value.name == "missing.html" - - with flask.Flask(__name__).test_request_context(): - assert flask.render_template("nested/nested.txt") == "I'm nested" - - -def test_default_static_max_age(app: flask.Flask) -> None: - class MyBlueprint(flask.Blueprint): - def get_send_file_max_age(self, filename): - return 100 - - blueprint = MyBlueprint( - "blueprint", __name__, url_prefix="/bp", static_folder="static" - ) - app.register_blueprint(blueprint) - - with app.test_request_context(), blueprint.send_static_file("index.html") as rv: - cc = parse_cache_control_header(rv.headers["Cache-Control"]) - assert cc.max_age == 100 - - -def test_templates_list(test_apps): - from blueprintapp import app - - templates = sorted(app.jinja_env.list_templates()) - assert templates == ["admin/index.html", "frontend/index.html"] - - -def test_dotted_name_not_allowed(app, client): - with pytest.raises(ValueError): - flask.Blueprint("app.ui", __name__) - - -def test_empty_name_not_allowed(app, client): - with pytest.raises(ValueError): - flask.Blueprint("", __name__) - - -def test_dotted_names_from_app(app, client): - test = flask.Blueprint("test", __name__) - - @app.route("/") - def app_index(): - return flask.url_for("test.index") - - @test.route("/test/") - def index(): - return flask.url_for("app_index") - - app.register_blueprint(test) - - rv = client.get("/") - assert rv.data == b"/test/" - - -def test_empty_url_defaults(app, client): - bp = flask.Blueprint("bp", __name__) - - @bp.route("/", defaults={"page": 1}) - @bp.route("/page/") - def something(page): - return str(page) - - app.register_blueprint(bp) - - assert client.get("/").data == b"1" - assert client.get("/page/2").data == b"2" - - -def test_route_decorator_custom_endpoint(app, client): - bp = flask.Blueprint("bp", __name__) - - @bp.route("/foo") - def foo(): - return flask.request.endpoint - - @bp.route("/bar", endpoint="bar") - def foo_bar(): - return flask.request.endpoint - - @bp.route("/bar/123", endpoint="123") - def foo_bar_foo(): - return flask.request.endpoint - - @bp.route("/bar/foo") - def bar_foo(): - return flask.request.endpoint - - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.request.endpoint - - assert client.get("/").data == b"index" - assert client.get("/py/foo").data == b"bp.foo" - assert client.get("/py/bar").data == b"bp.bar" - assert client.get("/py/bar/123").data == b"bp.123" - assert client.get("/py/bar/foo").data == b"bp.bar_foo" - - -def test_route_decorator_custom_endpoint_with_dots(app, client): - bp = flask.Blueprint("bp", __name__) - - with pytest.raises(ValueError): - bp.route("/", endpoint="a.b")(lambda: "") - - with pytest.raises(ValueError): - bp.add_url_rule("/", endpoint="a.b") - - def view(): - return "" - - view.__name__ = "a.b" - - with pytest.raises(ValueError): - bp.add_url_rule("/", view_func=view) - - -def test_endpoint_decorator(app, client): - from werkzeug.routing import Rule - - app.url_map.add(Rule("/foo", endpoint="bar")) - - bp = flask.Blueprint("bp", __name__) - - @bp.endpoint("bar") - def foobar(): - return flask.request.endpoint - - app.register_blueprint(bp, url_prefix="/bp_prefix") - - assert client.get("/foo").data == b"bar" - assert client.get("/bp_prefix/bar").status_code == 404 - - -def test_template_filter(app): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_filter() - def my_reverse(s): - return s[::-1] - - @bp.app_template_filter - def my_reverse_2(s): - return s[::-1] - - @bp.app_template_filter("my_reverse_custom_name_3") - def my_reverse_3(s): - return s[::-1] - - @bp.app_template_filter(name="my_reverse_custom_name_4") - def my_reverse_4(s): - return s[::-1] - - app.register_blueprint(bp, url_prefix="/py") - assert "my_reverse" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse"] == my_reverse - assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" - - assert "my_reverse_2" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse_2"] == my_reverse_2 - assert app.jinja_env.filters["my_reverse_2"]("abcd") == "dcba" - - assert "my_reverse_custom_name_3" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse_custom_name_3"] == my_reverse_3 - assert app.jinja_env.filters["my_reverse_custom_name_3"]("abcd") == "dcba" - - assert "my_reverse_custom_name_4" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse_custom_name_4"] == my_reverse_4 - assert app.jinja_env.filters["my_reverse_custom_name_4"]("abcd") == "dcba" - - -def test_add_template_filter(app): - bp = flask.Blueprint("bp", __name__) - - def my_reverse(s): - return s[::-1] - - bp.add_app_template_filter(my_reverse) - app.register_blueprint(bp, url_prefix="/py") - assert "my_reverse" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse"] == my_reverse - assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" - - -def test_template_filter_with_name(app): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_filter("strrev") - def my_reverse(s): - return s[::-1] - - app.register_blueprint(bp, url_prefix="/py") - assert "strrev" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["strrev"] == my_reverse - assert app.jinja_env.filters["strrev"]("abcd") == "dcba" - - -def test_add_template_filter_with_name(app): - bp = flask.Blueprint("bp", __name__) - - def my_reverse(s): - return s[::-1] - - bp.add_app_template_filter(my_reverse, "strrev") - app.register_blueprint(bp, url_prefix="/py") - assert "strrev" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["strrev"] == my_reverse - assert app.jinja_env.filters["strrev"]("abcd") == "dcba" - - -def test_template_filter_with_template(app, client): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_filter() - def super_reverse(s): - return s[::-1] - - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_template_filter_after_route_with_template(app, client): - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_filter() - def super_reverse(s): - return s[::-1] - - app.register_blueprint(bp, url_prefix="/py") - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_add_template_filter_with_template(app, client): - bp = flask.Blueprint("bp", __name__) - - def super_reverse(s): - return s[::-1] - - bp.add_app_template_filter(super_reverse) - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_template_filter_with_name_and_template(app, client): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_filter("super_reverse") - def my_reverse(s): - return s[::-1] - - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_add_template_filter_with_name_and_template(app, client): - bp = flask.Blueprint("bp", __name__) - - def my_reverse(s): - return s[::-1] - - bp.add_app_template_filter(my_reverse, "super_reverse") - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_template_test(app): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_test() - def is_boolean(value): - return isinstance(value, bool) - - @bp.app_template_test - def boolean_2(value): - return isinstance(value, bool) - - @bp.app_template_test("my_boolean_custom_name") - def boolean_3(value): - return isinstance(value, bool) - - @bp.app_template_test(name="my_boolean_custom_name_2") - def boolean_4(value): - return isinstance(value, bool) - - app.register_blueprint(bp, url_prefix="/py") - assert "is_boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["is_boolean"] == is_boolean - assert app.jinja_env.tests["is_boolean"](False) - - assert "boolean_2" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean_2"] == boolean_2 - assert app.jinja_env.tests["boolean_2"](False) - - assert "my_boolean_custom_name" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["my_boolean_custom_name"] == boolean_3 - assert app.jinja_env.tests["my_boolean_custom_name"](False) - - assert "my_boolean_custom_name_2" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["my_boolean_custom_name_2"] == boolean_4 - assert app.jinja_env.tests["my_boolean_custom_name_2"](False) - - -def test_add_template_test(app): - bp = flask.Blueprint("bp", __name__) - - def is_boolean(value): - return isinstance(value, bool) - - bp.add_app_template_test(is_boolean) - app.register_blueprint(bp, url_prefix="/py") - assert "is_boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["is_boolean"] == is_boolean - assert app.jinja_env.tests["is_boolean"](False) - - -def test_template_test_with_name(app): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_test("boolean") - def is_boolean(value): - return isinstance(value, bool) - - app.register_blueprint(bp, url_prefix="/py") - assert "boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean"] == is_boolean - assert app.jinja_env.tests["boolean"](False) - - -def test_add_template_test_with_name(app): - bp = flask.Blueprint("bp", __name__) - - def is_boolean(value): - return isinstance(value, bool) - - bp.add_app_template_test(is_boolean, "boolean") - app.register_blueprint(bp, url_prefix="/py") - assert "boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean"] == is_boolean - assert app.jinja_env.tests["boolean"](False) - - -def test_template_test_with_template(app, client): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_test() - def boolean(value): - return isinstance(value, bool) - - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_template_test_after_route_with_template(app, client): - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_test() - def boolean(value): - return isinstance(value, bool) - - app.register_blueprint(bp, url_prefix="/py") - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_add_template_test_with_template(app, client): - bp = flask.Blueprint("bp", __name__) - - def boolean(value): - return isinstance(value, bool) - - bp.add_app_template_test(boolean) - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_template_test_with_name_and_template(app, client): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_test("boolean") - def is_boolean(value): - return isinstance(value, bool) - - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_add_template_test_with_name_and_template(app, client): - bp = flask.Blueprint("bp", __name__) - - def is_boolean(value): - return isinstance(value, bool) - - bp.add_app_template_test(is_boolean, "boolean") - app.register_blueprint(bp, url_prefix="/py") - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_context_processing(app, client): - answer_bp = flask.Blueprint("answer_bp", __name__) - - def template_string(): - return flask.render_template_string( - "{% if notanswer %}{{ notanswer }} is not the answer. {% endif %}" - "{% if answer %}{{ answer }} is the answer.{% endif %}" - ) - - # App global context processor - @answer_bp.app_context_processor - def not_answer_context_processor(): - return {"notanswer": 43} - - # Blueprint local context processor - @answer_bp.context_processor - def answer_context_processor(): - return {"answer": 42} - - # Setup endpoints for testing - @answer_bp.route("/bp") - def bp_page(): - return template_string() - - @app.route("/") - def app_page(): - return template_string() - - # Register the blueprint - app.register_blueprint(answer_bp) - - app_page_bytes = client.get("/").data - answer_page_bytes = client.get("/bp").data - - assert b"43" in app_page_bytes - assert b"42" not in app_page_bytes - - assert b"42" in answer_page_bytes - assert b"43" in answer_page_bytes - - -def test_template_global(app): - bp = flask.Blueprint("bp", __name__) - - @bp.app_template_global() - def get_answer(): - return 42 - - @bp.app_template_global - def get_stuff_1(): - return "get_stuff_1" - - @bp.app_template_global("my_get_stuff_custom_name_2") - def get_stuff_2(): - return "get_stuff_2" - - @bp.app_template_global(name="my_get_stuff_custom_name_3") - def get_stuff_3(): - return "get_stuff_3" - - # Make sure the function is not in the jinja_env already - assert "get_answer" not in app.jinja_env.globals.keys() - app.register_blueprint(bp) - - # Tests - assert "get_answer" in app.jinja_env.globals.keys() - assert app.jinja_env.globals["get_answer"] is get_answer - assert app.jinja_env.globals["get_answer"]() == 42 - - assert "get_stuff_1" in app.jinja_env.globals.keys() - assert app.jinja_env.globals["get_stuff_1"] == get_stuff_1 - assert app.jinja_env.globals["get_stuff_1"](), "get_stuff_1" - - assert "my_get_stuff_custom_name_2" in app.jinja_env.globals.keys() - assert app.jinja_env.globals["my_get_stuff_custom_name_2"] == get_stuff_2 - assert app.jinja_env.globals["my_get_stuff_custom_name_2"](), "get_stuff_2" - - assert "my_get_stuff_custom_name_3" in app.jinja_env.globals.keys() - assert app.jinja_env.globals["my_get_stuff_custom_name_3"] == get_stuff_3 - assert app.jinja_env.globals["my_get_stuff_custom_name_3"](), "get_stuff_3" - - with app.app_context(): - rv = flask.render_template_string("{{ get_answer() }}") - assert rv == "42" - - rv = flask.render_template_string("{{ get_stuff_1() }}") - assert rv == "get_stuff_1" - - rv = flask.render_template_string("{{ my_get_stuff_custom_name_2() }}") - assert rv == "get_stuff_2" - - rv = flask.render_template_string("{{ my_get_stuff_custom_name_3() }}") - assert rv == "get_stuff_3" - - -def test_request_processing(app, client): - bp = flask.Blueprint("bp", __name__) - evts = [] - - @bp.before_request - def before_bp(): - evts.append("before") - - @bp.after_request - def after_bp(response): - response.data += b"|after" - evts.append("after") - return response - - @bp.teardown_request - def teardown_bp(exc): - evts.append("teardown") - - # Setup routes for testing - @bp.route("/bp") - def bp_endpoint(): - return "request" - - app.register_blueprint(bp) - - assert evts == [] - rv = client.get("/bp") - assert rv.data == b"request|after" - assert evts == ["before", "after", "teardown"] - - -def test_app_request_processing(app, client): - bp = flask.Blueprint("bp", __name__) - evts = [] - - @bp.before_app_request - def before_app(): - evts.append("before") - - @bp.after_app_request - def after_app(response): - response.data += b"|after" - evts.append("after") - return response - - @bp.teardown_app_request - def teardown_app(exc): - evts.append("teardown") - - app.register_blueprint(bp) - - # Setup routes for testing - @app.route("/") - def bp_endpoint(): - return "request" - - # before first request - assert evts == [] - - # first request - resp = client.get("/").data - assert resp == b"request|after" - assert evts == ["before", "after", "teardown"] - - # second request - resp = client.get("/").data - assert resp == b"request|after" - assert evts == ["before", "after", "teardown"] * 2 - - -def test_app_url_processors(app, client): - bp = flask.Blueprint("bp", __name__) - - # Register app-wide url defaults and preprocessor on blueprint - @bp.app_url_defaults - def add_language_code(endpoint, values): - values.setdefault("lang_code", flask.g.lang_code) - - @bp.app_url_value_preprocessor - def pull_lang_code(endpoint, values): - flask.g.lang_code = values.pop("lang_code") - - # Register route rules at the app level - @app.route("//") - def index(): - return flask.url_for("about") - - @app.route("//about") - def about(): - return flask.url_for("index") - - app.register_blueprint(bp) - - assert client.get("/de/").data == b"/de/about" - assert client.get("/de/about").data == b"/de/" - - -def test_nested_blueprint(app, client): - parent = flask.Blueprint("parent", __name__) - child = flask.Blueprint("child", __name__) - grandchild = flask.Blueprint("grandchild", __name__) - - @parent.errorhandler(403) - def forbidden(e): - return "Parent no", 403 - - @parent.route("/") - def parent_index(): - return "Parent yes" - - @parent.route("/no") - def parent_no(): - flask.abort(403) - - @child.route("/") - def child_index(): - return "Child yes" - - @child.route("/no") - def child_no(): - flask.abort(403) - - @grandchild.errorhandler(403) - def grandchild_forbidden(e): - return "Grandchild no", 403 - - @grandchild.route("/") - def grandchild_index(): - return "Grandchild yes" - - @grandchild.route("/no") - def grandchild_no(): - flask.abort(403) - - child.register_blueprint(grandchild, url_prefix="/grandchild") - parent.register_blueprint(child, url_prefix="/child") - app.register_blueprint(parent, url_prefix="/parent") - - assert client.get("/parent/").data == b"Parent yes" - assert client.get("/parent/child/").data == b"Child yes" - assert client.get("/parent/child/grandchild/").data == b"Grandchild yes" - assert client.get("/parent/no").data == b"Parent no" - assert client.get("/parent/child/no").data == b"Parent no" - assert client.get("/parent/child/grandchild/no").data == b"Grandchild no" - - -def test_nested_callback_order(app, client): - parent = flask.Blueprint("parent", __name__) - child = flask.Blueprint("child", __name__) - - @app.before_request - def app_before1(): - flask.g.setdefault("seen", []).append("app_1") - - @app.teardown_request - def app_teardown1(e=None): - assert flask.g.seen.pop() == "app_1" - - @app.before_request - def app_before2(): - flask.g.setdefault("seen", []).append("app_2") - - @app.teardown_request - def app_teardown2(e=None): - assert flask.g.seen.pop() == "app_2" - - @app.context_processor - def app_ctx(): - return dict(key="app") - - @parent.before_request - def parent_before1(): - flask.g.setdefault("seen", []).append("parent_1") - - @parent.teardown_request - def parent_teardown1(e=None): - assert flask.g.seen.pop() == "parent_1" - - @parent.before_request - def parent_before2(): - flask.g.setdefault("seen", []).append("parent_2") - - @parent.teardown_request - def parent_teardown2(e=None): - assert flask.g.seen.pop() == "parent_2" - - @parent.context_processor - def parent_ctx(): - return dict(key="parent") - - @child.before_request - def child_before1(): - flask.g.setdefault("seen", []).append("child_1") - - @child.teardown_request - def child_teardown1(e=None): - assert flask.g.seen.pop() == "child_1" - - @child.before_request - def child_before2(): - flask.g.setdefault("seen", []).append("child_2") - - @child.teardown_request - def child_teardown2(e=None): - assert flask.g.seen.pop() == "child_2" - - @child.context_processor - def child_ctx(): - return dict(key="child") - - @child.route("/a") - def a(): - return ", ".join(flask.g.seen) - - @child.route("/b") - def b(): - return flask.render_template_string("{{ key }}") - - parent.register_blueprint(child) - app.register_blueprint(parent) - assert ( - client.get("/a").data == b"app_1, app_2, parent_1, parent_2, child_1, child_2" - ) - assert client.get("/b").data == b"child" - - -@pytest.mark.parametrize( - "parent_init, child_init, parent_registration, child_registration", - [ - ("/parent", "/child", None, None), - ("/parent", None, None, "/child"), - (None, None, "/parent", "/child"), - ("/other", "/something", "/parent", "/child"), - ], -) -def test_nesting_url_prefixes( - parent_init, - child_init, - parent_registration, - child_registration, - app, - client, -) -> None: - parent = flask.Blueprint("parent", __name__, url_prefix=parent_init) - child = flask.Blueprint("child", __name__, url_prefix=child_init) - - @child.route("/") - def index(): - return "index" - - parent.register_blueprint(child, url_prefix=child_registration) - app.register_blueprint(parent, url_prefix=parent_registration) - - response = client.get("/parent/child/") - assert response.status_code == 200 - - -def test_nesting_subdomains(app, client) -> None: - app.subdomain_matching = True - app.config["SERVER_NAME"] = "example.test" - client.allow_subdomain_redirects = True - - parent = flask.Blueprint("parent", __name__) - child = flask.Blueprint("child", __name__) - - @child.route("/child/") - def index(): - return "child" - - parent.register_blueprint(child) - app.register_blueprint(parent, subdomain="api") - - response = client.get("/child/", base_url="http://api.example.test") - assert response.status_code == 200 - - -def test_child_and_parent_subdomain(app, client) -> None: - app.subdomain_matching = True - app.config["SERVER_NAME"] = "example.test" - client.allow_subdomain_redirects = True - - parent = flask.Blueprint("parent", __name__) - child = flask.Blueprint("child", __name__, subdomain="api") - - @child.route("/") - def index(): - return "child" - - parent.register_blueprint(child) - app.register_blueprint(parent, subdomain="parent") - - response = client.get("/", base_url="http://api.parent.example.test") - assert response.status_code == 200 - - response = client.get("/", base_url="http://parent.example.test") - assert response.status_code == 404 - - -def test_unique_blueprint_names(app, client) -> None: - bp = flask.Blueprint("bp", __name__) - bp2 = flask.Blueprint("bp", __name__) - - app.register_blueprint(bp) - - with pytest.raises(ValueError): - app.register_blueprint(bp) # same bp, same name, error - - app.register_blueprint(bp, name="again") # same bp, different name, ok - - with pytest.raises(ValueError): - app.register_blueprint(bp2) # different bp, same name, error - - app.register_blueprint(bp2, name="alt") # different bp, different name, ok - - -def test_self_registration(app, client) -> None: - bp = flask.Blueprint("bp", __name__) - with pytest.raises(ValueError): - bp.register_blueprint(bp) - - -def test_blueprint_renaming(app, client) -> None: - bp = flask.Blueprint("bp", __name__) - bp2 = flask.Blueprint("bp2", __name__) - - @bp.get("/") - def index(): - return flask.request.endpoint - - @bp.get("/error") - def error(): - flask.abort(403) - - @bp.errorhandler(403) - def forbidden(_: Exception): - return "Error", 403 - - @bp2.get("/") - def index2(): - return flask.request.endpoint - - bp.register_blueprint(bp2, url_prefix="/a", name="sub") - app.register_blueprint(bp, url_prefix="/a") - app.register_blueprint(bp, url_prefix="/b", name="alt") - - assert client.get("/a/").data == b"bp.index" - assert client.get("/b/").data == b"alt.index" - assert client.get("/a/a/").data == b"bp.sub.index2" - assert client.get("/b/a/").data == b"alt.sub.index2" - assert client.get("/a/error").data == b"Error" - assert client.get("/b/error").data == b"Error" diff --git a/tests/test_cli.py b/tests/test_cli.py deleted file mode 100644 index 2a34088b..00000000 --- a/tests/test_cli.py +++ /dev/null @@ -1,703 +0,0 @@ -# This file was part of Flask-CLI and was modified under the terms of -# its Revised BSD License. Copyright © 2015 CERN. -import importlib.metadata -import os -import platform -import ssl -import sys -import types -from functools import partial -from pathlib import Path - -import click -import pytest -from _pytest.monkeypatch import notset -from click.testing import CliRunner - -from flask import Blueprint -from flask import current_app -from flask import Flask -from flask.cli import AppGroup -from flask.cli import find_best_app -from flask.cli import FlaskGroup -from flask.cli import get_version -from flask.cli import load_dotenv -from flask.cli import locate_app -from flask.cli import NoAppException -from flask.cli import prepare_import -from flask.cli import run_command -from flask.cli import ScriptInfo -from flask.cli import with_appcontext - -cwd = Path.cwd() -test_path = (Path(__file__) / ".." / "test_apps").resolve() - - -@pytest.fixture -def runner(): - return CliRunner() - - -def test_cli_name(test_apps): - """Make sure the CLI object's name is the app's name and not the app itself""" - from cliapp.app import testapp - - assert testapp.cli.name == testapp.name - - -def test_find_best_app(test_apps): - class Module: - app = Flask("appname") - - assert find_best_app(Module) == Module.app - - class Module: - application = Flask("appname") - - assert find_best_app(Module) == Module.application - - class Module: - myapp = Flask("appname") - - assert find_best_app(Module) == Module.myapp - - class Module: - @staticmethod - def create_app(): - return Flask("appname") - - app = find_best_app(Module) - assert isinstance(app, Flask) - assert app.name == "appname" - - class Module: - @staticmethod - def create_app(**kwargs): - return Flask("appname") - - app = find_best_app(Module) - assert isinstance(app, Flask) - assert app.name == "appname" - - class Module: - @staticmethod - def make_app(): - return Flask("appname") - - app = find_best_app(Module) - assert isinstance(app, Flask) - assert app.name == "appname" - - class Module: - myapp = Flask("appname1") - - @staticmethod - def create_app(): - return Flask("appname2") - - assert find_best_app(Module) == Module.myapp - - class Module: - myapp = Flask("appname1") - - @staticmethod - def create_app(): - return Flask("appname2") - - assert find_best_app(Module) == Module.myapp - - class Module: - pass - - pytest.raises(NoAppException, find_best_app, Module) - - class Module: - myapp1 = Flask("appname1") - myapp2 = Flask("appname2") - - pytest.raises(NoAppException, find_best_app, Module) - - class Module: - @staticmethod - def create_app(foo, bar): - return Flask("appname2") - - pytest.raises(NoAppException, find_best_app, Module) - - class Module: - @staticmethod - def create_app(): - raise TypeError("bad bad factory!") - - pytest.raises(TypeError, find_best_app, Module) - - -@pytest.mark.parametrize( - "value,path,result", - ( - ("test", cwd, "test"), - ("test.py", cwd, "test"), - ("a/test", cwd / "a", "test"), - ("test/__init__.py", cwd, "test"), - ("test/__init__", cwd, "test"), - # nested package - ( - test_path / "cliapp" / "inner1" / "__init__", - test_path, - "cliapp.inner1", - ), - ( - test_path / "cliapp" / "inner1" / "inner2", - test_path, - "cliapp.inner1.inner2", - ), - # dotted name - ("test.a.b", cwd, "test.a.b"), - (test_path / "cliapp.app", test_path, "cliapp.app"), - # not a Python file, will be caught during import - (test_path / "cliapp" / "message.txt", test_path, "cliapp.message.txt"), - ), -) -def test_prepare_import(request, value, path, result): - """Expect the correct path to be set and the correct import and app names - to be returned. - - :func:`prepare_exec_for_file` has a side effect where the parent directory - of the given import is added to :data:`sys.path`. This is reset after the - test runs. - """ - original_path = sys.path[:] - - def reset_path(): - sys.path[:] = original_path - - request.addfinalizer(reset_path) - - assert prepare_import(value) == result - assert sys.path[0] == str(path) - - -@pytest.mark.parametrize( - "iname,aname,result", - ( - ("cliapp.app", None, "testapp"), - ("cliapp.app", "testapp", "testapp"), - ("cliapp.factory", None, "app"), - ("cliapp.factory", "create_app", "app"), - ("cliapp.factory", "create_app()", "app"), - ("cliapp.factory", 'create_app2("foo", "bar")', "app2_foo_bar"), - # trailing comma space - ("cliapp.factory", 'create_app2("foo", "bar", )', "app2_foo_bar"), - # strip whitespace - ("cliapp.factory", " create_app () ", "app"), - ), -) -def test_locate_app(test_apps, iname, aname, result): - assert locate_app(iname, aname).name == result - - -@pytest.mark.parametrize( - "iname,aname", - ( - ("notanapp.py", None), - ("cliapp/app", None), - ("cliapp.app", "notanapp"), - # not enough arguments - ("cliapp.factory", 'create_app2("foo")'), - # invalid identifier - ("cliapp.factory", "create_app("), - # no app returned - ("cliapp.factory", "no_app"), - # nested import error - ("cliapp.importerrorapp", None), - # not a Python file - ("cliapp.message.txt", None), - ), -) -def test_locate_app_raises(test_apps, iname, aname): - with pytest.raises(NoAppException): - locate_app(iname, aname) - - -def test_locate_app_suppress_raise(test_apps): - app = locate_app("notanapp.py", None, raise_if_not_found=False) - assert app is None - - # only direct import error is suppressed - with pytest.raises(NoAppException): - locate_app("cliapp.importerrorapp", None, raise_if_not_found=False) - - -def test_get_version(test_apps, capsys): - class MockCtx: - resilient_parsing = False - color = None - - def exit(self): - return - - ctx = MockCtx() - get_version(ctx, None, "test") - out, err = capsys.readouterr() - assert f"Python {platform.python_version()}" in out - assert f"Flask {importlib.metadata.version('flask')}" in out - assert f"Werkzeug {importlib.metadata.version('werkzeug')}" in out - - -def test_scriptinfo(test_apps, monkeypatch): - obj = ScriptInfo(app_import_path="cliapp.app:testapp") - app = obj.load_app() - assert app.name == "testapp" - assert obj.load_app() is app - - # import app with module's absolute path - cli_app_path = str(test_path / "cliapp" / "app.py") - - obj = ScriptInfo(app_import_path=cli_app_path) - app = obj.load_app() - assert app.name == "testapp" - assert obj.load_app() is app - obj = ScriptInfo(app_import_path=f"{cli_app_path}:testapp") - app = obj.load_app() - assert app.name == "testapp" - assert obj.load_app() is app - - def create_app(): - return Flask("createapp") - - obj = ScriptInfo(create_app=create_app) - app = obj.load_app() - assert app.name == "createapp" - assert obj.load_app() is app - - obj = ScriptInfo() - pytest.raises(NoAppException, obj.load_app) - - # import app from wsgi.py in current directory - monkeypatch.chdir(test_path / "helloworld") - obj = ScriptInfo() - app = obj.load_app() - assert app.name == "hello" - - # import app from app.py in current directory - monkeypatch.chdir(test_path / "cliapp") - obj = ScriptInfo() - app = obj.load_app() - assert app.name == "testapp" - - -def test_app_cli_has_app_context(app, runner): - def _param_cb(ctx, param, value): - # current_app should be available in parameter callbacks - return bool(current_app) - - @app.cli.command() - @click.argument("value", callback=_param_cb) - def check(value): - app = click.get_current_context().obj.load_app() - # the loaded app should be the same as current_app - same_app = current_app._get_current_object() is app - return same_app, value - - cli = FlaskGroup(create_app=lambda: app) - result = runner.invoke(cli, ["check", "x"], standalone_mode=False) - assert result.return_value == (True, True) - - -def test_with_appcontext(runner): - @click.command() - @with_appcontext - def testcmd(): - click.echo(current_app.name) - - obj = ScriptInfo(create_app=lambda: Flask("testapp")) - - result = runner.invoke(testcmd, obj=obj) - assert result.exit_code == 0 - assert result.output == "testapp\n" - - -def test_appgroup_app_context(runner): - @click.group(cls=AppGroup) - def cli(): - pass - - @cli.command() - def test(): - click.echo(current_app.name) - - @cli.group() - def subgroup(): - pass - - @subgroup.command() - def test2(): - click.echo(current_app.name) - - obj = ScriptInfo(create_app=lambda: Flask("testappgroup")) - - result = runner.invoke(cli, ["test"], obj=obj) - assert result.exit_code == 0 - assert result.output == "testappgroup\n" - - result = runner.invoke(cli, ["subgroup", "test2"], obj=obj) - assert result.exit_code == 0 - assert result.output == "testappgroup\n" - - -def test_flaskgroup_app_context(runner): - def create_app(): - return Flask("flaskgroup") - - @click.group(cls=FlaskGroup, create_app=create_app) - def cli(**params): - pass - - @cli.command() - def test(): - click.echo(current_app.name) - - result = runner.invoke(cli, ["test"]) - assert result.exit_code == 0 - assert result.output == "flaskgroup\n" - - -@pytest.mark.parametrize("set_debug_flag", (True, False)) -def test_flaskgroup_debug(runner, set_debug_flag): - def create_app(): - app = Flask("flaskgroup") - app.debug = True - return app - - @click.group(cls=FlaskGroup, create_app=create_app, set_debug_flag=set_debug_flag) - def cli(**params): - pass - - @cli.command() - def test(): - click.echo(str(current_app.debug)) - - result = runner.invoke(cli, ["test"]) - assert result.exit_code == 0 - assert result.output == f"{not set_debug_flag}\n" - - -def test_flaskgroup_nested(app, runner): - cli = click.Group("cli") - flask_group = FlaskGroup(name="flask", create_app=lambda: app) - cli.add_command(flask_group) - - @flask_group.command() - def show(): - click.echo(current_app.name) - - result = runner.invoke(cli, ["flask", "show"]) - assert result.output == "flask_test\n" - - -def test_no_command_echo_loading_error(): - from flask.cli import cli - - try: - runner = CliRunner(mix_stderr=False) - except (DeprecationWarning, TypeError): - # Click >= 8.2 - runner = CliRunner() - - result = runner.invoke(cli, ["missing"]) - assert result.exit_code == 2 - assert "FLASK_APP" in result.stderr - assert "Usage:" in result.stderr - - -def test_help_echo_loading_error(): - from flask.cli import cli - - try: - runner = CliRunner(mix_stderr=False) - except (DeprecationWarning, TypeError): - # Click >= 8.2 - runner = CliRunner() - - result = runner.invoke(cli, ["--help"]) - assert result.exit_code == 0 - assert "FLASK_APP" in result.stderr - assert "Usage:" in result.stdout - - -def test_help_echo_exception(): - def create_app(): - raise Exception("oh no") - - cli = FlaskGroup(create_app=create_app) - - try: - runner = CliRunner(mix_stderr=False) - except (DeprecationWarning, TypeError): - # Click >= 8.2 - runner = CliRunner() - - result = runner.invoke(cli, ["--help"]) - assert result.exit_code == 0 - assert "Exception: oh no" in result.stderr - assert "Usage:" in result.stdout - - -class TestRoutes: - @pytest.fixture - def app(self): - app = Flask(__name__) - app.add_url_rule( - "/get_post//", - methods=["GET", "POST"], - endpoint="yyy_get_post", - ) - app.add_url_rule("/zzz_post", methods=["POST"], endpoint="aaa_post") - return app - - @pytest.fixture - def invoke(self, app, runner): - cli = FlaskGroup(create_app=lambda: app) - return partial(runner.invoke, cli) - - def expect_order(self, order, output): - # skip the header and match the start of each row - for expect, line in zip(order, output.splitlines()[2:], strict=False): - # do this instead of startswith for nicer pytest output - assert line[: len(expect)] == expect - - def test_simple(self, invoke): - result = invoke(["routes"]) - assert result.exit_code == 0 - self.expect_order(["aaa_post", "static", "yyy_get_post"], result.output) - - def test_sort(self, app, invoke): - default_output = invoke(["routes"]).output - endpoint_output = invoke(["routes", "-s", "endpoint"]).output - assert default_output == endpoint_output - self.expect_order( - ["static", "yyy_get_post", "aaa_post"], - invoke(["routes", "-s", "methods"]).output, - ) - self.expect_order( - ["yyy_get_post", "static", "aaa_post"], - invoke(["routes", "-s", "rule"]).output, - ) - match_order = [r.endpoint for r in app.url_map.iter_rules()] - self.expect_order(match_order, invoke(["routes", "-s", "match"]).output) - - def test_all_methods(self, invoke): - output = invoke(["routes"]).output - assert "GET, HEAD, OPTIONS, POST" not in output - - output = invoke(["routes", "--all-methods"]).output - assert "GET, HEAD, OPTIONS, POST" in output - - def test_no_routes(self, runner): - app = Flask(__name__, static_folder=None) - cli = FlaskGroup(create_app=lambda: app) - result = runner.invoke(cli, ["routes"]) - assert result.exit_code == 0 - assert "No routes were registered." in result.output - - def test_subdomain(self, runner): - app = Flask(__name__, static_folder=None) - app.add_url_rule("/a", subdomain="a", endpoint="a") - app.add_url_rule("/b", subdomain="b", endpoint="b") - cli = FlaskGroup(create_app=lambda: app) - result = runner.invoke(cli, ["routes"]) - assert result.exit_code == 0 - assert "Subdomain" in result.output - - def test_host(self, runner): - app = Flask(__name__, static_folder=None, host_matching=True) - app.add_url_rule("/a", host="a", endpoint="a") - app.add_url_rule("/b", host="b", endpoint="b") - cli = FlaskGroup(create_app=lambda: app) - result = runner.invoke(cli, ["routes"]) - assert result.exit_code == 0 - assert "Host" in result.output - - -def dotenv_not_available(): - try: - import dotenv # noqa: F401 - except ImportError: - return True - - return False - - -need_dotenv = pytest.mark.skipif( - dotenv_not_available(), reason="dotenv is not installed" -) - - -@need_dotenv -def test_load_dotenv(monkeypatch): - # can't use monkeypatch.delitem since the keys don't exist yet - for item in ("FOO", "BAR", "SPAM", "HAM"): - monkeypatch._setitem.append((os.environ, item, notset)) - - monkeypatch.setenv("EGGS", "3") - monkeypatch.chdir(test_path) - assert load_dotenv() - assert Path.cwd() == test_path - # .flaskenv doesn't overwrite .env - assert os.environ["FOO"] == "env" - # set only in .flaskenv - assert os.environ["BAR"] == "bar" - # set only in .env - assert os.environ["SPAM"] == "1" - # set manually, files don't overwrite - assert os.environ["EGGS"] == "3" - # test env file encoding - assert os.environ["HAM"] == "火腿" - # Non existent file should not load - assert not load_dotenv("non-existent-file", load_defaults=False) - - -@need_dotenv -def test_dotenv_path(monkeypatch): - for item in ("FOO", "BAR", "EGGS"): - monkeypatch._setitem.append((os.environ, item, notset)) - - load_dotenv(test_path / ".flaskenv") - assert Path.cwd() == cwd - assert "FOO" in os.environ - - -def test_dotenv_optional(monkeypatch): - monkeypatch.setitem(sys.modules, "dotenv", None) - monkeypatch.chdir(test_path) - load_dotenv() - assert "FOO" not in os.environ - - -@need_dotenv -def test_disable_dotenv_from_env(monkeypatch, runner): - monkeypatch.chdir(test_path) - monkeypatch.setitem(os.environ, "FLASK_SKIP_DOTENV", "1") - runner.invoke(FlaskGroup()) - assert "FOO" not in os.environ - - -def test_run_cert_path(): - # no key - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", __file__]) - - # no cert - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--key", __file__]) - - # cert specified first - ctx = run_command.make_context("run", ["--cert", __file__, "--key", __file__]) - assert ctx.params["cert"] == (__file__, __file__) - - # key specified first - ctx = run_command.make_context("run", ["--key", __file__, "--cert", __file__]) - assert ctx.params["cert"] == (__file__, __file__) - - -def test_run_cert_adhoc(monkeypatch): - monkeypatch.setitem(sys.modules, "cryptography", None) - - # cryptography not installed - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", "adhoc"]) - - # cryptography installed - monkeypatch.setitem(sys.modules, "cryptography", types.ModuleType("cryptography")) - ctx = run_command.make_context("run", ["--cert", "adhoc"]) - assert ctx.params["cert"] == "adhoc" - - # no key with adhoc - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", "adhoc", "--key", __file__]) - - -def test_run_cert_import(monkeypatch): - monkeypatch.setitem(sys.modules, "not_here", None) - - # ImportError - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", "not_here"]) - - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", "flask"]) - - # SSLContext - ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) - - monkeypatch.setitem(sys.modules, "ssl_context", ssl_context) - ctx = run_command.make_context("run", ["--cert", "ssl_context"]) - assert ctx.params["cert"] is ssl_context - - # no --key with SSLContext - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", "ssl_context", "--key", __file__]) - - -def test_run_cert_no_ssl(monkeypatch): - monkeypatch.setitem(sys.modules, "ssl", None) - - with pytest.raises(click.BadParameter): - run_command.make_context("run", ["--cert", "not_here"]) - - -def test_cli_blueprints(app): - """Test blueprint commands register correctly to the application""" - custom = Blueprint("custom", __name__, cli_group="customized") - nested = Blueprint("nested", __name__) - merged = Blueprint("merged", __name__, cli_group=None) - late = Blueprint("late", __name__) - - @custom.cli.command("custom") - def custom_command(): - click.echo("custom_result") - - @nested.cli.command("nested") - def nested_command(): - click.echo("nested_result") - - @merged.cli.command("merged") - def merged_command(): - click.echo("merged_result") - - @late.cli.command("late") - def late_command(): - click.echo("late_result") - - app.register_blueprint(custom) - app.register_blueprint(nested) - app.register_blueprint(merged) - app.register_blueprint(late, cli_group="late_registration") - - app_runner = app.test_cli_runner() - - result = app_runner.invoke(args=["customized", "custom"]) - assert "custom_result" in result.output - - result = app_runner.invoke(args=["nested", "nested"]) - assert "nested_result" in result.output - - result = app_runner.invoke(args=["merged"]) - assert "merged_result" in result.output - - result = app_runner.invoke(args=["late_registration", "late"]) - assert "late_result" in result.output - - -def test_cli_empty(app): - """If a Blueprint's CLI group is empty, do not register it.""" - bp = Blueprint("blue", __name__, cli_group="blue") - app.register_blueprint(bp) - - result = app.test_cli_runner().invoke(args=["blue", "--help"]) - assert result.exit_code == 2, f"Unexpected success:\n\n{result.output}" - - -def test_run_exclude_patterns(): - ctx = run_command.make_context("run", ["--exclude-patterns", __file__]) - assert ctx.params["exclude_patterns"] == [__file__] diff --git a/tests/test_config.py b/tests/test_config.py deleted file mode 100644 index e5b1906e..00000000 --- a/tests/test_config.py +++ /dev/null @@ -1,250 +0,0 @@ -import json -import os - -import pytest - -import flask - -# config keys used for the TestConfig -TEST_KEY = "foo" -SECRET_KEY = "config" - - -def common_object_test(app): - assert app.secret_key == "config" - assert app.config["TEST_KEY"] == "foo" - assert "TestConfig" not in app.config - - -def test_config_from_pyfile(): - app = flask.Flask(__name__) - app.config.from_pyfile(f"{__file__.rsplit('.', 1)[0]}.py") - common_object_test(app) - - -def test_config_from_object(): - app = flask.Flask(__name__) - app.config.from_object(__name__) - common_object_test(app) - - -def test_config_from_file_json(): - app = flask.Flask(__name__) - current_dir = os.path.dirname(os.path.abspath(__file__)) - app.config.from_file(os.path.join(current_dir, "static", "config.json"), json.load) - common_object_test(app) - - -def test_config_from_file_toml(): - tomllib = pytest.importorskip("tomllib", reason="tomllib added in 3.11") - app = flask.Flask(__name__) - current_dir = os.path.dirname(os.path.abspath(__file__)) - app.config.from_file( - os.path.join(current_dir, "static", "config.toml"), tomllib.load, text=False - ) - common_object_test(app) - - -def test_from_prefixed_env(monkeypatch): - monkeypatch.setenv("FLASK_STRING", "value") - monkeypatch.setenv("FLASK_BOOL", "true") - monkeypatch.setenv("FLASK_INT", "1") - monkeypatch.setenv("FLASK_FLOAT", "1.2") - monkeypatch.setenv("FLASK_LIST", "[1, 2]") - monkeypatch.setenv("FLASK_DICT", '{"k": "v"}') - monkeypatch.setenv("NOT_FLASK_OTHER", "other") - - app = flask.Flask(__name__) - app.config.from_prefixed_env() - - assert app.config["STRING"] == "value" - assert app.config["BOOL"] is True - assert app.config["INT"] == 1 - assert app.config["FLOAT"] == 1.2 - assert app.config["LIST"] == [1, 2] - assert app.config["DICT"] == {"k": "v"} - assert "OTHER" not in app.config - - -def test_from_prefixed_env_custom_prefix(monkeypatch): - monkeypatch.setenv("FLASK_A", "a") - monkeypatch.setenv("NOT_FLASK_A", "b") - - app = flask.Flask(__name__) - app.config.from_prefixed_env("NOT_FLASK") - - assert app.config["A"] == "b" - - -def test_from_prefixed_env_nested(monkeypatch): - monkeypatch.setenv("FLASK_EXIST__ok", "other") - monkeypatch.setenv("FLASK_EXIST__inner__ik", "2") - monkeypatch.setenv("FLASK_EXIST__new__more", '{"k": false}') - monkeypatch.setenv("FLASK_NEW__K", "v") - - app = flask.Flask(__name__) - app.config["EXIST"] = {"ok": "value", "flag": True, "inner": {"ik": 1}} - app.config.from_prefixed_env() - - if os.name != "nt": - assert app.config["EXIST"] == { - "ok": "other", - "flag": True, - "inner": {"ik": 2}, - "new": {"more": {"k": False}}, - } - else: - # Windows env var keys are always uppercase. - assert app.config["EXIST"] == { - "ok": "value", - "OK": "other", - "flag": True, - "inner": {"ik": 1}, - "INNER": {"IK": 2}, - "NEW": {"MORE": {"k": False}}, - } - - assert app.config["NEW"] == {"K": "v"} - - -def test_config_from_mapping(): - app = flask.Flask(__name__) - app.config.from_mapping({"SECRET_KEY": "config", "TEST_KEY": "foo"}) - common_object_test(app) - - app = flask.Flask(__name__) - app.config.from_mapping([("SECRET_KEY", "config"), ("TEST_KEY", "foo")]) - common_object_test(app) - - app = flask.Flask(__name__) - app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo") - common_object_test(app) - - app = flask.Flask(__name__) - app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo", skip_key="skip") - common_object_test(app) - - app = flask.Flask(__name__) - with pytest.raises(TypeError): - app.config.from_mapping({}, {}) - - -def test_config_from_class(): - class Base: - TEST_KEY = "foo" - - class Test(Base): - SECRET_KEY = "config" - - app = flask.Flask(__name__) - app.config.from_object(Test) - common_object_test(app) - - -def test_config_from_envvar(monkeypatch): - monkeypatch.setattr("os.environ", {}) - app = flask.Flask(__name__) - - with pytest.raises(RuntimeError) as e: - app.config.from_envvar("FOO_SETTINGS") - - assert "'FOO_SETTINGS' is not set" in str(e.value) - assert not app.config.from_envvar("FOO_SETTINGS", silent=True) - - monkeypatch.setattr( - "os.environ", {"FOO_SETTINGS": f"{__file__.rsplit('.', 1)[0]}.py"} - ) - assert app.config.from_envvar("FOO_SETTINGS") - common_object_test(app) - - -def test_config_from_envvar_missing(monkeypatch): - monkeypatch.setattr("os.environ", {"FOO_SETTINGS": "missing.cfg"}) - app = flask.Flask(__name__) - with pytest.raises(IOError) as e: - app.config.from_envvar("FOO_SETTINGS") - msg = str(e.value) - assert msg.startswith( - "[Errno 2] Unable to load configuration file (No such file or directory):" - ) - assert msg.endswith("missing.cfg'") - assert not app.config.from_envvar("FOO_SETTINGS", silent=True) - - -def test_config_missing(): - app = flask.Flask(__name__) - with pytest.raises(IOError) as e: - app.config.from_pyfile("missing.cfg") - msg = str(e.value) - assert msg.startswith( - "[Errno 2] Unable to load configuration file (No such file or directory):" - ) - assert msg.endswith("missing.cfg'") - assert not app.config.from_pyfile("missing.cfg", silent=True) - - -def test_config_missing_file(): - app = flask.Flask(__name__) - with pytest.raises(IOError) as e: - app.config.from_file("missing.json", load=json.load) - msg = str(e.value) - assert msg.startswith( - "[Errno 2] Unable to load configuration file (No such file or directory):" - ) - assert msg.endswith("missing.json'") - assert not app.config.from_file("missing.json", load=json.load, silent=True) - - -def test_custom_config_class(): - class Config(flask.Config): - pass - - class Flask(flask.Flask): - config_class = Config - - app = Flask(__name__) - assert isinstance(app.config, Config) - app.config.from_object(__name__) - common_object_test(app) - - -def test_session_lifetime(): - app = flask.Flask(__name__) - app.config["PERMANENT_SESSION_LIFETIME"] = 42 - assert app.permanent_session_lifetime.seconds == 42 - - -def test_get_namespace(): - app = flask.Flask(__name__) - app.config["FOO_OPTION_1"] = "foo option 1" - app.config["FOO_OPTION_2"] = "foo option 2" - app.config["BAR_STUFF_1"] = "bar stuff 1" - app.config["BAR_STUFF_2"] = "bar stuff 2" - foo_options = app.config.get_namespace("FOO_") - assert 2 == len(foo_options) - assert "foo option 1" == foo_options["option_1"] - assert "foo option 2" == foo_options["option_2"] - bar_options = app.config.get_namespace("BAR_", lowercase=False) - assert 2 == len(bar_options) - assert "bar stuff 1" == bar_options["STUFF_1"] - assert "bar stuff 2" == bar_options["STUFF_2"] - foo_options = app.config.get_namespace("FOO_", trim_namespace=False) - assert 2 == len(foo_options) - assert "foo option 1" == foo_options["foo_option_1"] - assert "foo option 2" == foo_options["foo_option_2"] - bar_options = app.config.get_namespace( - "BAR_", lowercase=False, trim_namespace=False - ) - assert 2 == len(bar_options) - assert "bar stuff 1" == bar_options["BAR_STUFF_1"] - assert "bar stuff 2" == bar_options["BAR_STUFF_2"] - - -@pytest.mark.parametrize("encoding", ["utf-8", "iso-8859-15", "latin-1"]) -def test_from_pyfile_weird_encoding(tmp_path, encoding): - f = tmp_path / "my_config.py" - f.write_text(f'# -*- coding: {encoding} -*-\nTEST_VALUE = "föö"\n', encoding) - app = flask.Flask(__name__) - app.config.from_pyfile(os.fspath(f)) - value = app.config["TEST_VALUE"] - assert value == "föö" diff --git a/tests/test_converters.py b/tests/test_converters.py deleted file mode 100644 index d94a7658..00000000 --- a/tests/test_converters.py +++ /dev/null @@ -1,42 +0,0 @@ -from werkzeug.routing import BaseConverter - -from flask import request -from flask import session -from flask import url_for - - -def test_custom_converters(app, client): - class ListConverter(BaseConverter): - def to_python(self, value): - return value.split(",") - - def to_url(self, value): - base_to_url = super().to_url - return ",".join(base_to_url(x) for x in value) - - app.url_map.converters["list"] = ListConverter - - @app.route("/") - def index(args): - return "|".join(args) - - assert client.get("/1,2,3").data == b"1|2|3" - - with app.test_request_context(): - assert url_for("index", args=[4, 5, 6]) == "/4,5,6" - - -def test_context_available(app, client): - class ContextConverter(BaseConverter): - def to_python(self, value): - assert request is not None - assert session is not None - return value - - app.url_map.converters["ctx"] = ContextConverter - - @app.get("/") - def index(name): - return name - - assert client.get("/admin").data == b"admin" diff --git a/tests/test_helpers.py b/tests/test_helpers.py deleted file mode 100644 index c195b028..00000000 --- a/tests/test_helpers.py +++ /dev/null @@ -1,377 +0,0 @@ -import io -import os - -import pytest -import werkzeug.exceptions - -import flask -from flask.helpers import get_debug_flag - - -class FakePath: - """Fake object to represent a ``PathLike object``. - - This represents a ``pathlib.Path`` object in python 3. - See: https://www.python.org/dev/peps/pep-0519/ - """ - - def __init__(self, path): - self.path = path - - def __fspath__(self): - return self.path - - -class PyBytesIO: - def __init__(self, *args, **kwargs): - self._io = io.BytesIO(*args, **kwargs) - - def __getattr__(self, name): - return getattr(self._io, name) - - -class TestSendfile: - def test_send_file(self, app, req_ctx): - with app.open_resource("static/index.html") as f: - expect = f.read() - - with flask.send_file("static/index.html") as rv: - assert rv.direct_passthrough - assert rv.mimetype == "text/html" - rv.direct_passthrough = False - assert rv.data == expect - - def test_static_file(self, app, req_ctx): - # Default max_age is None. - - # Test with static file handler. - with app.send_static_file("index.html") as rv: - assert rv.cache_control.max_age is None - - # Test with direct use of send_file. - with flask.send_file("static/index.html") as rv: - assert rv.cache_control.max_age is None - - app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 3600 - - # Test with static file handler. - with app.send_static_file("index.html") as rv: - assert rv.cache_control.max_age == 3600 - - # Test with direct use of send_file. - with flask.send_file("static/index.html") as rv: - assert rv.cache_control.max_age == 3600 - - # Test with pathlib.Path. - with app.send_static_file(FakePath("index.html")) as rv: - assert rv.cache_control.max_age == 3600 - - class StaticFileApp(flask.Flask): - def get_send_file_max_age(self, filename): - return 10 - - app = StaticFileApp(__name__) - - with app.test_request_context(): - # Test with static file handler. - with app.send_static_file("index.html") as rv: - assert rv.cache_control.max_age == 10 - - # Test with direct use of send_file. - with flask.send_file("static/index.html") as rv: - assert rv.cache_control.max_age == 10 - - def test_send_from_directory(self, app, req_ctx): - app.root_path = os.path.join( - os.path.dirname(__file__), "test_apps", "subdomaintestmodule" - ) - - with flask.send_from_directory("static", "hello.txt") as rv: - rv.direct_passthrough = False - assert rv.data.strip() == b"Hello Subdomain" - - -class TestUrlFor: - def test_url_for_with_anchor(self, app, req_ctx): - @app.route("/") - def index(): - return "42" - - assert flask.url_for("index", _anchor="x y") == "/#x%20y" - - def test_url_for_with_scheme(self, app, req_ctx): - @app.route("/") - def index(): - return "42" - - assert ( - flask.url_for("index", _external=True, _scheme="https") - == "https://localhost/" - ) - - def test_url_for_with_scheme_not_external(self, app, req_ctx): - app.add_url_rule("/", endpoint="index") - - # Implicit external with scheme. - url = flask.url_for("index", _scheme="https") - assert url == "https://localhost/" - - # Error when external=False with scheme - with pytest.raises(ValueError): - flask.url_for("index", _scheme="https", _external=False) - - def test_url_for_with_alternating_schemes(self, app, req_ctx): - @app.route("/") - def index(): - return "42" - - assert flask.url_for("index", _external=True) == "http://localhost/" - assert ( - flask.url_for("index", _external=True, _scheme="https") - == "https://localhost/" - ) - assert flask.url_for("index", _external=True) == "http://localhost/" - - def test_url_with_method(self, app, req_ctx): - from flask.views import MethodView - - class MyView(MethodView): - def get(self, id=None): - if id is None: - return "List" - return f"Get {id:d}" - - def post(self): - return "Create" - - myview = MyView.as_view("myview") - app.add_url_rule("/myview/", methods=["GET"], view_func=myview) - app.add_url_rule("/myview/", methods=["GET"], view_func=myview) - app.add_url_rule("/myview/create", methods=["POST"], view_func=myview) - - assert flask.url_for("myview", _method="GET") == "/myview/" - assert flask.url_for("myview", id=42, _method="GET") == "/myview/42" - assert flask.url_for("myview", _method="POST") == "/myview/create" - - def test_url_for_with_self(self, app, req_ctx): - @app.route("/") - def index(self): - return "42" - - assert flask.url_for("index", self="2") == "/2" - - -def test_redirect_no_app(): - response = flask.redirect("https://localhost", 307) - assert response.location == "https://localhost" - assert response.status_code == 307 - - -def test_redirect_with_app(app): - def redirect(location, code=302): - raise ValueError - - app.redirect = redirect - - with app.app_context(), pytest.raises(ValueError): - flask.redirect("other") - - -def test_abort_no_app(): - with pytest.raises(werkzeug.exceptions.Unauthorized): - flask.abort(401) - - with pytest.raises(LookupError): - flask.abort(900) - - -def test_app_aborter_class(): - class MyAborter(werkzeug.exceptions.Aborter): - pass - - class MyFlask(flask.Flask): - aborter_class = MyAborter - - app = MyFlask(__name__) - assert isinstance(app.aborter, MyAborter) - - -def test_abort_with_app(app): - class My900Error(werkzeug.exceptions.HTTPException): - code = 900 - - app.aborter.mapping[900] = My900Error - - with app.app_context(), pytest.raises(My900Error): - flask.abort(900) - - -class TestNoImports: - """Test Flasks are created without import. - - Avoiding ``__import__`` helps create Flask instances where there are errors - at import time. Those runtime errors will be apparent to the user soon - enough, but tools which build Flask instances meta-programmatically benefit - from a Flask which does not ``__import__``. Instead of importing to - retrieve file paths or metadata on a module or package, use the pkgutil and - imp modules in the Python standard library. - """ - - def test_name_with_import_error(self, modules_tmp_path): - (modules_tmp_path / "importerror.py").write_text("raise NotImplementedError()") - try: - flask.Flask("importerror") - except NotImplementedError: - AssertionError("Flask(import_name) is importing import_name.") - - -class TestStreaming: - def test_streaming_with_context(self, app, client): - @app.route("/") - def index(): - def generate(): - yield "Hello " - yield flask.request.args["name"] - yield "!" - - return flask.Response(flask.stream_with_context(generate())) - - rv = client.get("/?name=World") - assert rv.data == b"Hello World!" - - def test_streaming_with_context_as_decorator(self, app, client): - @app.route("/") - def index(): - @flask.stream_with_context - def generate(hello): - yield hello - yield flask.request.args["name"] - yield "!" - - return flask.Response(generate("Hello ")) - - rv = client.get("/?name=World") - assert rv.data == b"Hello World!" - - def test_streaming_with_context_and_custom_close(self, app, client): - called = [] - - class Wrapper: - def __init__(self, gen): - self._gen = gen - - def __iter__(self): - return self - - def close(self): - called.append(42) - - def __next__(self): - return next(self._gen) - - next = __next__ - - @app.route("/") - def index(): - def generate(): - yield "Hello " - yield flask.request.args["name"] - yield "!" - - return flask.Response(flask.stream_with_context(Wrapper(generate()))) - - rv = client.get("/?name=World") - assert rv.data == b"Hello World!" - assert called == [42] - - def test_stream_keeps_session(self, app, client): - @app.route("/") - def index(): - flask.session["test"] = "flask" - - @flask.stream_with_context - def gen(): - yield flask.session["test"] - - return flask.Response(gen()) - - rv = client.get("/") - assert rv.data == b"flask" - - def test_async_view(self, app, client): - @app.route("/") - async def index(): - flask.session["test"] = "flask" - - @flask.stream_with_context - def gen(): - yield flask.session["test"] - - return flask.Response(gen()) - - # response is closed without reading stream - client.get().close() - - # response stream is read - with client.get() as rv: - assert rv.text == "flask" - - # same as above, but with client context preservation - with client: - client.get().close() - - with client, client.get() as rv: - assert rv.text == "flask" - - -class TestHelpers: - @pytest.mark.parametrize( - ("debug", "expect"), - [ - ("", False), - ("0", False), - ("False", False), - ("No", False), - ("True", True), - ], - ) - def test_get_debug_flag(self, monkeypatch, debug, expect): - monkeypatch.setenv("FLASK_DEBUG", debug) - assert get_debug_flag() == expect - - def test_make_response(self): - app = flask.Flask(__name__) - with app.test_request_context(): - rv = flask.helpers.make_response() - assert rv.status_code == 200 - assert rv.mimetype == "text/html" - - rv = flask.helpers.make_response("Hello") - assert rv.status_code == 200 - assert rv.data == b"Hello" - assert rv.mimetype == "text/html" - - -@pytest.mark.parametrize("mode", ("r", "rb", "rt")) -def test_open_resource(mode): - app = flask.Flask(__name__) - - with app.open_resource("static/index.html", mode) as f: - assert "

Hello World!

" in str(f.read()) - - -@pytest.mark.parametrize("mode", ("w", "x", "a", "r+")) -def test_open_resource_exceptions(mode): - app = flask.Flask(__name__) - - with pytest.raises(ValueError): - app.open_resource("static/index.html", mode) - - -@pytest.mark.parametrize("encoding", ("utf-8", "utf-16-le")) -def test_open_resource_with_encoding(tmp_path, encoding): - app = flask.Flask(__name__, root_path=os.fspath(tmp_path)) - (tmp_path / "test").write_text("test", encoding=encoding) - - with app.open_resource("test", mode="rt", encoding=encoding) as f: - assert f.read() == "test" diff --git a/tests/test_instance_config.py b/tests/test_instance_config.py deleted file mode 100644 index 835a8784..00000000 --- a/tests/test_instance_config.py +++ /dev/null @@ -1,111 +0,0 @@ -import os - -import pytest - -import flask - - -def test_explicit_instance_paths(modules_tmp_path): - with pytest.raises(ValueError, match=".*must be absolute"): - flask.Flask(__name__, instance_path="instance") - - app = flask.Flask(__name__, instance_path=os.fspath(modules_tmp_path)) - assert app.instance_path == os.fspath(modules_tmp_path) - - -def test_uninstalled_module_paths(modules_tmp_path, purge_module): - (modules_tmp_path / "config_module_app.py").write_text( - "import os\n" - "import flask\n" - "here = os.path.abspath(os.path.dirname(__file__))\n" - "app = flask.Flask(__name__)\n" - ) - purge_module("config_module_app") - - from config_module_app import app - - assert app.instance_path == os.fspath(modules_tmp_path / "instance") - - -def test_uninstalled_package_paths(modules_tmp_path, purge_module): - app = modules_tmp_path / "config_package_app" - app.mkdir() - (app / "__init__.py").write_text( - "import os\n" - "import flask\n" - "here = os.path.abspath(os.path.dirname(__file__))\n" - "app = flask.Flask(__name__)\n" - ) - purge_module("config_package_app") - - from config_package_app import app - - assert app.instance_path == os.fspath(modules_tmp_path / "instance") - - -def test_uninstalled_namespace_paths(tmp_path, monkeypatch, purge_module): - def create_namespace(package): - project = tmp_path / f"project-{package}" - monkeypatch.syspath_prepend(os.fspath(project)) - ns = project / "namespace" / package - ns.mkdir(parents=True) - (ns / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n") - return project - - _ = create_namespace("package1") - project2 = create_namespace("package2") - purge_module("namespace.package2") - purge_module("namespace") - - from namespace.package2 import app - - assert app.instance_path == os.fspath(project2 / "instance") - - -def test_installed_module_paths( - modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages -): - (site_packages / "site_app.py").write_text( - "import flask\napp = flask.Flask(__name__)\n" - ) - purge_module("site_app") - - from site_app import app - - assert app.instance_path == os.fspath( - modules_tmp_path / "var" / "site_app-instance" - ) - - -def test_installed_package_paths( - modules_tmp_path, modules_tmp_path_prefix, purge_module, monkeypatch -): - installed_path = modules_tmp_path / "path" - installed_path.mkdir() - monkeypatch.syspath_prepend(installed_path) - - app = installed_path / "installed_package" - app.mkdir() - (app / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n") - purge_module("installed_package") - - from installed_package import app - - assert app.instance_path == os.fspath( - modules_tmp_path / "var" / "installed_package-instance" - ) - - -def test_prefix_package_paths( - modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages -): - app = site_packages / "site_package" - app.mkdir() - (app / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n") - purge_module("site_package") - - import site_package - - assert site_package.app.instance_path == os.fspath( - modules_tmp_path / "var" / "site_package-instance" - ) diff --git a/tests/test_json.py b/tests/test_json.py deleted file mode 100644 index 1e2b27dc..00000000 --- a/tests/test_json.py +++ /dev/null @@ -1,346 +0,0 @@ -import datetime -import decimal -import io -import uuid - -import pytest -from werkzeug.http import http_date - -import flask -from flask import json -from flask.json.provider import DefaultJSONProvider - - -@pytest.mark.parametrize("debug", (True, False)) -def test_bad_request_debug_message(app, client, debug): - app.config["DEBUG"] = debug - app.config["TRAP_BAD_REQUEST_ERRORS"] = False - - @app.route("/json", methods=["POST"]) - def post_json(): - flask.request.get_json() - return None - - rv = client.post("/json", data=None, content_type="application/json") - assert rv.status_code == 400 - contains = b"Failed to decode JSON object" in rv.data - assert contains == debug - - -def test_json_bad_requests(app, client): - @app.route("/json", methods=["POST"]) - def return_json(): - return flask.jsonify(foo=str(flask.request.get_json())) - - rv = client.post("/json", data="malformed", content_type="application/json") - assert rv.status_code == 400 - - -def test_json_custom_mimetypes(app, client): - @app.route("/json", methods=["POST"]) - def return_json(): - return flask.request.get_json() - - rv = client.post("/json", data='"foo"', content_type="application/x+json") - assert rv.data == b"foo" - - -@pytest.mark.parametrize( - "test_value,expected", [(True, '"\\u2603"'), (False, '"\u2603"')] -) -def test_json_as_unicode(test_value, expected, app, app_ctx): - app.json.ensure_ascii = test_value - rv = app.json.dumps("\N{SNOWMAN}") - assert rv == expected - - -def test_json_dump_to_file(app, app_ctx): - test_data = {"name": "Flask"} - out = io.StringIO() - - flask.json.dump(test_data, out) - out.seek(0) - rv = flask.json.load(out) - assert rv == test_data - - -@pytest.mark.parametrize( - "test_value", [0, -1, 1, 23, 3.14, "s", "longer string", True, False, None] -) -def test_jsonify_basic_types(test_value, app, client): - url = "/jsonify_basic_types" - app.add_url_rule(url, url, lambda x=test_value: flask.jsonify(x)) - rv = client.get(url) - assert rv.mimetype == "application/json" - assert flask.json.loads(rv.data) == test_value - - -def test_jsonify_dicts(app, client): - d = { - "a": 0, - "b": 23, - "c": 3.14, - "d": "t", - "e": "Hi", - "f": True, - "g": False, - "h": ["test list", 10, False], - "i": {"test": "dict"}, - } - - @app.route("/kw") - def return_kwargs(): - return flask.jsonify(**d) - - @app.route("/dict") - def return_dict(): - return flask.jsonify(d) - - for url in "/kw", "/dict": - rv = client.get(url) - assert rv.mimetype == "application/json" - assert flask.json.loads(rv.data) == d - - -def test_jsonify_arrays(app, client): - """Test jsonify of lists and args unpacking.""" - a_list = [ - 0, - 42, - 3.14, - "t", - "hello", - True, - False, - ["test list", 2, False], - {"test": "dict"}, - ] - - @app.route("/args_unpack") - def return_args_unpack(): - return flask.jsonify(*a_list) - - @app.route("/array") - def return_array(): - return flask.jsonify(a_list) - - for url in "/args_unpack", "/array": - rv = client.get(url) - assert rv.mimetype == "application/json" - assert flask.json.loads(rv.data) == a_list - - -@pytest.mark.parametrize( - "value", [datetime.datetime(1973, 3, 11, 6, 30, 45), datetime.date(1975, 1, 5)] -) -def test_jsonify_datetime(app, client, value): - @app.route("/") - def index(): - return flask.jsonify(value=value) - - r = client.get() - assert r.json["value"] == http_date(value) - - -class FixedOffset(datetime.tzinfo): - """Fixed offset in hours east from UTC. - - This is a slight adaptation of the ``FixedOffset`` example found in - https://docs.python.org/2.7/library/datetime.html. - """ - - def __init__(self, hours, name): - self.__offset = datetime.timedelta(hours=hours) - self.__name = name - - def utcoffset(self, dt): - return self.__offset - - def tzname(self, dt): - return self.__name - - def dst(self, dt): - return datetime.timedelta() - - -@pytest.mark.parametrize("tz", (("UTC", 0), ("PST", -8), ("KST", 9))) -def test_jsonify_aware_datetimes(tz): - """Test if aware datetime.datetime objects are converted into GMT.""" - tzinfo = FixedOffset(hours=tz[1], name=tz[0]) - dt = datetime.datetime(2017, 1, 1, 12, 34, 56, tzinfo=tzinfo) - gmt = FixedOffset(hours=0, name="GMT") - expected = dt.astimezone(gmt).strftime('"%a, %d %b %Y %H:%M:%S %Z"') - assert flask.json.dumps(dt) == expected - - -def test_jsonify_uuid_types(app, client): - """Test jsonify with uuid.UUID types""" - - test_uuid = uuid.UUID(bytes=b"\xde\xad\xbe\xef" * 4) - url = "/uuid_test" - app.add_url_rule(url, url, lambda: flask.jsonify(x=test_uuid)) - - rv = client.get(url) - - rv_x = flask.json.loads(rv.data)["x"] - assert rv_x == str(test_uuid) - rv_uuid = uuid.UUID(rv_x) - assert rv_uuid == test_uuid - - -def test_json_decimal(): - rv = flask.json.dumps(decimal.Decimal("0.003")) - assert rv == '"0.003"' - - -def test_json_attr(app, client): - @app.route("/add", methods=["POST"]) - def add(): - json = flask.request.get_json() - return str(json["a"] + json["b"]) - - rv = client.post( - "/add", - data=flask.json.dumps({"a": 1, "b": 2}), - content_type="application/json", - ) - assert rv.data == b"3" - - -def test_tojson_filter(app, req_ctx): - # The tojson filter is tested in Jinja, this confirms that it's - # using Flask's dumps. - rv = flask.render_template_string( - "const data = {{ data|tojson }};", - data={"name": "", "time": datetime.datetime(2021, 2, 1, 7, 15)}, - ) - assert rv == ( - 'const data = {"name": "\\u003c/script\\u003e",' - ' "time": "Mon, 01 Feb 2021 07:15:00 GMT"};' - ) - - -def test_json_customization(app, client): - class X: # noqa: B903, for Python2 compatibility - def __init__(self, val): - self.val = val - - def default(o): - if isinstance(o, X): - return f"<{o.val}>" - - return DefaultJSONProvider.default(o) - - class CustomProvider(DefaultJSONProvider): - def object_hook(self, obj): - if len(obj) == 1 and "_foo" in obj: - return X(obj["_foo"]) - - return obj - - def loads(self, s, **kwargs): - kwargs.setdefault("object_hook", self.object_hook) - return super().loads(s, **kwargs) - - app.json = CustomProvider(app) - app.json.default = default - - @app.route("/", methods=["POST"]) - def index(): - return flask.json.dumps(flask.request.get_json()["x"]) - - rv = client.post( - "/", - data=flask.json.dumps({"x": {"_foo": 42}}), - content_type="application/json", - ) - assert rv.data == b'"<42>"' - - -def _has_encoding(name): - try: - import codecs - - codecs.lookup(name) - return True - except LookupError: - return False - - -def test_json_key_sorting(app, client): - app.debug = True - assert app.json.sort_keys - d = dict.fromkeys(range(20), "foo") - - @app.route("/") - def index(): - return flask.jsonify(values=d) - - rv = client.get("/") - lines = [x.strip() for x in rv.data.strip().decode("utf-8").splitlines()] - sorted_by_str = [ - "{", - '"values": {', - '"0": "foo",', - '"1": "foo",', - '"10": "foo",', - '"11": "foo",', - '"12": "foo",', - '"13": "foo",', - '"14": "foo",', - '"15": "foo",', - '"16": "foo",', - '"17": "foo",', - '"18": "foo",', - '"19": "foo",', - '"2": "foo",', - '"3": "foo",', - '"4": "foo",', - '"5": "foo",', - '"6": "foo",', - '"7": "foo",', - '"8": "foo",', - '"9": "foo"', - "}", - "}", - ] - sorted_by_int = [ - "{", - '"values": {', - '"0": "foo",', - '"1": "foo",', - '"2": "foo",', - '"3": "foo",', - '"4": "foo",', - '"5": "foo",', - '"6": "foo",', - '"7": "foo",', - '"8": "foo",', - '"9": "foo",', - '"10": "foo",', - '"11": "foo",', - '"12": "foo",', - '"13": "foo",', - '"14": "foo",', - '"15": "foo",', - '"16": "foo",', - '"17": "foo",', - '"18": "foo",', - '"19": "foo"', - "}", - "}", - ] - - try: - assert lines == sorted_by_int - except AssertionError: - assert lines == sorted_by_str - - -def test_html_method(): - class ObjectWithHTML: - def __html__(self): - return "

test

" - - result = json.dumps(ObjectWithHTML()) - assert result == '"

test

"' diff --git a/tests/test_json_tag.py b/tests/test_json_tag.py deleted file mode 100644 index 677160a6..00000000 --- a/tests/test_json_tag.py +++ /dev/null @@ -1,86 +0,0 @@ -from datetime import datetime -from datetime import timezone -from uuid import uuid4 - -import pytest -from markupsafe import Markup - -from flask.json.tag import JSONTag -from flask.json.tag import TaggedJSONSerializer - - -@pytest.mark.parametrize( - "data", - ( - {" t": (1, 2, 3)}, - {" t__": b"a"}, - {" di": " di"}, - {"x": (1, 2, 3), "y": 4}, - (1, 2, 3), - [(1, 2, 3)], - b"\xff", - Markup(""), - uuid4(), - datetime.now(tz=timezone.utc).replace(microsecond=0), - ), -) -def test_dump_load_unchanged(data): - s = TaggedJSONSerializer() - assert s.loads(s.dumps(data)) == data - - -def test_duplicate_tag(): - class TagDict(JSONTag): - key = " d" - - s = TaggedJSONSerializer() - pytest.raises(KeyError, s.register, TagDict) - s.register(TagDict, force=True, index=0) - assert isinstance(s.tags[" d"], TagDict) - assert isinstance(s.order[0], TagDict) - - -def test_custom_tag(): - class Foo: # noqa: B903, for Python2 compatibility - def __init__(self, data): - self.data = data - - class TagFoo(JSONTag): - __slots__ = () - key = " f" - - def check(self, value): - return isinstance(value, Foo) - - def to_json(self, value): - return self.serializer.tag(value.data) - - def to_python(self, value): - return Foo(value) - - s = TaggedJSONSerializer() - s.register(TagFoo) - assert s.loads(s.dumps(Foo("bar"))).data == "bar" - - -def test_tag_interface(): - t = JSONTag(None) - pytest.raises(NotImplementedError, t.check, None) - pytest.raises(NotImplementedError, t.to_json, None) - pytest.raises(NotImplementedError, t.to_python, None) - - -def test_tag_order(): - class Tag1(JSONTag): - key = " 1" - - class Tag2(JSONTag): - key = " 2" - - s = TaggedJSONSerializer() - - s.register(Tag1, index=-1) - assert isinstance(s.order[-2], Tag1) - - s.register(Tag2, index=None) - assert isinstance(s.order[-1], Tag2) diff --git a/tests/test_logging.py b/tests/test_logging.py deleted file mode 100644 index a5f04636..00000000 --- a/tests/test_logging.py +++ /dev/null @@ -1,98 +0,0 @@ -import logging -import sys -from io import StringIO - -import pytest - -from flask.logging import default_handler -from flask.logging import has_level_handler -from flask.logging import wsgi_errors_stream - - -@pytest.fixture(autouse=True) -def reset_logging(pytestconfig): - root_handlers = logging.root.handlers[:] - logging.root.handlers = [] - root_level = logging.root.level - - logger = logging.getLogger("flask_test") - logger.handlers = [] - logger.setLevel(logging.NOTSET) - - logging_plugin = pytestconfig.pluginmanager.unregister(name="logging-plugin") - - yield - - logging.root.handlers[:] = root_handlers - logging.root.setLevel(root_level) - - logger.handlers = [] - logger.setLevel(logging.NOTSET) - - if logging_plugin: - pytestconfig.pluginmanager.register(logging_plugin, "logging-plugin") - - -def test_logger(app): - assert app.logger.name == "flask_test" - assert app.logger.level == logging.NOTSET - assert app.logger.handlers == [default_handler] - - -def test_logger_debug(app): - app.debug = True - assert app.logger.level == logging.DEBUG - assert app.logger.handlers == [default_handler] - - -def test_existing_handler(app): - logging.root.addHandler(logging.StreamHandler()) - assert app.logger.level == logging.NOTSET - assert not app.logger.handlers - - -def test_wsgi_errors_stream(app, client): - @app.route("/") - def index(): - app.logger.error("test") - return "" - - stream = StringIO() - client.get("/", errors_stream=stream) - assert "ERROR in test_logging: test" in stream.getvalue() - - assert wsgi_errors_stream._get_current_object() is sys.stderr - - with app.test_request_context(errors_stream=stream): - assert wsgi_errors_stream._get_current_object() is stream - - -def test_has_level_handler(): - logger = logging.getLogger("flask.app") - assert not has_level_handler(logger) - - handler = logging.StreamHandler() - logging.root.addHandler(handler) - assert has_level_handler(logger) - - logger.propagate = False - assert not has_level_handler(logger) - logger.propagate = True - - handler.setLevel(logging.ERROR) - assert not has_level_handler(logger) - - -def test_log_view_exception(app, client): - @app.route("/") - def index(): - raise Exception("test") - - app.testing = False - stream = StringIO() - rv = client.get("/", errors_stream=stream) - assert rv.status_code == 500 - assert rv.data - err = stream.getvalue() - assert "Exception on / [GET]" in err - assert "Exception: test" in err diff --git a/tests/test_regression.py b/tests/test_regression.py deleted file mode 100644 index 0ddcf972..00000000 --- a/tests/test_regression.py +++ /dev/null @@ -1,30 +0,0 @@ -import flask - - -def test_aborting(app): - class Foo(Exception): - whatever = 42 - - @app.errorhandler(Foo) - def handle_foo(e): - return str(e.whatever) - - @app.route("/") - def index(): - raise flask.abort(flask.redirect(flask.url_for("test"))) - - @app.route("/test") - def test(): - raise Foo() - - with app.test_client() as c: - rv = c.get("/") - location_parts = rv.headers["Location"].rpartition("/") - - if location_parts[0]: - # For older Werkzeug that used absolute redirects. - assert location_parts[0] == "http://localhost" - - assert location_parts[2] == "test" - rv = c.get("/test") - assert rv.data == b"42" diff --git a/tests/test_reqctx.py b/tests/test_reqctx.py deleted file mode 100644 index 06bed245..00000000 --- a/tests/test_reqctx.py +++ /dev/null @@ -1,249 +0,0 @@ -from __future__ import annotations - -import collections.abc as cabc -import warnings -from concurrent import futures - -import pytest - -import flask -from flask.sessions import SecureCookieSessionInterface -from flask.sessions import SessionInterface -from flask.testing import FlaskClient - - -def test_teardown_on_pop(app): - buffer = [] - - @app.teardown_request - def end_of_request(exception): - buffer.append(exception) - - ctx = app.test_request_context() - ctx.push() - assert buffer == [] - ctx.pop() - assert buffer == [None] - - -def test_teardown_with_previous_exception(app): - buffer = [] - - @app.teardown_request - def end_of_request(exception): - buffer.append(exception) - - try: - raise Exception("dummy") - except Exception: - pass - - with app.test_request_context(): - assert buffer == [] - assert buffer == [None] - - -def test_teardown_with_handled_exception(app): - buffer = [] - - @app.teardown_request - def end_of_request(exception): - buffer.append(exception) - - with app.test_request_context(): - assert buffer == [] - try: - raise Exception("dummy") - except Exception: - pass - assert buffer == [None] - - -def test_proper_test_request_context(app): - app.config.update(SERVER_NAME="localhost.localdomain:5000") - - @app.route("/") - def index(): - return None - - @app.route("/", subdomain="foo") - def sub(): - return None - - with app.test_request_context("/"): - assert ( - flask.url_for("index", _external=True) - == "http://localhost.localdomain:5000/" - ) - - with app.test_request_context("/"): - assert ( - flask.url_for("sub", _external=True) - == "http://foo.localhost.localdomain:5000/" - ) - - # suppress Werkzeug 0.15 warning about name mismatch - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", "Current server name", UserWarning, "flask.app" - ) - with app.test_request_context( - "/", environ_overrides={"HTTP_HOST": "localhost"} - ): - pass - - app.config.update(SERVER_NAME="localhost") - with app.test_request_context("/", environ_overrides={"SERVER_NAME": "localhost"}): - pass - - app.config.update(SERVER_NAME="localhost:80") - with app.test_request_context( - "/", environ_overrides={"SERVER_NAME": "localhost:80"} - ): - pass - - -def test_context_binding(app): - @app.route("/") - def index(): - return f"Hello {flask.request.args['name']}!" - - @app.route("/meh") - def meh(): - return flask.request.url - - with app.test_request_context("/?name=World"): - assert index() == "Hello World!" - with app.test_request_context("/meh"): - assert meh() == "http://localhost/meh" - assert not flask.request - - -def test_context_test(app): - assert not flask.request - assert not flask.has_request_context() - ctx = app.test_request_context() - ctx.push() - try: - assert flask.request - assert flask.has_request_context() - finally: - ctx.pop() - - -def test_manual_context_binding(app): - @app.route("/") - def index(): - return f"Hello {flask.request.args['name']}!" - - ctx = app.test_request_context("/?name=World") - ctx.push() - assert index() == "Hello World!" - ctx.pop() - with pytest.raises(RuntimeError): - index() - - -def test_copy_context_thread( - request: pytest.FixtureRequest, app: flask.Flask, client: FlaskClient -) -> None: - executor = futures.ThreadPoolExecutor(max_workers=2) - request.addfinalizer(lambda: executor.shutdown(cancel_futures=True)) - result: cabc.Iterator[int] | None = None - - @app.route("/") - def index(): - flask.session["fizz"] = "buzz" - - @flask.copy_current_request_context - def work(n: int) -> int: - assert flask.current_app == app - assert flask.request.path == "/" - assert flask.request.args["foo"] == "bar" - assert flask.session["fizz"] == "buzz" - return n - - nonlocal result - result = executor.map(work, range(10)) - return "Hello World!" - - rv = client.get(query_string={"foo": "bar"}) - assert rv.text == "Hello World!" - - assert result is not None - assert set(result) == set(range(10)) - - -def test_session_error_pops_context(): - class SessionError(Exception): - pass - - class FailingSessionInterface(SessionInterface): - def open_session(self, app, request): - raise SessionError() - - class CustomFlask(flask.Flask): - session_interface = FailingSessionInterface() - - app = CustomFlask(__name__) - - @app.route("/") - def index(): - # shouldn't get here - AssertionError() - - response = app.test_client().get("/") - assert response.status_code == 500 - assert not flask.request - assert not flask.current_app - - -def test_session_dynamic_cookie_name(): - # This session interface will use a cookie with a different name if the - # requested url ends with the string "dynamic_cookie" - class PathAwareSessionInterface(SecureCookieSessionInterface): - def get_cookie_name(self, app): - if flask.request.url.endswith("dynamic_cookie"): - return "dynamic_cookie_name" - else: - return super().get_cookie_name(app) - - class CustomFlask(flask.Flask): - session_interface = PathAwareSessionInterface() - - app = CustomFlask(__name__) - app.secret_key = "secret_key" - - @app.route("/set", methods=["POST"]) - def set(): - flask.session["value"] = flask.request.form["value"] - return "value set" - - @app.route("/get") - def get(): - v = flask.session.get("value", "None") - return v - - @app.route("/set_dynamic_cookie", methods=["POST"]) - def set_dynamic_cookie(): - flask.session["value"] = flask.request.form["value"] - return "value set" - - @app.route("/get_dynamic_cookie") - def get_dynamic_cookie(): - v = flask.session.get("value", "None") - return v - - test_client = app.test_client() - - # first set the cookie in both /set urls but each with a different value - assert test_client.post("/set", data={"value": "42"}).data == b"value set" - assert ( - test_client.post("/set_dynamic_cookie", data={"value": "616"}).data - == b"value set" - ) - - # now check that the relevant values come back - meaning that different - # cookies are being used for the urls that end with "dynamic cookie" - assert test_client.get("/get").data == b"42" - assert test_client.get("/get_dynamic_cookie").data == b"616" diff --git a/tests/test_request.py b/tests/test_request.py deleted file mode 100644 index 3e95ab32..00000000 --- a/tests/test_request.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import annotations - -from flask import Flask -from flask import Request -from flask import request -from flask.testing import FlaskClient - - -def test_max_content_length(app: Flask, client: FlaskClient) -> None: - app.config["MAX_CONTENT_LENGTH"] = 50 - - @app.post("/") - def index(): - request.form["myfile"] - AssertionError() - - @app.errorhandler(413) - def catcher(error): - return "42" - - rv = client.post("/", data={"myfile": "foo" * 50}) - assert rv.data == b"42" - - -def test_limit_config(app: Flask): - app.config["MAX_CONTENT_LENGTH"] = 100 - app.config["MAX_FORM_MEMORY_SIZE"] = 50 - app.config["MAX_FORM_PARTS"] = 3 - r = Request({}) - - # no app context, use Werkzeug defaults - assert r.max_content_length is None - assert r.max_form_memory_size == 500_000 - assert r.max_form_parts == 1_000 - - # in app context, use config - with app.app_context(): - assert r.max_content_length == 100 - assert r.max_form_memory_size == 50 - assert r.max_form_parts == 3 - - # regardless of app context, use override - r.max_content_length = 90 - r.max_form_memory_size = 30 - r.max_form_parts = 4 - - assert r.max_content_length == 90 - assert r.max_form_memory_size == 30 - assert r.max_form_parts == 4 - - with app.app_context(): - assert r.max_content_length == 90 - assert r.max_form_memory_size == 30 - assert r.max_form_parts == 4 - - -def test_trusted_hosts_config(app: Flask) -> None: - app.config["TRUSTED_HOSTS"] = ["example.test", ".other.test"] - - @app.get("/") - def index() -> str: - return "" - - client = app.test_client() - r = client.get(base_url="http://example.test") - assert r.status_code == 200 - r = client.get(base_url="http://a.other.test") - assert r.status_code == 200 - r = client.get(base_url="http://bad.test") - assert r.status_code == 400 diff --git a/tests/test_session_interface.py b/tests/test_session_interface.py deleted file mode 100644 index 5564be74..00000000 --- a/tests/test_session_interface.py +++ /dev/null @@ -1,28 +0,0 @@ -import flask -from flask.globals import app_ctx -from flask.sessions import SessionInterface - - -def test_open_session_with_endpoint(): - """If request.endpoint (or other URL matching behavior) is needed - while loading the session, RequestContext.match_request() can be - called manually. - """ - - class MySessionInterface(SessionInterface): - def save_session(self, app, session, response): - pass - - def open_session(self, app, request): - app_ctx.match_request() - assert request.endpoint is not None - - app = flask.Flask(__name__) - app.session_interface = MySessionInterface() - - @app.get("/") - def index(): - return "Hello, World!" - - response = app.test_client().get("/") - assert response.status_code == 200 diff --git a/tests/test_signals.py b/tests/test_signals.py deleted file mode 100644 index 32ab333e..00000000 --- a/tests/test_signals.py +++ /dev/null @@ -1,181 +0,0 @@ -import flask - - -def test_template_rendered(app, client): - @app.route("/") - def index(): - return flask.render_template("simple_template.html", whiskey=42) - - recorded = [] - - def record(sender, template, context): - recorded.append((template, context)) - - flask.template_rendered.connect(record, app) - try: - client.get("/") - assert len(recorded) == 1 - template, context = recorded[0] - assert template.name == "simple_template.html" - assert context["whiskey"] == 42 - finally: - flask.template_rendered.disconnect(record, app) - - -def test_before_render_template(): - app = flask.Flask(__name__) - - @app.route("/") - def index(): - return flask.render_template("simple_template.html", whiskey=42) - - recorded = [] - - def record(sender, template, context): - context["whiskey"] = 43 - recorded.append((template, context)) - - flask.before_render_template.connect(record, app) - try: - rv = app.test_client().get("/") - assert len(recorded) == 1 - template, context = recorded[0] - assert template.name == "simple_template.html" - assert context["whiskey"] == 43 - assert rv.data == b"

43

" - finally: - flask.before_render_template.disconnect(record, app) - - -def test_request_signals(): - app = flask.Flask(__name__) - calls = [] - - def before_request_signal(sender): - calls.append("before-signal") - - def after_request_signal(sender, response): - assert response.data == b"stuff" - calls.append("after-signal") - - @app.before_request - def before_request_handler(): - calls.append("before-handler") - - @app.after_request - def after_request_handler(response): - calls.append("after-handler") - response.data = "stuff" - return response - - @app.route("/") - def index(): - calls.append("handler") - return "ignored anyway" - - flask.request_started.connect(before_request_signal, app) - flask.request_finished.connect(after_request_signal, app) - - try: - rv = app.test_client().get("/") - assert rv.data == b"stuff" - - assert calls == [ - "before-signal", - "before-handler", - "handler", - "after-handler", - "after-signal", - ] - finally: - flask.request_started.disconnect(before_request_signal, app) - flask.request_finished.disconnect(after_request_signal, app) - - -def test_request_exception_signal(): - app = flask.Flask(__name__) - recorded = [] - - @app.route("/") - def index(): - raise ZeroDivisionError - - def record(sender, exception): - recorded.append(exception) - - flask.got_request_exception.connect(record, app) - try: - assert app.test_client().get("/").status_code == 500 - assert len(recorded) == 1 - assert isinstance(recorded[0], ZeroDivisionError) - finally: - flask.got_request_exception.disconnect(record, app) - - -def test_appcontext_signals(app, client): - recorded = [] - - def record_push(sender, **kwargs): - recorded.append("push") - - def record_pop(sender, **kwargs): - recorded.append("pop") - - @app.route("/") - def index(): - return "Hello" - - flask.appcontext_pushed.connect(record_push, app) - flask.appcontext_popped.connect(record_pop, app) - try: - rv = client.get("/") - assert rv.data == b"Hello" - assert recorded == ["push", "pop"] - finally: - flask.appcontext_pushed.disconnect(record_push, app) - flask.appcontext_popped.disconnect(record_pop, app) - - -def test_flash_signal(app): - @app.route("/") - def index(): - flask.flash("This is a flash message", category="notice") - return flask.redirect("/other") - - recorded = [] - - def record(sender, message, category): - recorded.append((message, category)) - - flask.message_flashed.connect(record, app) - try: - client = app.test_client() - with client.session_transaction(): - client.get("/") - assert len(recorded) == 1 - message, category = recorded[0] - assert message == "This is a flash message" - assert category == "notice" - finally: - flask.message_flashed.disconnect(record, app) - - -def test_appcontext_tearing_down_signal(app, client): - app.testing = False - recorded = [] - - def record_teardown(sender, exc): - recorded.append(exc) - - @app.route("/") - def index(): - raise ZeroDivisionError - - flask.appcontext_tearing_down.connect(record_teardown, app) - try: - rv = client.get("/") - assert rv.status_code == 500 - assert len(recorded) == 1 - assert isinstance(recorded[0], ZeroDivisionError) - finally: - flask.appcontext_tearing_down.disconnect(record_teardown, app) diff --git a/tests/test_subclassing.py b/tests/test_subclassing.py deleted file mode 100644 index 3b9fe316..00000000 --- a/tests/test_subclassing.py +++ /dev/null @@ -1,21 +0,0 @@ -from io import StringIO - -import flask - - -def test_suppressed_exception_logging(): - class SuppressedFlask(flask.Flask): - def log_exception(self, ctx, exc_info): - pass - - out = StringIO() - app = SuppressedFlask(__name__) - - @app.route("/") - def index(): - raise Exception("test") - - rv = app.test_client().get("/", errors_stream=out) - assert rv.status_code == 500 - assert b"Internal Server Error" in rv.data - assert not out.getvalue() diff --git a/tests/test_templating.py b/tests/test_templating.py deleted file mode 100644 index 85549df0..00000000 --- a/tests/test_templating.py +++ /dev/null @@ -1,532 +0,0 @@ -import logging - -import pytest -import werkzeug.serving -from jinja2 import TemplateNotFound -from markupsafe import Markup - -import flask - - -def test_context_processing(app, client): - @app.context_processor - def context_processor(): - return {"injected_value": 42} - - @app.route("/") - def index(): - return flask.render_template("context_template.html", value=23) - - rv = client.get("/") - assert rv.data == b"

23|42" - - -def test_original_win(app, client): - @app.route("/") - def index(): - return flask.render_template_string("{{ config }}", config=42) - - rv = client.get("/") - assert rv.data == b"42" - - -def test_simple_stream(app, client): - @app.route("/") - def index(): - return flask.stream_template_string("{{ config }}", config=42) - - rv = client.get("/") - assert rv.data == b"42" - - -def test_request_less_rendering(app, app_ctx): - app.config["WORLD_NAME"] = "Special World" - - @app.context_processor - def context_processor(): - return dict(foo=42) - - rv = flask.render_template_string("Hello {{ config.WORLD_NAME }} {{ foo }}") - assert rv == "Hello Special World 42" - - -def test_standard_context(app, client): - @app.route("/") - def index(): - flask.g.foo = 23 - flask.session["test"] = "aha" - return flask.render_template_string( - """ - {{ request.args.foo }} - {{ g.foo }} - {{ config.DEBUG }} - {{ session.test }} - """ - ) - - rv = client.get("/?foo=42") - assert rv.data.split() == [b"42", b"23", b"False", b"aha"] - - -def test_escaping(app, client): - text = "

Hello World!" - - @app.route("/") - def index(): - return flask.render_template( - "escaping_template.html", text=text, html=Markup(text) - ) - - lines = client.get("/").data.splitlines() - assert lines == [ - b"<p>Hello World!", - b"

Hello World!", - b"

Hello World!", - b"

Hello World!", - b"<p>Hello World!", - b"

Hello World!", - ] - - -def test_no_escaping(app, client): - text = "

Hello World!" - - @app.route("/") - def index(): - return flask.render_template( - "non_escaping_template.txt", text=text, html=Markup(text) - ) - - lines = client.get("/").data.splitlines() - assert lines == [ - b"

Hello World!", - b"

Hello World!", - b"

Hello World!", - b"

Hello World!", - b"<p>Hello World!", - b"

Hello World!", - b"

Hello World!", - b"

Hello World!", - ] - - -def test_escaping_without_template_filename(app, client, req_ctx): - assert flask.render_template_string("{{ foo }}", foo="") == "<test>" - assert flask.render_template("mail.txt", foo="") == " Mail" - - -def test_macros(app, req_ctx): - macro = flask.get_template_attribute("_macro.html", "hello") - assert macro("World") == "Hello World!" - - -def test_template_filter(app): - @app.template_filter() - def my_reverse(s): - return s[::-1] - - assert "my_reverse" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse"] == my_reverse - assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" - - @app.template_filter - def my_reverse_2(s): - return s[::-1] - - assert "my_reverse_2" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse_2"] == my_reverse_2 - assert app.jinja_env.filters["my_reverse_2"]("abcd") == "dcba" - - @app.template_filter("my_reverse_custom_name_3") - def my_reverse_3(s): - return s[::-1] - - assert "my_reverse_custom_name_3" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse_custom_name_3"] == my_reverse_3 - assert app.jinja_env.filters["my_reverse_custom_name_3"]("abcd") == "dcba" - - @app.template_filter(name="my_reverse_custom_name_4") - def my_reverse_4(s): - return s[::-1] - - assert "my_reverse_custom_name_4" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse_custom_name_4"] == my_reverse_4 - assert app.jinja_env.filters["my_reverse_custom_name_4"]("abcd") == "dcba" - - -def test_add_template_filter(app): - def my_reverse(s): - return s[::-1] - - app.add_template_filter(my_reverse) - assert "my_reverse" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["my_reverse"] == my_reverse - assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" - - -def test_template_filter_with_name(app): - @app.template_filter("strrev") - def my_reverse(s): - return s[::-1] - - assert "strrev" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["strrev"] == my_reverse - assert app.jinja_env.filters["strrev"]("abcd") == "dcba" - - -def test_add_template_filter_with_name(app): - def my_reverse(s): - return s[::-1] - - app.add_template_filter(my_reverse, "strrev") - assert "strrev" in app.jinja_env.filters.keys() - assert app.jinja_env.filters["strrev"] == my_reverse - assert app.jinja_env.filters["strrev"]("abcd") == "dcba" - - -def test_template_filter_with_template(app, client): - @app.template_filter() - def super_reverse(s): - return s[::-1] - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_add_template_filter_with_template(app, client): - def super_reverse(s): - return s[::-1] - - app.add_template_filter(super_reverse) - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_template_filter_with_name_and_template(app, client): - @app.template_filter("super_reverse") - def my_reverse(s): - return s[::-1] - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_add_template_filter_with_name_and_template(app, client): - def my_reverse(s): - return s[::-1] - - app.add_template_filter(my_reverse, "super_reverse") - - @app.route("/") - def index(): - return flask.render_template("template_filter.html", value="abcd") - - rv = client.get("/") - assert rv.data == b"dcba" - - -def test_template_test(app): - @app.template_test() - def boolean(value): - return isinstance(value, bool) - - assert "boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean"] == boolean - assert app.jinja_env.tests["boolean"](False) - - @app.template_test - def boolean_2(value): - return isinstance(value, bool) - - assert "boolean_2" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean_2"] == boolean_2 - assert app.jinja_env.tests["boolean_2"](False) - - @app.template_test("my_boolean_custom_name") - def boolean_3(value): - return isinstance(value, bool) - - assert "my_boolean_custom_name" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["my_boolean_custom_name"] == boolean_3 - assert app.jinja_env.tests["my_boolean_custom_name"](False) - - @app.template_test(name="my_boolean_custom_name_2") - def boolean_4(value): - return isinstance(value, bool) - - assert "my_boolean_custom_name_2" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["my_boolean_custom_name_2"] == boolean_4 - assert app.jinja_env.tests["my_boolean_custom_name_2"](False) - - -def test_add_template_test(app): - def boolean(value): - return isinstance(value, bool) - - app.add_template_test(boolean) - assert "boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean"] == boolean - assert app.jinja_env.tests["boolean"](False) - - -def test_template_test_with_name(app): - @app.template_test("boolean") - def is_boolean(value): - return isinstance(value, bool) - - assert "boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean"] == is_boolean - assert app.jinja_env.tests["boolean"](False) - - -def test_add_template_test_with_name(app): - def is_boolean(value): - return isinstance(value, bool) - - app.add_template_test(is_boolean, "boolean") - assert "boolean" in app.jinja_env.tests.keys() - assert app.jinja_env.tests["boolean"] == is_boolean - assert app.jinja_env.tests["boolean"](False) - - -def test_template_test_with_template(app, client): - @app.template_test() - def boolean(value): - return isinstance(value, bool) - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_add_template_test_with_template(app, client): - def boolean(value): - return isinstance(value, bool) - - app.add_template_test(boolean) - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_template_test_with_name_and_template(app, client): - @app.template_test("boolean") - def is_boolean(value): - return isinstance(value, bool) - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_add_template_test_with_name_and_template(app, client): - def is_boolean(value): - return isinstance(value, bool) - - app.add_template_test(is_boolean, "boolean") - - @app.route("/") - def index(): - return flask.render_template("template_test.html", value=False) - - rv = client.get("/") - assert b"Success!" in rv.data - - -def test_add_template_global(app, app_ctx): - @app.template_global() - def get_stuff(): - return 42 - - assert "get_stuff" in app.jinja_env.globals.keys() - assert app.jinja_env.globals["get_stuff"] == get_stuff - assert app.jinja_env.globals["get_stuff"](), 42 - - rv = flask.render_template_string("{{ get_stuff() }}") - assert rv == "42" - - @app.template_global - def get_stuff_1(): - return "get_stuff_1" - - assert "get_stuff_1" in app.jinja_env.globals.keys() - assert app.jinja_env.globals["get_stuff_1"] == get_stuff_1 - assert app.jinja_env.globals["get_stuff_1"](), "get_stuff_1" - - rv = flask.render_template_string("{{ get_stuff_1() }}") - assert rv == "get_stuff_1" - - @app.template_global("my_get_stuff_custom_name_2") - def get_stuff_2(): - return "get_stuff_2" - - assert "my_get_stuff_custom_name_2" in app.jinja_env.globals.keys() - assert app.jinja_env.globals["my_get_stuff_custom_name_2"] == get_stuff_2 - assert app.jinja_env.globals["my_get_stuff_custom_name_2"](), "get_stuff_2" - - rv = flask.render_template_string("{{ my_get_stuff_custom_name_2() }}") - assert rv == "get_stuff_2" - - @app.template_global(name="my_get_stuff_custom_name_3") - def get_stuff_3(): - return "get_stuff_3" - - assert "my_get_stuff_custom_name_3" in app.jinja_env.globals.keys() - assert app.jinja_env.globals["my_get_stuff_custom_name_3"] == get_stuff_3 - assert app.jinja_env.globals["my_get_stuff_custom_name_3"](), "get_stuff_3" - - rv = flask.render_template_string("{{ my_get_stuff_custom_name_3() }}") - assert rv == "get_stuff_3" - - -def test_custom_template_loader(client): - class MyFlask(flask.Flask): - def create_global_jinja_loader(self): - from jinja2 import DictLoader - - return DictLoader({"index.html": "Hello Custom World!"}) - - app = MyFlask(__name__) - - @app.route("/") - def index(): - return flask.render_template("index.html") - - c = app.test_client() - rv = c.get("/") - assert rv.data == b"Hello Custom World!" - - -def test_iterable_loader(app, client): - @app.context_processor - def context_processor(): - return {"whiskey": "Jameson"} - - @app.route("/") - def index(): - return flask.render_template( - [ - "no_template.xml", # should skip this one - "simple_template.html", # should render this - "context_template.html", - ], - value=23, - ) - - rv = client.get("/") - assert rv.data == b"

Jameson

" - - -def test_templates_auto_reload(app): - # debug is False, config option is None - assert app.debug is False - assert app.config["TEMPLATES_AUTO_RELOAD"] is None - assert app.jinja_env.auto_reload is False - # debug is False, config option is False - app = flask.Flask(__name__) - app.config["TEMPLATES_AUTO_RELOAD"] = False - assert app.debug is False - assert app.jinja_env.auto_reload is False - # debug is False, config option is True - app = flask.Flask(__name__) - app.config["TEMPLATES_AUTO_RELOAD"] = True - assert app.debug is False - assert app.jinja_env.auto_reload is True - # debug is True, config option is None - app = flask.Flask(__name__) - app.config["DEBUG"] = True - assert app.config["TEMPLATES_AUTO_RELOAD"] is None - assert app.jinja_env.auto_reload is True - # debug is True, config option is False - app = flask.Flask(__name__) - app.config["DEBUG"] = True - app.config["TEMPLATES_AUTO_RELOAD"] = False - assert app.jinja_env.auto_reload is False - # debug is True, config option is True - app = flask.Flask(__name__) - app.config["DEBUG"] = True - app.config["TEMPLATES_AUTO_RELOAD"] = True - assert app.jinja_env.auto_reload is True - - -def test_templates_auto_reload_debug_run(app, monkeypatch): - def run_simple_mock(*args, **kwargs): - pass - - monkeypatch.setattr(werkzeug.serving, "run_simple", run_simple_mock) - - app.run() - assert not app.jinja_env.auto_reload - - app.run(debug=True) - assert app.jinja_env.auto_reload - - -def test_template_loader_debugging(test_apps, monkeypatch): - from blueprintapp import app - - called = [] - - class _TestHandler(logging.Handler): - def handle(self, record): - called.append(True) - text = str(record.msg) - assert "1: trying loader of application 'blueprintapp'" in text - assert ( - "2: trying loader of blueprint 'admin' (blueprintapp.apps.admin)" - ) in text - assert ( - "trying loader of blueprint 'frontend' (blueprintapp.apps.frontend)" - ) in text - assert "Error: the template could not be found" in text - assert ( - "looked up from an endpoint that belongs to the blueprint 'frontend'" - ) in text - assert "See https://flask.palletsprojects.com/blueprints/#templates" in text - - with app.test_client() as c: - monkeypatch.setitem(app.config, "EXPLAIN_TEMPLATE_LOADING", True) - monkeypatch.setattr( - logging.getLogger("blueprintapp"), "handlers", [_TestHandler()] - ) - - with pytest.raises(TemplateNotFound) as excinfo: - c.get("/missing") - - assert "missing_template.html" in str(excinfo.value) - - assert len(called) == 1 - - -def test_custom_jinja_env(): - class CustomEnvironment(flask.templating.Environment): - pass - - class CustomFlask(flask.Flask): - jinja_environment = CustomEnvironment - - app = CustomFlask(__name__) - assert isinstance(app.jinja_env, CustomEnvironment) diff --git a/tests/test_testing.py b/tests/test_testing.py deleted file mode 100644 index c172f14d..00000000 --- a/tests/test_testing.py +++ /dev/null @@ -1,384 +0,0 @@ -import importlib.metadata - -import click -import pytest - -import flask -from flask import appcontext_popped -from flask.cli import ScriptInfo -from flask.globals import _cv_app -from flask.json import jsonify -from flask.testing import EnvironBuilder -from flask.testing import FlaskCliRunner - - -def test_environ_defaults_from_config(app, client): - app.config["SERVER_NAME"] = "example.com:1234" - app.config["APPLICATION_ROOT"] = "/foo" - - @app.route("/") - def index(): - return flask.request.url - - ctx = app.test_request_context() - assert ctx.request.url == "http://example.com:1234/foo/" - - rv = client.get("/") - assert rv.data == b"http://example.com:1234/foo/" - - -def test_environ_defaults(app, client, app_ctx, req_ctx): - @app.route("/") - def index(): - return flask.request.url - - ctx = app.test_request_context() - assert ctx.request.url == "http://localhost/" - with client: - rv = client.get("/") - assert rv.data == b"http://localhost/" - - -def test_environ_base_default(app, client): - @app.route("/") - def index(): - flask.g.remote_addr = flask.request.remote_addr - flask.g.user_agent = flask.request.user_agent.string - return "" - - with client: - client.get("/") - assert flask.g.remote_addr == "127.0.0.1" - assert flask.g.user_agent == ( - f"Werkzeug/{importlib.metadata.version('werkzeug')}" - ) - - -def test_environ_base_modified(app, client): - @app.route("/") - def index(): - flask.g.remote_addr = flask.request.remote_addr - flask.g.user_agent = flask.request.user_agent.string - return "" - - client.environ_base["REMOTE_ADDR"] = "192.168.0.22" - client.environ_base["HTTP_USER_AGENT"] = "Foo" - - with client: - client.get("/") - assert flask.g.remote_addr == "192.168.0.22" - assert flask.g.user_agent == "Foo" - - -def test_client_open_environ(app, client, request): - @app.route("/index") - def index(): - return flask.request.remote_addr - - builder = EnvironBuilder(app, path="/index", method="GET") - - rv = client.open(builder) - assert rv.data == b"127.0.0.1" - - environ = builder.get_environ() - client.environ_base["REMOTE_ADDR"] = "127.0.0.2" - rv = client.open(environ) - assert rv.data == b"127.0.0.2" - - -def test_specify_url_scheme(app, client): - @app.route("/") - def index(): - return flask.request.url - - ctx = app.test_request_context(url_scheme="https") - assert ctx.request.url == "https://localhost/" - - rv = client.get("/", url_scheme="https") - assert rv.data == b"https://localhost/" - - -def test_path_is_url(app): - eb = EnvironBuilder(app, "https://example.com/") - assert eb.url_scheme == "https" - assert eb.host == "example.com" - assert eb.script_root == "" - assert eb.path == "/" - - -def test_environbuilder_json_dumps(app): - """EnvironBuilder.json_dumps() takes settings from the app.""" - app.json.ensure_ascii = False - eb = EnvironBuilder(app, json="\u20ac") - assert eb.input_stream.read().decode("utf8") == '"\u20ac"' - - -def test_blueprint_with_subdomain(): - app = flask.Flask(__name__, subdomain_matching=True) - app.config["SERVER_NAME"] = "example.com:1234" - app.config["APPLICATION_ROOT"] = "/foo" - client = app.test_client() - - bp = flask.Blueprint("company", __name__, subdomain="xxx") - - @bp.route("/") - def index(): - return flask.request.url - - app.register_blueprint(bp) - - ctx = app.test_request_context("/", subdomain="xxx") - assert ctx.request.url == "http://xxx.example.com:1234/foo/" - - with ctx: - assert ctx.request.blueprint == bp.name - - rv = client.get("/", subdomain="xxx") - assert rv.data == b"http://xxx.example.com:1234/foo/" - - -def test_redirect_session(app, client, app_ctx): - @app.route("/redirect") - def index(): - flask.session["redirect"] = True - return flask.redirect("/target") - - @app.route("/target") - def get_session(): - flask.session["target"] = True - return "" - - with client: - client.get("/redirect", follow_redirects=True) - assert flask.session["redirect"] is True - assert flask.session["target"] is True - - -def test_session_transactions(app, client): - @app.route("/") - def index(): - return str(flask.session["foo"]) - - with client: - with client.session_transaction() as sess: - assert len(sess) == 0 - sess["foo"] = [42] - assert len(sess) == 1 - rv = client.get("/") - assert rv.data == b"[42]" - with client.session_transaction() as sess: - assert len(sess) == 1 - assert sess["foo"] == [42] - - -def test_session_transactions_no_null_sessions(): - app = flask.Flask(__name__) - - with app.test_client() as c: - with pytest.raises(RuntimeError) as e: - with c.session_transaction(): - pass - assert "Session backend did not open a session" in str(e.value) - - -def test_session_transactions_keep_context(app, client, req_ctx): - client.get("/") - req = flask.request._get_current_object() - assert req is not None - with client.session_transaction(): - assert req is flask.request._get_current_object() - - -def test_session_transaction_needs_cookies(app): - c = app.test_client(use_cookies=False) - - with pytest.raises(TypeError, match="Cookies are disabled."): - with c.session_transaction(): - pass - - -def test_test_client_context_binding(app, client): - app.testing = False - - @app.route("/") - def index(): - flask.g.value = 42 - return "Hello World!" - - @app.route("/other") - def other(): - raise ZeroDivisionError - - with client: - resp = client.get("/") - assert flask.g.value == 42 - assert resp.data == b"Hello World!" - assert resp.status_code == 200 - - with client: - resp = client.get("/other") - assert not hasattr(flask.g, "value") - assert b"Internal Server Error" in resp.data - assert resp.status_code == 500 - flask.g.value = 23 - - with pytest.raises(RuntimeError): - flask.g.value # noqa: B018 - - -def test_reuse_client(client): - c = client - - with c: - assert client.get("/").status_code == 404 - - with c: - assert client.get("/").status_code == 404 - - -def test_full_url_request(app, client): - @app.route("/action", methods=["POST"]) - def action(): - return "x" - - with client: - rv = client.post("http://domain.com/action?vodka=42", data={"gin": 43}) - assert rv.status_code == 200 - assert "gin" in flask.request.form - assert "vodka" in flask.request.args - - -def test_json_request_and_response(app, client): - @app.route("/echo", methods=["POST"]) - def echo(): - return jsonify(flask.request.get_json()) - - with client: - json_data = {"drink": {"gin": 1, "tonic": True}, "price": 10} - rv = client.post("/echo", json=json_data) - - # Request should be in JSON - assert flask.request.is_json - assert flask.request.get_json() == json_data - - # Response should be in JSON - assert rv.status_code == 200 - assert rv.is_json - assert rv.get_json() == json_data - - -def test_client_json_no_app_context(app, client): - @app.route("/hello", methods=["POST"]) - def hello(): - return f"Hello, {flask.request.json['name']}!" - - class Namespace: - count = 0 - - def add(self, app): - self.count += 1 - - ns = Namespace() - - with appcontext_popped.connected_to(ns.add, app): - rv = client.post("/hello", json={"name": "Flask"}) - - assert rv.get_data(as_text=True) == "Hello, Flask!" - assert ns.count == 1 - - -def test_subdomain(): - app = flask.Flask(__name__, subdomain_matching=True) - app.config["SERVER_NAME"] = "example.com" - client = app.test_client() - - @app.route("/", subdomain="") - def view(company_id): - return company_id - - with app.test_request_context(): - url = flask.url_for("view", company_id="xxx") - - with client: - response = client.get(url) - - assert 200 == response.status_code - assert b"xxx" == response.data - - -def test_nosubdomain(app, client): - app.config["SERVER_NAME"] = "example.com" - - @app.route("/") - def view(company_id): - return company_id - - with app.test_request_context(): - url = flask.url_for("view", company_id="xxx") - - with client: - response = client.get(url) - - assert 200 == response.status_code - assert b"xxx" == response.data - - -def test_cli_runner_class(app): - runner = app.test_cli_runner() - assert isinstance(runner, FlaskCliRunner) - - class SubRunner(FlaskCliRunner): - pass - - app.test_cli_runner_class = SubRunner - runner = app.test_cli_runner() - assert isinstance(runner, SubRunner) - - -def test_cli_invoke(app): - @app.cli.command("hello") - def hello_command(): - click.echo("Hello, World!") - - runner = app.test_cli_runner() - # invoke with command name - result = runner.invoke(args=["hello"]) - assert "Hello" in result.output - # invoke with command object - result = runner.invoke(hello_command) - assert "Hello" in result.output - - -def test_cli_custom_obj(app): - class NS: - called = False - - def create_app(): - NS.called = True - return app - - @app.cli.command("hello") - def hello_command(): - click.echo("Hello, World!") - - script_info = ScriptInfo(create_app=create_app) - runner = app.test_cli_runner() - runner.invoke(hello_command, obj=script_info) - assert NS.called - - -def test_client_pop_all_preserved(app, req_ctx, client): - @app.route("/") - def index(): - # stream_with_context pushes a third context, preserved by response - return flask.stream_with_context("hello") - - # req_ctx fixture pushed an initial context - with client: - # request pushes a second request context, preserved by client - rv = client.get("/") - - # close the response, releasing the context held by stream_with_context - rv.close() - # only req_ctx fixture should still be pushed - assert _cv_app.get(None) is req_ctx diff --git a/tests/test_user_error_handler.py b/tests/test_user_error_handler.py deleted file mode 100644 index 79c5a73c..00000000 --- a/tests/test_user_error_handler.py +++ /dev/null @@ -1,295 +0,0 @@ -import pytest -from werkzeug.exceptions import Forbidden -from werkzeug.exceptions import HTTPException -from werkzeug.exceptions import InternalServerError -from werkzeug.exceptions import NotFound - -import flask - - -def test_error_handler_no_match(app, client): - class CustomException(Exception): - pass - - @app.errorhandler(CustomException) - def custom_exception_handler(e): - assert isinstance(e, CustomException) - return "custom" - - with pytest.raises(TypeError) as exc_info: - app.register_error_handler(CustomException(), None) - - assert "CustomException() is an instance, not a class." in str(exc_info.value) - - with pytest.raises(ValueError) as exc_info: - app.register_error_handler(list, None) - - assert "'list' is not a subclass of Exception." in str(exc_info.value) - - @app.errorhandler(500) - def handle_500(e): - assert isinstance(e, InternalServerError) - - if e.original_exception is not None: - return f"wrapped {type(e.original_exception).__name__}" - - return "direct" - - with pytest.raises(ValueError) as exc_info: - app.register_error_handler(999, None) - - assert "Use a subclass of HTTPException" in str(exc_info.value) - - @app.route("/custom") - def custom_test(): - raise CustomException() - - @app.route("/keyerror") - def key_error(): - raise KeyError() - - @app.route("/abort") - def do_abort(): - flask.abort(500) - - app.testing = False - assert client.get("/custom").data == b"custom" - assert client.get("/keyerror").data == b"wrapped KeyError" - assert client.get("/abort").data == b"direct" - - -def test_error_handler_subclass(app): - class ParentException(Exception): - pass - - class ChildExceptionUnregistered(ParentException): - pass - - class ChildExceptionRegistered(ParentException): - pass - - @app.errorhandler(ParentException) - def parent_exception_handler(e): - assert isinstance(e, ParentException) - return "parent" - - @app.errorhandler(ChildExceptionRegistered) - def child_exception_handler(e): - assert isinstance(e, ChildExceptionRegistered) - return "child-registered" - - @app.route("/parent") - def parent_test(): - raise ParentException() - - @app.route("/child-unregistered") - def unregistered_test(): - raise ChildExceptionUnregistered() - - @app.route("/child-registered") - def registered_test(): - raise ChildExceptionRegistered() - - c = app.test_client() - - assert c.get("/parent").data == b"parent" - assert c.get("/child-unregistered").data == b"parent" - assert c.get("/child-registered").data == b"child-registered" - - -def test_error_handler_http_subclass(app): - class ForbiddenSubclassRegistered(Forbidden): - pass - - class ForbiddenSubclassUnregistered(Forbidden): - pass - - @app.errorhandler(403) - def code_exception_handler(e): - assert isinstance(e, Forbidden) - return "forbidden" - - @app.errorhandler(ForbiddenSubclassRegistered) - def subclass_exception_handler(e): - assert isinstance(e, ForbiddenSubclassRegistered) - return "forbidden-registered" - - @app.route("/forbidden") - def forbidden_test(): - raise Forbidden() - - @app.route("/forbidden-registered") - def registered_test(): - raise ForbiddenSubclassRegistered() - - @app.route("/forbidden-unregistered") - def unregistered_test(): - raise ForbiddenSubclassUnregistered() - - c = app.test_client() - - assert c.get("/forbidden").data == b"forbidden" - assert c.get("/forbidden-unregistered").data == b"forbidden" - assert c.get("/forbidden-registered").data == b"forbidden-registered" - - -def test_error_handler_blueprint(app): - bp = flask.Blueprint("bp", __name__) - - @bp.errorhandler(500) - def bp_exception_handler(e): - return "bp-error" - - @bp.route("/error") - def bp_test(): - raise InternalServerError() - - @app.errorhandler(500) - def app_exception_handler(e): - return "app-error" - - @app.route("/error") - def app_test(): - raise InternalServerError() - - app.register_blueprint(bp, url_prefix="/bp") - - c = app.test_client() - - assert c.get("/error").data == b"app-error" - assert c.get("/bp/error").data == b"bp-error" - - -def test_default_error_handler(): - bp = flask.Blueprint("bp", __name__) - - @bp.errorhandler(HTTPException) - def bp_exception_handler(e): - assert isinstance(e, HTTPException) - assert isinstance(e, NotFound) - return "bp-default" - - @bp.errorhandler(Forbidden) - def bp_forbidden_handler(e): - assert isinstance(e, Forbidden) - return "bp-forbidden" - - @bp.route("/undefined") - def bp_registered_test(): - raise NotFound() - - @bp.route("/forbidden") - def bp_forbidden_test(): - raise Forbidden() - - app = flask.Flask(__name__) - - @app.errorhandler(HTTPException) - def catchall_exception_handler(e): - assert isinstance(e, HTTPException) - assert isinstance(e, NotFound) - return "default" - - @app.errorhandler(Forbidden) - def catchall_forbidden_handler(e): - assert isinstance(e, Forbidden) - return "forbidden" - - @app.route("/forbidden") - def forbidden(): - raise Forbidden() - - @app.route("/slash/") - def slash(): - return "slash" - - app.register_blueprint(bp, url_prefix="/bp") - - c = app.test_client() - assert c.get("/bp/undefined").data == b"bp-default" - assert c.get("/bp/forbidden").data == b"bp-forbidden" - assert c.get("/undefined").data == b"default" - assert c.get("/forbidden").data == b"forbidden" - # Don't handle RequestRedirect raised when adding slash. - assert c.get("/slash", follow_redirects=True).data == b"slash" - - -class TestGenericHandlers: - """Test how very generic handlers are dispatched to.""" - - class Custom(Exception): - pass - - @pytest.fixture() - def app(self, app): - @app.route("/custom") - def do_custom(): - raise self.Custom() - - @app.route("/error") - def do_error(): - raise KeyError() - - @app.route("/abort") - def do_abort(): - flask.abort(500) - - @app.route("/raise") - def do_raise(): - raise InternalServerError() - - app.config["PROPAGATE_EXCEPTIONS"] = False - return app - - def report_error(self, e): - original = getattr(e, "original_exception", None) - - if original is not None: - return f"wrapped {type(original).__name__}" - - return f"direct {type(e).__name__}" - - @pytest.mark.parametrize("to_handle", (InternalServerError, 500)) - def test_handle_class_or_code(self, app, client, to_handle): - """``InternalServerError`` and ``500`` are aliases, they should - have the same behavior. Both should only receive - ``InternalServerError``, which might wrap another error. - """ - - @app.errorhandler(to_handle) - def handle_500(e): - assert isinstance(e, InternalServerError) - return self.report_error(e) - - assert client.get("/custom").data == b"wrapped Custom" - assert client.get("/error").data == b"wrapped KeyError" - assert client.get("/abort").data == b"direct InternalServerError" - assert client.get("/raise").data == b"direct InternalServerError" - - def test_handle_generic_http(self, app, client): - """``HTTPException`` should only receive ``HTTPException`` - subclasses. It will receive ``404`` routing exceptions. - """ - - @app.errorhandler(HTTPException) - def handle_http(e): - assert isinstance(e, HTTPException) - return str(e.code) - - assert client.get("/error").data == b"500" - assert client.get("/abort").data == b"500" - assert client.get("/not-found").data == b"404" - - def test_handle_generic(self, app, client): - """Generic ``Exception`` will handle all exceptions directly, - including ``HTTPExceptions``. - """ - - @app.errorhandler(Exception) - def handle_exception(e): - return self.report_error(e) - - assert client.get("/custom").data == b"direct Custom" - assert client.get("/error").data == b"direct KeyError" - assert client.get("/abort").data == b"direct InternalServerError" - assert client.get("/not-found").data == b"direct NotFound" diff --git a/tests/test_views.py b/tests/test_views.py deleted file mode 100644 index d002b504..00000000 --- a/tests/test_views.py +++ /dev/null @@ -1,272 +0,0 @@ -import pytest -from werkzeug.http import parse_set_header - -import flask.views -from flask.testing import FlaskClient - - -def common_test(app): - c = app.test_client() - - assert c.get("/").data == b"GET" - assert c.post("/").data == b"POST" - assert c.put("/").status_code == 405 - meths = parse_set_header(c.open("/", method="OPTIONS").headers["Allow"]) - assert sorted(meths) == ["GET", "HEAD", "OPTIONS", "POST"] - - -def test_basic_view(app): - class Index(flask.views.View): - methods = ["GET", "POST"] - - def dispatch_request(self): - return flask.request.method - - app.add_url_rule("/", view_func=Index.as_view("index")) - common_test(app) - - -def test_method_based_view(app): - class Index(flask.views.MethodView): - def get(self): - return "GET" - - def post(self): - return "POST" - - app.add_url_rule("/", view_func=Index.as_view("index")) - - common_test(app) - - -def test_view_patching(app): - class Index(flask.views.MethodView): - def get(self): - raise ZeroDivisionError - - def post(self): - raise ZeroDivisionError - - class Other(Index): - def get(self): - return "GET" - - def post(self): - return "POST" - - view = Index.as_view("index") - view.view_class = Other - app.add_url_rule("/", view_func=view) - common_test(app) - - -def test_view_inheritance(app, client): - class Index(flask.views.MethodView): - def get(self): - return "GET" - - def post(self): - return "POST" - - class BetterIndex(Index): - def delete(self): - return "DELETE" - - app.add_url_rule("/", view_func=BetterIndex.as_view("index")) - - meths = parse_set_header(client.open("/", method="OPTIONS").headers["Allow"]) - assert sorted(meths) == ["DELETE", "GET", "HEAD", "OPTIONS", "POST"] - - -def test_view_decorators(app, client): - def add_x_parachute(f): - def new_function(*args, **kwargs): - resp = flask.make_response(f(*args, **kwargs)) - resp.headers["X-Parachute"] = "awesome" - return resp - - return new_function - - class Index(flask.views.View): - decorators = [add_x_parachute] - - def dispatch_request(self): - return "Awesome" - - app.add_url_rule("/", view_func=Index.as_view("index")) - rv = client.get("/") - assert rv.headers["X-Parachute"] == "awesome" - assert rv.data == b"Awesome" - - -def test_view_provide_automatic_options_attr_disable( - app: flask.Flask, client: FlaskClient -) -> None: - """Automatic options can be disabled by the view class attribute.""" - - class Index(flask.views.View): - provide_automatic_options = False - - def dispatch_request(self): - return "Hello World!" - - app.add_url_rule("/", view_func=Index.as_view("index")) - rv = client.options() - assert rv.status_code == 405 - - -def test_view_provide_automatic_options_attr_enable( - app: flask.Flask, client: FlaskClient -) -> None: - """When default automatic options is disabled in config, it can still be - enabled by the view class attribute. - """ - app.config["PROVIDE_AUTOMATIC_OPTIONS"] = False - - class Index(flask.views.View): - provide_automatic_options = True - - def dispatch_request(self): - return "Hello World!" - - app.add_url_rule("/", view_func=Index.as_view("index")) - rv = client.options("/") - assert rv.allow == {"GET", "HEAD", "OPTIONS"} - - -def test_provide_automatic_options_method_disable( - app: flask.Flask, client: FlaskClient -) -> None: - """Automatic options is ignored if the route handles options.""" - - class Index(flask.views.View): - methods = ["OPTIONS"] - - def dispatch_request(self): - return "", {"X-Test": "test"} - - app.add_url_rule("/", view_func=Index.as_view("index")) - rv = client.options() - assert rv.headers["X-Test"] == "test" - - -def test_implicit_head(app, client): - class Index(flask.views.MethodView): - def get(self): - return flask.Response("Blub", headers={"X-Method": flask.request.method}) - - app.add_url_rule("/", view_func=Index.as_view("index")) - rv = client.get("/") - assert rv.data == b"Blub" - assert rv.headers["X-Method"] == "GET" - rv = client.head("/") - assert rv.data == b"" - assert rv.headers["X-Method"] == "HEAD" - - -def test_explicit_head(app, client): - class Index(flask.views.MethodView): - def get(self): - return "GET" - - def head(self): - return flask.Response("", headers={"X-Method": "HEAD"}) - - app.add_url_rule("/", view_func=Index.as_view("index")) - rv = client.get("/") - assert rv.data == b"GET" - rv = client.head("/") - assert rv.data == b"" - assert rv.headers["X-Method"] == "HEAD" - - -def test_endpoint_override(app): - app.debug = True - - class Index(flask.views.View): - methods = ["GET", "POST"] - - def dispatch_request(self): - return flask.request.method - - app.add_url_rule("/", view_func=Index.as_view("index")) - - with pytest.raises(AssertionError): - app.add_url_rule("/other", view_func=Index.as_view("index")) - - # But these tests should still pass. We just log a warning. - common_test(app) - - -def test_methods_var_inheritance(app, client): - class BaseView(flask.views.MethodView): - methods = ["GET", "PROPFIND"] - - class ChildView(BaseView): - def get(self): - return "GET" - - def propfind(self): - return "PROPFIND" - - app.add_url_rule("/", view_func=ChildView.as_view("index")) - - assert client.get("/").data == b"GET" - assert client.open("/", method="PROPFIND").data == b"PROPFIND" - assert ChildView.methods == {"PROPFIND", "GET"} - - -def test_multiple_inheritance(app, client): - class GetView(flask.views.MethodView): - def get(self): - return "GET" - - class DeleteView(flask.views.MethodView): - def delete(self): - return "DELETE" - - class GetDeleteView(GetView, DeleteView): - pass - - app.add_url_rule("/", view_func=GetDeleteView.as_view("index")) - - assert client.get("/").data == b"GET" - assert client.delete("/").data == b"DELETE" - assert sorted(GetDeleteView.methods) == ["DELETE", "GET"] - - -def test_remove_method_from_parent(app, client): - class GetView(flask.views.MethodView): - def get(self): - return "GET" - - class OtherView(flask.views.MethodView): - def post(self): - return "POST" - - class View(GetView, OtherView): - methods = ["GET"] - - app.add_url_rule("/", view_func=View.as_view("index")) - - assert client.get("/").data == b"GET" - assert client.post("/").status_code == 405 - assert sorted(View.methods) == ["GET"] - - -def test_init_once(app, client): - n = 0 - - class CountInit(flask.views.View): - init_every_request = False - - def __init__(self): - nonlocal n - n += 1 - - def dispatch_request(self): - return str(n) - - app.add_url_rule("/", view_func=CountInit.as_view("index")) - assert client.get("/").data == b"1" - assert client.get("/").data == b"1" diff --git a/tests/type_check/typing_app_decorators.py b/tests/type_check/typing_app_decorators.py deleted file mode 100644 index 0e25a30c..00000000 --- a/tests/type_check/typing_app_decorators.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -from flask import Flask -from flask import Response - -app = Flask(__name__) - - -@app.after_request -def after_sync(response: Response) -> Response: - return Response() - - -@app.after_request -async def after_async(response: Response) -> Response: - return Response() - - -@app.before_request -def before_sync() -> None: ... - - -@app.before_request -async def before_async() -> None: ... - - -@app.teardown_appcontext -def teardown_sync(exc: BaseException | None) -> None: ... - - -@app.teardown_appcontext -async def teardown_async(exc: BaseException | None) -> None: ... diff --git a/tests/type_check/typing_error_handler.py b/tests/type_check/typing_error_handler.py deleted file mode 100644 index ec9c886f..00000000 --- a/tests/type_check/typing_error_handler.py +++ /dev/null @@ -1,33 +0,0 @@ -from __future__ import annotations - -from http import HTTPStatus - -from werkzeug.exceptions import BadRequest -from werkzeug.exceptions import NotFound - -from flask import Flask - -app = Flask(__name__) - - -@app.errorhandler(400) -@app.errorhandler(HTTPStatus.BAD_REQUEST) -@app.errorhandler(BadRequest) -def handle_400(e: BadRequest) -> str: - return "" - - -@app.errorhandler(ValueError) -def handle_custom(e: ValueError) -> str: - return "" - - -@app.errorhandler(ValueError) -def handle_accept_base(e: Exception) -> str: - return "" - - -@app.errorhandler(BadRequest) -@app.errorhandler(404) -def handle_multiple(e: BadRequest | NotFound) -> str: - return "" diff --git a/tests/type_check/typing_route.py b/tests/type_check/typing_route.py deleted file mode 100644 index 8bc271b2..00000000 --- a/tests/type_check/typing_route.py +++ /dev/null @@ -1,112 +0,0 @@ -from __future__ import annotations - -import typing as t -from http import HTTPStatus - -from flask import Flask -from flask import jsonify -from flask import stream_template -from flask.templating import render_template -from flask.views import View -from flask.wrappers import Response - -app = Flask(__name__) - - -@app.route("/str") -def hello_str() -> str: - return "

Hello, World!

" - - -@app.route("/bytes") -def hello_bytes() -> bytes: - return b"

Hello, World!

" - - -@app.route("/json") -def hello_json() -> Response: - return jsonify("Hello, World!") - - -@app.route("/json/dict") -def hello_json_dict() -> dict[str, t.Any]: - return {"response": "Hello, World!"} - - -@app.route("/json/dict") -def hello_json_list() -> list[t.Any]: - return [{"message": "Hello"}, {"message": "World"}] - - -class StatusJSON(t.TypedDict): - status: str - - -@app.route("/typed-dict") -def typed_dict() -> StatusJSON: - return {"status": "ok"} - - -@app.route("/generator") -def hello_generator() -> t.Generator[str, None, None]: - def show() -> t.Generator[str, None, None]: - for x in range(100): - yield f"data:{x}\n\n" - - return show() - - -@app.route("/generator-expression") -def hello_generator_expression() -> t.Iterator[bytes]: - return (f"data:{x}\n\n".encode() for x in range(100)) - - -@app.route("/iterator") -def hello_iterator() -> t.Iterator[str]: - return iter([f"data:{x}\n\n" for x in range(100)]) - - -@app.route("/status") -@app.route("/status/") -def tuple_status(code: int = 200) -> tuple[str, int]: - return "hello", code - - -@app.route("/status-enum") -def tuple_status_enum() -> tuple[str, int]: - return "hello", HTTPStatus.OK - - -@app.route("/headers") -def tuple_headers() -> tuple[str, dict[str, str]]: - return "Hello, World!", {"Content-Type": "text/plain"} - - -@app.route("/template") -@app.route("/template/") -def return_template(name: str | None = None) -> str: - return render_template("index.html", name=name) - - -@app.route("/template") -def return_template_stream() -> t.Iterator[str]: - return stream_template("index.html", name="Hello") - - -@app.route("/async") -async def async_route() -> str: - return "Hello" - - -class RenderTemplateView(View): - def __init__(self: RenderTemplateView, template_name: str) -> None: - self.template_name = template_name - - def dispatch_request(self: RenderTemplateView) -> str: - return render_template(self.template_name) - - -app.add_url_rule( - "/about", - view_func=RenderTemplateView.as_view("about_page", template_name="about.html"), -) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..15911b4c --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[tox] +envlist=py25,py26,py27 + +[testenv] +commands=make test diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 5c1957e3..00000000 --- a/uv.lock +++ /dev/null @@ -1,1811 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.10" -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version >= '3.12' and python_full_version < '3.15'", - "python_full_version == '3.11.*'", - "python_full_version < '3.11'", -] - -[[package]] -name = "alabaster" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, -] - -[[package]] -name = "anyio" -version = "4.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, -] - -[[package]] -name = "asgiref" -version = "3.11.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, -] - -[[package]] -name = "ast-serialize" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, - { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, - { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, - { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, - { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, - { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, - { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, - { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, - { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, - { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, - { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, - { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, - { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, - { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, - { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, - { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, - { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, - { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, - { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, - { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, - { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, - { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, - { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, - { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, - { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, - { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, - { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, -] - -[[package]] -name = "babel" -version = "2.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, -] - -[[package]] -name = "blinker" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, -] - -[[package]] -name = "cachetools" -version = "7.1.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8f/c1/67cfb86aa21144796ff51068326d467fbef8ee42f8d08a3a8a926106cf0c/cachetools-7.1.3.tar.gz", hash = "sha256:135cfe944bc3c1e805505f65dae0bef375a2f96261171ab66c79ef77d0bda39d", size = 45780, upload-time = "2026-05-18T18:21:03.819Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/52/8ff5c1a3b2e821ced9b2998fba3ee29aa4525c0bf51e5ee55dd6f61a4ed5/cachetools-7.1.3-py3-none-any.whl", hash = "sha256:9876787e2346e20584d5cca236cb5d49d04e7193de91646f230725b2e1e8b804", size = 16763, upload-time = "2026-05-18T18:21:02.386Z" }, -] - -[[package]] -name = "certifi" -version = "2026.4.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - -[[package]] -name = "cfgv" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, - { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, - { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, - { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, - { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, - { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, - { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, -] - -[[package]] -name = "click" -version = "8.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "cryptography" -version = "48.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, - { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, - { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, - { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, - { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, - { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, - { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, - { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, - { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, - { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, - { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, - { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, -] - -[[package]] -name = "distlib" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, -] - -[[package]] -name = "docutils" -version = "0.21.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - -[[package]] -name = "filelock" -version = "3.29.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, -] - -[[package]] -name = "flask" -version = "3.2.0.dev0" -source = { editable = "." } -dependencies = [ - { name = "blinker" }, - { name = "click" }, - { name = "itsdangerous" }, - { name = "jinja2" }, - { name = "markupsafe" }, - { name = "werkzeug" }, -] - -[package.optional-dependencies] -async = [ - { name = "asgiref" }, -] -dotenv = [ - { name = "python-dotenv" }, -] - -[package.dev-dependencies] -dev = [ - { name = "ruff" }, - { name = "tox" }, - { name = "tox-uv" }, -] -docs = [ - { name = "pallets-sphinx-themes" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "sphinx-tabs" }, - { name = "sphinxcontrib-log-cabinet" }, -] -docs-auto = [ - { name = "sphinx-autobuild", version = "2024.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx-autobuild", version = "2025.8.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -gha-update = [ - { name = "gha-update", marker = "python_full_version >= '3.12'" }, -] -pre-commit = [ - { name = "pre-commit" }, - { name = "pre-commit-uv" }, -] -tests = [ - { name = "asgiref" }, - { name = "pytest" }, - { name = "python-dotenv" }, -] -typing = [ - { name = "asgiref" }, - { name = "cryptography" }, - { name = "mypy" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "python-dotenv" }, - { name = "types-contextvars" }, - { name = "types-dataclasses" }, -] - -[package.metadata] -requires-dist = [ - { name = "asgiref", marker = "extra == 'async'", specifier = ">=3.2" }, - { name = "blinker", specifier = ">=1.9.0" }, - { name = "click", specifier = ">=8.1.3" }, - { name = "itsdangerous", specifier = ">=2.2.0" }, - { name = "jinja2", specifier = ">=3.1.2" }, - { name = "markupsafe", specifier = ">=2.1.1" }, - { name = "python-dotenv", marker = "extra == 'dotenv'" }, - { name = "werkzeug", specifier = ">=3.1.0" }, -] -provides-extras = ["async", "dotenv"] - -[package.metadata.requires-dev] -dev = [ - { name = "ruff" }, - { name = "tox" }, - { name = "tox-uv" }, -] -docs = [ - { name = "pallets-sphinx-themes" }, - { name = "sphinx", specifier = "<9" }, - { name = "sphinx-tabs" }, - { name = "sphinxcontrib-log-cabinet" }, -] -docs-auto = [{ name = "sphinx-autobuild" }] -gha-update = [{ name = "gha-update", marker = "python_full_version >= '3.12'" }] -pre-commit = [ - { name = "pre-commit" }, - { name = "pre-commit-uv" }, -] -tests = [ - { name = "asgiref" }, - { name = "pytest" }, - { name = "python-dotenv" }, -] -typing = [ - { name = "asgiref" }, - { name = "cryptography" }, - { name = "mypy" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "python-dotenv" }, - { name = "types-contextvars" }, - { name = "types-dataclasses" }, -] - -[[package]] -name = "gha-update" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click", marker = "python_full_version >= '3.12'" }, - { name = "httpx", marker = "python_full_version >= '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/46/e8/eb710e08998a22b314cc068f14805cfdd12e3934a5496c8916c5a164c65a/gha_update-0.2.0.tar.gz", hash = "sha256:328ee0db09346ad13ee90646698cea2ec1f9035964ddd7c2a728a91034c3f4b0", size = 4756, upload-time = "2025-07-14T03:13:33.254Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/29/a0e42b0b80d614aa82929f65cce0d51443eea296802b2094cadc5660321b/gha_update-0.2.0-py3-none-any.whl", hash = "sha256:ec5641bf23f71baa1232fc61b3059fb08456e1b78150d1e9c1bab69b37046e49", size = 5323, upload-time = "2025-07-14T03:13:31.932Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi", marker = "python_full_version >= '3.12'" }, - { name = "h11", marker = "python_full_version >= '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.12'" }, - { name = "certifi", marker = "python_full_version >= '3.12'" }, - { name = "httpcore", marker = "python_full_version >= '3.12'" }, - { name = "idna", marker = "python_full_version >= '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "identify" -version = "2.6.19" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, -] - -[[package]] -name = "idna" -version = "3.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, -] - -[[package]] -name = "imagesize" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - -[[package]] -name = "itsdangerous" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "librt" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706, upload-time = "2026-05-10T18:15:16.129Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605, upload-time = "2026-05-10T18:15:18.148Z" }, - { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555, upload-time = "2026-05-10T18:15:19.569Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434, upload-time = "2026-05-10T18:15:20.87Z" }, - { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918, upload-time = "2026-05-10T18:15:22.616Z" }, - { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334, upload-time = "2026-05-10T18:15:24.2Z" }, - { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287, upload-time = "2026-05-10T18:15:26.226Z" }, - { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202, upload-time = "2026-05-10T18:15:27.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517, upload-time = "2026-05-10T18:15:29.614Z" }, - { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878, upload-time = "2026-05-10T18:15:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070, upload-time = "2026-05-10T18:15:32.551Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918, upload-time = "2026-05-10T18:15:33.678Z" }, - { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, - { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, - { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, - { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, - { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, - { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, - { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, - { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, - { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, - { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, - { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, - { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, - { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, - { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, - { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, - { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, - { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, - { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, - { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, - { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, - { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, - { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, - { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, - { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, - { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, - { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, - { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, - { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, - { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, - { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, - { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, - { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, - { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, - { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, - { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, - { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, - { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, - { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, - { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, - { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, - { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, - { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, - { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - -[[package]] -name = "mypy" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ast-serialize" }, - { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc", size = 14778792, upload-time = "2026-05-11T18:36:23.605Z" }, - { url = "https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849", size = 13645739, upload-time = "2026-05-11T18:37:22.752Z" }, - { url = "https://files.pythonhosted.org/packages/65/01/455c31b170e9468265074840bf18863a8482a24103fdaabe4e199392aa5f/mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd", size = 14074199, upload-time = "2026-05-11T18:35:09.292Z" }, - { url = "https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166", size = 14953128, upload-time = "2026-05-11T18:31:57.678Z" }, - { url = "https://files.pythonhosted.org/packages/7f/2f/a196f5331d96170ad3d28f144d2aba690d4b2911381f68d51e489c7ab82a/mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8", size = 15249378, upload-time = "2026-05-11T18:33:00.101Z" }, - { url = "https://files.pythonhosted.org/packages/54/de/94d321cc12da9f71341ac0c270efbed5c725750c7b4c334d957de9a087d9/mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8", size = 11060994, upload-time = "2026-05-11T18:33:18.848Z" }, - { url = "https://files.pythonhosted.org/packages/e1/62/0c27ca55219a7c764a7fb88c7bb2b7b2f9780ade8bbf16bc8ed8400eef6b/mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e", size = 9976743, upload-time = "2026-05-11T18:31:25.554Z" }, - { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" }, - { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" }, - { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" }, - { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" }, - { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" }, - { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, - { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, - { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, - { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, - { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, - { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, - { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, - { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, - { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, - { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, - { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, - { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, - { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, - { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, - { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, - { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, - { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, - { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - -[[package]] -name = "nodeenv" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, -] - -[[package]] -name = "packaging" -version = "26.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, -] - -[[package]] -name = "pallets-sphinx-themes" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "sphinx-notfound-page" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/60/1ee4b7dfce36150cea485b6d47358d58e59e96e8c5bdadee8030d483ce38/pallets_sphinx_themes-2.5.0.tar.gz", hash = "sha256:ef8609704b80e8a5d508a88adeb0559234311a1cf3362427aca9d283b22c80ef", size = 216582, upload-time = "2026-02-25T16:28:22.83Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/b4/313608ebc395ff81ef2805790cddf626867caff7c40cb3204658830623e3/pallets_sphinx_themes-2.5.0-py3-none-any.whl", hash = "sha256:5a41191d4d5dd9ac9cd7a17b14ebdea700626d8a7a708ff66b1c6e1e4ee8b4da", size = 144591, upload-time = "2026-02-25T16:28:21.413Z" }, -] - -[[package]] -name = "pathspec" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.9.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "pre-commit" -version = "4.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, -] - -[[package]] -name = "pre-commit-uv" -version = "4.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pre-commit" }, - { name = "uv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/da/dafb3c4d282e316082267ae0d0eec5a3f746bf7238f5c1f13a6a29f16356/pre_commit_uv-4.2.1.tar.gz", hash = "sha256:a67f320aa2479cebf1294defc1682a4b37b7d010fa2cf773b58036d6474dee1b", size = 7107, upload-time = "2026-02-18T04:59:54.232Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/89/a5275bea3e80ae9c67d5209d658bb61fae2c0683cf4e35cce4084e00871a/pre_commit_uv-4.2.1-py3-none-any.whl", hash = "sha256:81207f923afdd5e1f1f2d19bae91f40fe825c7a81d789fff54cdb240e67d6374", size = 5688, upload-time = "2026-02-18T04:59:52.738Z" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pygments" -version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, -] - -[[package]] -name = "pyproject-api" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330", size = 22785, upload-time = "2025-10-09T19:12:27.21Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09", size = 13218, upload-time = "2025-10-09T19:12:24.428Z" }, -] - -[[package]] -name = "pyright" -version = "1.1.409" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/4e/3aa27f74211522dba7e9cbc3e74de779c6d4b654c54e50a4840623be8014/pyright-1.1.409.tar.gz", hash = "sha256:986ee05beca9e077c165758ad123667c679e050059a2546aa02473930394bc93", size = 4430434, upload-time = "2026-04-23T11:02:03.799Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/6b/330d8ebae582b30c2959a1ef4c3bc344ebde48c2ff0c3f113c4710735e11/pyright-1.1.409-py3-none-any.whl", hash = "sha256:aa3ea228cab90c845c7a60d28db7a844c04315356392aa09fafcee98c8c22fb3", size = 6438161, upload-time = "2026-04-23T11:02:01.309Z" }, -] - -[[package]] -name = "pytest" -version = "9.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, -] - -[[package]] -name = "python-discovery" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/60/e88788207d81e46362cfbef0d4aaf4c0f49efc3c12d4c3fa3f542c34ebec/python_discovery-1.3.1.tar.gz", hash = "sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6", size = 68011, upload-time = "2026-05-12T20:53:36.336Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl", hash = "sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c", size = 33185, upload-time = "2026-05-12T20:53:34.969Z" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "requests" -version = "2.34.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, -] - -[[package]] -name = "roman-numerals" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, -] - -[[package]] -name = "roman-numerals-py" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "roman-numerals", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/de96fca640f4f656eb79bbee0e79aeec52e3e0e359f8a3e6a0d366378b64/roman_numerals_py-4.1.0.tar.gz", hash = "sha256:f5d7b2b4ca52dd855ef7ab8eb3590f428c0b1ea480736ce32b01fef2a5f8daf9", size = 4274, upload-time = "2025-12-17T18:25:41.153Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/2c/daca29684cbe9fd4bc711f8246da3c10adca1ccc4d24436b17572eb2590e/roman_numerals_py-4.1.0-py3-none-any.whl", hash = "sha256:553114c1167141c1283a51743759723ecd05604a1b6b507225e91dc1a6df0780", size = 4547, upload-time = "2025-12-17T18:25:40.136Z" }, -] - -[[package]] -name = "ruff" -version = "0.15.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" }, - { url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" }, - { url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" }, - { url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" }, - { url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" }, - { url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" }, - { url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" }, - { url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" }, - { url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" }, - { url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" }, - { url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" }, - { url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, -] - -[[package]] -name = "snowballstemmer" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, -] - -[[package]] -name = "sphinx" -version = "8.1.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "alabaster", marker = "python_full_version < '3.11'" }, - { name = "babel", marker = "python_full_version < '3.11'" }, - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.11'" }, - { name = "imagesize", marker = "python_full_version < '3.11'" }, - { name = "jinja2", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "requests", marker = "python_full_version < '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, -] - -[[package]] -name = "sphinx" -version = "8.2.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version >= '3.12' and python_full_version < '3.15'", - "python_full_version == '3.11.*'", -] -dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.11'" }, - { name = "babel", marker = "python_full_version >= '3.11'" }, - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.11'" }, - { name = "imagesize", marker = "python_full_version >= '3.11'" }, - { name = "jinja2", marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "requests", marker = "python_full_version >= '3.11'" }, - { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, -] - -[[package]] -name = "sphinx-autobuild" -version = "2024.10.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "starlette", marker = "python_full_version < '3.11'" }, - { name = "uvicorn", marker = "python_full_version < '3.11'" }, - { name = "watchfiles", marker = "python_full_version < '3.11'" }, - { name = "websockets", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023, upload-time = "2024-10-02T23:15:30.172Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/c0/eba125db38c84d3c74717008fd3cb5000b68cd7e2cbafd1349c6a38c3d3b/sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa", size = 11908, upload-time = "2024-10-02T23:15:28.739Z" }, -] - -[[package]] -name = "sphinx-autobuild" -version = "2025.8.25" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version >= '3.12' and python_full_version < '3.15'", - "python_full_version == '3.11.*'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "starlette", marker = "python_full_version >= '3.11'" }, - { name = "uvicorn", marker = "python_full_version >= '3.11'" }, - { name = "watchfiles", marker = "python_full_version >= '3.11'" }, - { name = "websockets", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" }, -] - -[[package]] -name = "sphinx-notfound-page" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/b2/67603444a8ee97b4a8ea71b0a9d6bab1727ed65e362c87e02f818ee57b8a/sphinx_notfound_page-1.1.0.tar.gz", hash = "sha256:913e1754370bb3db201d9300d458a8b8b5fb22e9246a816643a819a9ea2b8067", size = 7392, upload-time = "2025-01-28T18:45:02.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/d4/019fe439c840a7966012bbb95ccbdd81c5c10271749706793b43beb05145/sphinx_notfound_page-1.1.0-py3-none-any.whl", hash = "sha256:835dc76ff7914577a1f58d80a2c8418fb6138c0932c8da8adce4d9096fbcd389", size = 8167, upload-time = "2025-01-28T18:45:00.465Z" }, -] - -[[package]] -name = "sphinx-tabs" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "pygments" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/30/ca5b0de830f369968d8e3483dd45a8908fd10169c05cd9837f0bd075982e/sphinx_tabs-3.5.0.tar.gz", hash = "sha256:91dba1187e4c35fd37380a56ac228bbd54c6c649b2351829f3bf033718277537", size = 17006, upload-time = "2026-03-03T23:00:30.404Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl", hash = "sha256:154be49de4d5c8249ea08c5d9bf88ca8f9c31e00a178305a93cbc33e000339e5", size = 9871, upload-time = "2026-03-03T23:00:28.89Z" }, -] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, -] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, -] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, -] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, -] - -[[package]] -name = "sphinxcontrib-log-cabinet" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/75/26/0687391e10c605a4d0c7ebe118c57c51ecc687128bcdae5803d9b96def81/sphinxcontrib-log-cabinet-1.0.1.tar.gz", hash = "sha256:103b2e62df4e57abb943bea05ee9c2beb7da922222c8b77314ffd6ab9901c558", size = 4072, upload-time = "2019-07-05T23:22:34.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/e7/dbfc155c1b4c429a9a8149032a56bfb7bab4efabc656abb24ab4619c715d/sphinxcontrib_log_cabinet-1.0.1-py2.py3-none-any.whl", hash = "sha256:3decc888e8e453d1912cd95d50efb0794a4670a214efa65e71a7de277dcfe2cd", size = 4887, upload-time = "2019-07-05T23:22:32.969Z" }, -] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, -] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, -] - -[[package]] -name = "starlette" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, -] - -[[package]] -name = "tomli" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, - { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, - { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, - { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, - { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, - { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, - { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, - { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, - { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, - { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, - { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, - { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, - { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, - { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, - { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, -] - -[[package]] -name = "tomli-w" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, -] - -[[package]] -name = "tox" -version = "4.54.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cachetools" }, - { name = "colorama" }, - { name = "filelock" }, - { name = "packaging" }, - { name = "platformdirs" }, - { name = "pluggy" }, - { name = "pyproject-api" }, - { name = "python-discovery" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "tomli-w" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/2c/7ca5edb5ecd6bcc5cc926fe87e62a84dcd3cbd03a32f9d0bee98d2bee7cf/tox-4.54.0.tar.gz", hash = "sha256:21e36fd8256590379620848d0b03b52f4d541b65b749de1a17c3e616978dad58", size = 279256, upload-time = "2026-05-12T19:13:05.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/18/20cf56a76c5d6117547179db9b5d31cc56e3e90507d1b0b748da74aa95c5/tox-4.54.0-py3-none-any.whl", hash = "sha256:a2d7c1177242ae9c3d9e404039e9f945ce16a3e5dfc66972c643e27d7e764f4b", size = 214527, upload-time = "2026-05-12T19:13:04.334Z" }, -] - -[[package]] -name = "tox-uv" -version = "1.35.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tox-uv-bare" }, - { name = "uv" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/dc/6e9994c799bdbb309f829dd6b8d98764dd0757302f3433c380438a3a127b/tox_uv-1.35.2-py3-none-any.whl", hash = "sha256:2d99b0e3c782ba49e7cbe521c8d344758595961b17a3633738d67096641c1bde", size = 6565, upload-time = "2026-05-05T01:34:16.07Z" }, -] - -[[package]] -name = "tox-uv-bare" -version = "1.35.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "tox" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0a/cb/168dc1ccf24e4065a9a0a33df55709ed2b5eb73bd2b13ddd53187e5dffb8/tox_uv_bare-1.35.2.tar.gz", hash = "sha256:49e28a804c97f23ea17e25859960c0fa78f35bccb7e14344cfd840e89a9aade9", size = 32333, upload-time = "2026-05-05T01:34:18.916Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/53/4a33dc81da39db7b31e5622333df361e8fe055b7ec636bd5fea762c9182d/tox_uv_bare-1.35.2-py3-none-any.whl", hash = "sha256:c0d590a41d1054a1ad0874e9e5943ff52402786e3d4599d8f8d37a65b566ef53", size = 22307, upload-time = "2026-05-05T01:34:17.681Z" }, -] - -[[package]] -name = "types-contextvars" -version = "2.4.7.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/20/6a0271fe78050f15eaad21f94a4efebbddcfd0cadac0d35b056e8d32b40f/types-contextvars-2.4.7.3.tar.gz", hash = "sha256:a15a1624c709d04974900ea4f8c4fc2676941bf7d4771a9c9c4ac3daa0e0060d", size = 3166, upload-time = "2023-07-07T09:16:39.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/5e/770b3dd271a925b4428ee17664536d037695698eb764b4c5ed6fe815169b/types_contextvars-2.4.7.3-py3-none-any.whl", hash = "sha256:bcd8e97a5b58e76d20f5cc161ba39b29b60ac46dcc6edf3e23c1d33f99b34351", size = 2752, upload-time = "2023-07-07T09:16:37.855Z" }, -] - -[[package]] -name = "types-dataclasses" -version = "0.6.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/6a/dec8fbc818b1e716cb2d9424f1ea0f6f3b1443460eb6a70d00d9d8527360/types-dataclasses-0.6.6.tar.gz", hash = "sha256:4b5a2fcf8e568d5a1974cd69010e320e1af8251177ec968de7b9bb49aa49f7b9", size = 2884, upload-time = "2022-06-30T09:49:21.449Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/85/23ab2bbc280266af5bf22ded4e070946d1694d1721ced90666b649eaa795/types_dataclasses-0.6.6-py3-none-any.whl", hash = "sha256:a0a1ab5324ba30363a15c9daa0f053ae4fff914812a1ebd8ad84a08e5349574d", size = 2868, upload-time = "2022-06-30T09:49:19.977Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "urllib3" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, -] - -[[package]] -name = "uv" -version = "0.11.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/34/609d5d01ba21dc8f0974610ca7802fbb2c946a0c38665cfe5c5aeddbefb5/uv-0.11.15.tar.gz", hash = "sha256:755f959ec6a2fd8ccb6ee76ad90ab759d2eb1f4797444078645dd1ee4bca92d6", size = 4159545, upload-time = "2026-05-18T19:57:48.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/7c/dcc230c5911884d8848145dabcac8fb95a5ed6f9fe1c57fae8242618f28a/uv-0.11.15-py3-none-linux_armv6l.whl", hash = "sha256:83b04ab49514a0a761ffedb36a748ee81f87746671e72088e5f32c9585e5f1a9", size = 23110183, upload-time = "2026-05-18T19:57:23.051Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f3/efd4e044b60eb9c3c12ee386be098d56c335538ccec7caa49349cfba9344/uv-0.11.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cae61f737be075b90be9e3f07d961072aed7019f4c9b8ed5c5d41c4d6cade3", size = 22637941, upload-time = "2026-05-18T19:57:26.752Z" }, - { url = "https://files.pythonhosted.org/packages/a6/b8/48627f895a1569e576822e0a8416aa4797eb4a4551de21a4ad97b9b5819d/uv-0.11.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9accae33619a9166e5c48531deb455d672cfb89f9357a00975e669c76b0bd49f", size = 21258803, upload-time = "2026-05-18T19:57:05.473Z" }, - { url = "https://files.pythonhosted.org/packages/af/50/4bc8a148274feabee2d9c9f1fa15009e10c0228dfe57981ee3ea2ef1d481/uv-0.11.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c0cf52cd6d50bb9e05e2d968f45f80761107e4cbc8d4a26d9758f9d8274aaec1", size = 23066178, upload-time = "2026-05-18T19:57:33.058Z" }, - { url = "https://files.pythonhosted.org/packages/a9/56/139fc3bec9a8b0a25bfe2196123adb9f16124da437bf4fbcf0d21cfcafb2/uv-0.11.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:49dc6ed70bff00937384f96cdc4b1a4742d18e5504ec2c4a1214dba2dee5687a", size = 22705332, upload-time = "2026-05-18T19:57:36.714Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b0/b18b3dd204f8c213236a1ebd148e009861637129a8cce34df0e9aa22ed40/uv-0.11.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:adb9a89352539fdd8f7cd5f9966cf9f94fc5b98e0ccdf5003a04123dc6423bec", size = 22707534, upload-time = "2026-05-18T19:58:04.117Z" }, - { url = "https://files.pythonhosted.org/packages/76/36/3ca09f95572df99d361b49c96b1297149e96e120d8d1ecf074095a4b6da4/uv-0.11.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40ff67e3f8e8a7533781a2e892a534975a93acb83ea35460e64e7b2bf2111774", size = 24096607, upload-time = "2026-05-18T19:58:11.625Z" }, - { url = "https://files.pythonhosted.org/packages/64/be/3bdee21a296bbf5336a526e3613d0e7d4538dacc39c62d7fcba55d15f6b0/uv-0.11.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6463a299ed7e6b5a800ed6f108af8e1588352629424133ddef7572b0e1e1118", size = 25082562, upload-time = "2026-05-18T19:57:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/cd/73/f371f3689ffe741066468d001d85f739fc4b5574de83b639ef19b5e8a7f4/uv-0.11.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68c1e62d4b78578b90b833553286b65d6a7e327537716441068583ba652ec4f5", size = 24253391, upload-time = "2026-05-18T19:57:18.47Z" }, - { url = "https://files.pythonhosted.org/packages/d3/16/fe392d618af6b00c064b3e718d585dcf791546a77c5123a5bec07ce53a0a/uv-0.11.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98edf1bdaf82447014852051d93e3ee95012509c567bf057fd117e6bdbd9a807", size = 24415871, upload-time = "2026-05-18T19:58:19.651Z" }, - { url = "https://files.pythonhosted.org/packages/6e/24/2e92a052fb6334fcd746d1c7cb57847c204b118c84f5da53c0f9e129f7b7/uv-0.11.15-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:be8f76d25bcf4c92bb384240ac1bf9aa7f51063d0bdeca4c9cf0ec3ed8b145e0", size = 23159007, upload-time = "2026-05-18T19:57:10.653Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2e/6923d0658d164bb2c435ed1868aa2d49b3074594679917a001ff92dc95bb/uv-0.11.15-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:f9f4fbbf4fe485522054f3c7496c6e8e932d6436e4200ff3daf718db0b7c7bd5", size = 23769385, upload-time = "2026-05-18T19:58:15.856Z" }, - { url = "https://files.pythonhosted.org/packages/a4/99/7e34cd949e57360814e8064cc9fb7104df445d0f6a663504e5f7473480aa/uv-0.11.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0ed920e896b2fd13a35031707e307e42fbb2681458b967440a17272d86d49137", size = 23860973, upload-time = "2026-05-18T19:57:55.575Z" }, - { url = "https://files.pythonhosted.org/packages/28/98/8fe1f5f9d816e94569a0298dd8e0936801097625fa1952162951f0d628b6/uv-0.11.15-py3-none-musllinux_1_1_i686.whl", hash = "sha256:41d907611f3e6a13262807fd7f0a17849f76285ca80f536f6b3943732bdc6656", size = 23431392, upload-time = "2026-05-18T19:57:59.814Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6b/76a1ce2fa860026913a5941700cdc7d715fce9c3277a3fa3489cf2523ca0/uv-0.11.15-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e3b68f8bf1a4568710f77e5bda9182ce7682811d89a8e7468c22460e032b234d", size = 24519478, upload-time = "2026-05-18T19:57:51.165Z" }, - { url = "https://files.pythonhosted.org/packages/43/60/1d58e8a05718cb50494763115710b73846cacb651fd735d285233fd72c59/uv-0.11.15-py3-none-win32.whl", hash = "sha256:8e2da3076761086a5b76869c3f38ef0509c836046ef41ddd19485dfd7271dca9", size = 22020178, upload-time = "2026-05-18T19:58:07.64Z" }, - { url = "https://files.pythonhosted.org/packages/55/53/40fcefcb348af660488597ed3c01363df7344e60611f8883750dc596f5c6/uv-0.11.15-py3-none-win_amd64.whl", hash = "sha256:cc3915ab291a1ecaf31de05f5d8bd70d09c66fe9911a53f70d9efa62ff0dbd8a", size = 24668779, upload-time = "2026-05-18T19:57:44.894Z" }, - { url = "https://files.pythonhosted.org/packages/e5/7d/fa3a9960c95af9bbe2a629048760d0b9b4fead8ccd4f2235af747ec7cdf0/uv-0.11.15-py3-none-win_arm64.whl", hash = "sha256:4f39426a13dee24897aed60c4b98058c66f18bd983885ac5f4a54a04b24fbddf", size = 23198178, upload-time = "2026-05-18T19:57:14.68Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.47.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" }, -] - -[[package]] -name = "virtualenv" -version = "21.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, - { name = "python-discovery" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl", hash = "sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3", size = 7594554, upload-time = "2026-05-13T18:01:27.815Z" }, -] - -[[package]] -name = "watchfiles" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/5a/2bf22ecb24916983bf1cc0095e7dea2741d14d6553b0d6a2ac8bc96eca93/watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9", size = 400471, upload-time = "2026-05-18T04:31:08.908Z" }, - { url = "https://files.pythonhosted.org/packages/55/70/dea1f6a0e76607841a60fb51af150e70124864673f61704abb62b90cdcc7/watchfiles-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4", size = 394599, upload-time = "2026-05-18T04:30:19.845Z" }, - { url = "https://files.pythonhosted.org/packages/18/52/752dcc7dc817baef5e89518732925795ce52e36a683a9a3c9fb68b21504e/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631", size = 455458, upload-time = "2026-05-18T04:30:29.126Z" }, - { url = "https://files.pythonhosted.org/packages/12/48/366ebbb22fcc504c2f72b45f0b7e72f40a18795cc01752c16066d597b67a/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994", size = 460513, upload-time = "2026-05-18T04:31:40.85Z" }, - { url = "https://files.pythonhosted.org/packages/ad/44/1f9e1b15e7a729062e0d0c3d0d7225ea4ab98b2267ef87287153be2495fc/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e", size = 493616, upload-time = "2026-05-18T04:30:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/7e/55/8b1086dcc8a1d6a697a62767bd7ea368e74c61c6fd171683cfe24a3fe5d2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19", size = 573154, upload-time = "2026-05-18T04:30:37.903Z" }, - { url = "https://files.pythonhosted.org/packages/14/7a/242f400cc77fafa7b18d53d19d9cb64fc6a6f61f28c55913bae7c674d92a/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8", size = 467046, upload-time = "2026-05-18T04:30:41.869Z" }, - { url = "https://files.pythonhosted.org/packages/02/c8/79eee650c62d2c186598489814468e389b5def0ebe755399ff645b35b1b2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07", size = 457100, upload-time = "2026-05-18T04:31:13.064Z" }, - { url = "https://files.pythonhosted.org/packages/81/36/519f6dbb7a95e4fe7c1513ed25b1520295ef9905a27f1f2226a73892bfb7/watchfiles-1.2.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551", size = 467038, upload-time = "2026-05-18T04:30:32.915Z" }, - { url = "https://files.pythonhosted.org/packages/2f/12/951af6b9f89097e02511122258402cb3578443021930b70cf968d6310dc0/watchfiles-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310", size = 632563, upload-time = "2026-05-18T04:30:11.539Z" }, - { url = "https://files.pythonhosted.org/packages/28/cc/0cba1f0a6117b7ec117271bdc3cb3a5a252005959755a2c09a745e0942cc/watchfiles-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df", size = 660851, upload-time = "2026-05-18T04:31:53.186Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f2/26347558cc8bf6877845e66b315f644d03c173906aa09e233a3f4fd23928/watchfiles-1.2.0-cp310-cp310-win32.whl", hash = "sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1", size = 277023, upload-time = "2026-05-18T04:30:18.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/68/a5e67b6b68e94f4c1511d61c46c55eba0737583620b6febf194c7b9cc23f/watchfiles-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d", size = 290107, upload-time = "2026-05-18T04:32:09.677Z" }, - { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, - { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, - { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, - { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, - { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, - { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, - { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, - { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, - { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, - { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, - { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, - { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, - { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, - { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, - { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, - { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, - { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, - { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, - { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, - { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, - { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, - { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, - { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, - { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, - { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, - { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, - { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, - { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, - { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, - { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, - { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, - { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, - { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, - { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, - { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, - { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, - { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, - { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, - { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, - { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, - { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, - { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, - { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, - { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, - { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, - { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, - { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, - { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, - { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, - { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, - { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, - { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, - { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, - { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, - { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, - { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, - { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, - { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, - { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, - { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, - { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, - { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, - { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, - { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, - { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, - { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, - { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, - { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, - { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, - { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, -] - -[[package]] -name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, - { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, - { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, - { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, - { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, - { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, - { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, -] - -[[package]] -name = "werkzeug" -version = "3.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, -]