diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..4519826 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,17 @@ +{ + "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 new file mode 100644 index 0000000..eaebea6 --- /dev/null +++ b/.devcontainer/on-create-command.sh @@ -0,0 +1,7 @@ +#!/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 new file mode 100644 index 0000000..2ff985a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +charset = utf-8 +max_line_length = 88 + +[*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}] +indent_size = 2 diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..0917c79 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,27 @@ +--- +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 new file mode 100644 index 0000000..3f27ac9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +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 + url: https://github.com/pallets/flask/discussions/ + about: Ask questions about your own code on the Discussions tab. + - name: Questions on + 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 new file mode 100644 index 0000000..52c2aed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,15 @@ +--- +name: Feature request +about: Suggest a new feature for Flask +--- + + + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1f47f12 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + groups: + github-actions: + patterns: + - '*' + - package-ecosystem: pip + directory: /requirements/ + schedule: + interval: monthly + groups: + python-requirements: + patterns: + - '*' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..eb124d2 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ + + + + + diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml new file mode 100644 index 0000000..22228a1 --- /dev/null +++ b/.github/workflows/lock.yaml @@ -0,0 +1,23 @@ +name: Lock inactive closed issues +# Lock closed issues that have not received any further activity for two weeks. +# This does not close open issues, only humans may do that. It is easier to +# respond to new issues with fresh examples rather than continuing discussions +# on old issues. + +on: + schedule: + - cron: '0 0 * * *' +permissions: + issues: write + pull-requests: write +concurrency: + group: lock +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 + with: + issue-inactive-days: 14 + pr-inactive-days: 14 + discussion-inactive-days: 14 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..da89199 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,73 @@ +name: Publish +on: + push: + tags: + - '*' +jobs: + build: + runs-on: ubuntu-latest + outputs: + hash: ${{ steps.hash.outputs.hash }} + steps: + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: '3.x' + cache: pip + cache-dependency-path: requirements*/*.txt + - run: pip install -r requirements/build.txt + # Use the commit date instead of the current date during the build. + - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV + - run: python -m build + # Generate hashes used for provenance. + - name: generate hash + id: hash + run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT + - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + with: + path: ./dist + provenance: + needs: [build] + permissions: + actions: read + id-token: write + contents: write + # Can't pin with hash due to how this workflow works. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 + with: + base64-subjects: ${{ needs.build.outputs.hash }} + create-release: + # Upload the sdist, wheels, and provenance to a GitHub release. They remain + # available as build artifacts for a while as well. + needs: [provenance] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + - name: create release + run: > + gh release create --draft --repo ${{ github.repository }} + ${{ github.ref_name }} + *.intoto.jsonl/* artifact/* + env: + GH_TOKEN: ${{ github.token }} + publish-pypi: + needs: [provenance] + # Wait for approval before attempting to upload to PyPI. This allows reviewing the + # files in the draft release. + 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@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + - uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14 + with: + repository-url: https://test.pypi.org/legacy/ + packages-dir: artifact/ + - uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14 + with: + packages-dir: artifact/ diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..2b9a162 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,60 @@ +name: Tests +on: + push: + branches: + - main + - '*.x' + paths-ignore: + - 'docs/**' + - '*.md' + - '*.rst' + pull_request: + paths-ignore: + - 'docs/**' + - '*.md' + - '*.rst' +jobs: + tests: + name: ${{ matrix.name || matrix.python }} + runs-on: ${{ matrix.os || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: + include: + - {python: '3.13'} + - {python: '3.12'} + - {name: Windows, python: '3.12', os: windows-latest} + - {name: Mac, python: '3.12', os: macos-latest} + - {python: '3.11'} + - {python: '3.10'} + - {python: '3.9'} + - {python: '3.8'} + - {name: PyPy, python: 'pypy-3.10', tox: pypy310} + - {name: Minimum Versions, python: '3.12', tox: py-min} + - {name: Development Versions, python: '3.8', tox: py-dev} + steps: + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: ${{ matrix.python }} + allow-prereleases: true + cache: pip + cache-dependency-path: requirements*/*.txt + - run: pip install tox + - run: tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }} + typing: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: '3.x' + cache: pip + cache-dependency-path: requirements*/*.txt + - name: cache mypy + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + with: + path: ./.mypy_cache + key: mypy|${{ hashFiles('pyproject.toml') }} + - run: pip install tox + - run: tox run -e typing diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62c1b88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.idea/ +.vscode/ +.venv*/ +venv*/ +__pycache__/ +dist/ +.coverage* +htmlcov/ +.tox/ +docs/_build/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ed8d790 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +ci: + autoupdate_schedule: monthly +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.3 + hooks: + - id: ruff + - id: ruff-format + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.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 new file mode 100644 index 0000000..865c685 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: 2 +build: + os: ubuntu-22.04 + tools: + python: '3.12' +python: + install: + - requirements: requirements/docs.txt + - method: pip + path: . +sphinx: + builder: dirhtml + fail_on_warning: true diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000..a992fc6 --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,1571 @@ +Version 3.1.0 +------------- + +Unreleased + + +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:`5230` +- 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. + + +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 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 Jinja2 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. +- 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 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/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f4ba197 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at report@palletsprojects.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..fed4497 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,238 @@ +How to contribute to Flask +========================== + +Thank you for considering contributing to Flask! + + +Support questions +----------------- + +Please don't use the issue tracker for this. The issue tracker is a tool +to address bugs and feature requests in Flask itself. Use one of the +following resources for questions about using Flask or issues with your +own code: + +- The ``#questions`` channel on our Discord chat: + https://discord.gg/pallets +- Ask on `Stack Overflow`_. Search with Google first using: + ``site:stackoverflow.com flask {search term, exception message, etc.}`` +- Ask on our `GitHub Discussions`_ for long term discussion or larger + questions. + +.. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent +.. _GitHub Discussions: https://github.com/pallets/flask/discussions + + +Reporting issues +---------------- + +Include the following information in your post: + +- Describe what you expected to happen. +- If possible, include a `minimal reproducible example`_ to help us + identify the issue. This also helps check that the issue is not with + your own code. +- Describe what actually happened. Include the full traceback if there + was an exception. +- List your Python and Flask versions. If possible, check if this + issue is already fixed in the latest releases or the latest code in + the repository. + +.. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example + + +Submitting patches +------------------ + +If there is not an open issue for what you want to submit, prefer +opening one for discussion before working on a PR. You can work on any +issue that doesn't have an open PR linked to it or a maintainer assigned +to it. These show up in the sidebar. No need to ask if you can work on +an issue that interests you. + +Include the following in your patch: + +- Use `Black`_ to format your code. This and other tools will run + automatically if you install `pre-commit`_ using the instructions + below. +- Include tests if your patch adds or changes code. Make sure the test + fails without your patch. +- Update any relevant docs pages and docstrings. Docs pages and + docstrings should be wrapped at 72 characters. +- Add an entry in ``CHANGES.rst``. Use the same style as other + entries. Also include ``.. versionchanged::`` inline changelogs in + relevant docstrings. + +.. _Black: https://black.readthedocs.io +.. _pre-commit: https://pre-commit.com + + +First time setup using GitHub Codespaces +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`GitHub Codespaces`_ creates a development environment that is already set up for the +project. By default it opens in Visual Studio Code for the Web, but this can +be changed in your GitHub profile settings to use Visual Studio Code or JetBrains +PyCharm on your local computer. + +- Make sure you have a `GitHub account`_. +- From the project's repository page, click the green "Code" button and then "Create + codespace on main". +- The codespace will be set up, then Visual Studio Code will open. However, you'll + need to wait a bit longer for the Python extension to be installed. You'll know it's + ready when the terminal at the bottom shows that the virtualenv was activated. +- Check out a branch and `start coding`_. + +.. _GitHub Codespaces: https://docs.github.com/en/codespaces +.. _devcontainer: https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers + +First time setup in your local environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Make sure you have a `GitHub account`_. +- Download and install the `latest version of git`_. +- Configure git with your `username`_ and `email`_. + + .. code-block:: text + + $ git config --global user.name 'your name' + $ git config --global user.email 'your email' + +- Fork Flask to your GitHub account by clicking the `Fork`_ button. +- `Clone`_ your fork locally, replacing ``your-username`` in the command below with + your actual username. + + .. code-block:: text + + $ git clone https://github.com/your-username/flask + $ cd flask + +- Create a virtualenv. Use the latest version of Python. + + - Linux/macOS + + .. code-block:: text + + $ python3 -m venv .venv + $ . .venv/bin/activate + + - Windows + + .. code-block:: text + + > py -3 -m venv .venv + > .venv\Scripts\activate + +- Install the development dependencies, then install Flask in editable mode. + + .. code-block:: text + + $ python -m pip install -U pip + $ pip install -r requirements/dev.txt && pip install -e . + +- Install the pre-commit hooks. + + .. code-block:: text + + $ pre-commit install --install-hooks + +.. _GitHub account: https://github.com/join +.. _latest version of git: https://git-scm.com/downloads +.. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git +.. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address +.. _Fork: https://github.com/pallets/flask/fork +.. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork + +.. _start coding: + +Start coding +~~~~~~~~~~~~ + +- Create a branch to identify the issue you would like to work on. If you're + submitting a bug or documentation fix, branch off of the latest ".x" branch. + + .. code-block:: text + + $ git fetch origin + $ git checkout -b your-branch-name origin/2.0.x + + If you're submitting a feature addition or change, branch off of the "main" branch. + + .. code-block:: text + + $ git fetch origin + $ git checkout -b your-branch-name origin/main + +- Using your favorite editor, make your changes, `committing as you go`_. + + - If you are in a codespace, you will be prompted to `create a fork`_ the first + time you make a commit. Enter ``Y`` to continue. + +- Include tests that cover any code changes you make. Make sure the test fails without + your patch. Run the tests as described below. +- Push your commits to your fork on GitHub and `create a pull request`_. Link to the + issue being addressed with ``fixes #123`` in the pull request description. + + .. code-block:: text + + $ git push --set-upstream origin your-branch-name + +.. _committing as you go: https://afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes +.. _create a fork: https://docs.github.com/en/codespaces/developing-in-codespaces/using-source-control-in-your-codespace#about-automatic-forking +.. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request + +.. _Running the tests: + +Running the tests +~~~~~~~~~~~~~~~~~ + +Run the basic test suite with pytest. + +.. code-block:: text + + $ pytest + +This runs the tests for the current environment, which is usually +sufficient. CI will run the full suite when you submit your pull +request. You can run the full test suite with tox if you don't want to +wait. + +.. code-block:: text + + $ tox + + +Running test coverage +~~~~~~~~~~~~~~~~~~~~~ + +Generating a report of lines that do not have test coverage can indicate +where to start contributing. Run ``pytest`` using ``coverage`` and +generate a report. + +If you are using GitHub Codespaces, ``coverage`` is already installed +so you can skip the installation command. + +.. code-block:: text + + $ pip install coverage + $ coverage run -m pytest + $ coverage html + +Open ``htmlcov/index.html`` in your browser to explore the report. + +Read more about `coverage `__. + + +Building the docs +~~~~~~~~~~~~~~~~~ + +Build the docs in the ``docs`` directory using Sphinx. + +.. code-block:: text + + $ cd docs + $ make html + +Open ``_build/html/index.html`` in your browser to view the docs. + +Read more about `Sphinx `__. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..9d227a0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,28 @@ +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/README.md b/README.md new file mode 100644 index 0000000..df4d41c --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# 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 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal 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 = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# 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) diff --git a/docs/_static/debugger.png b/docs/_static/debugger.png new file mode 100644 index 0000000..7d4181f Binary files /dev/null and b/docs/_static/debugger.png differ diff --git a/docs/_static/flask-horizontal.png b/docs/_static/flask-horizontal.png new file mode 100644 index 0000000..a0df2c6 Binary files /dev/null and b/docs/_static/flask-horizontal.png differ diff --git a/docs/_static/flask-vertical.png b/docs/_static/flask-vertical.png new file mode 100644 index 0000000..d1fd149 Binary files /dev/null and b/docs/_static/flask-vertical.png differ diff --git a/docs/_static/pycharm-run-config.png b/docs/_static/pycharm-run-config.png new file mode 100644 index 0000000..ad02554 Binary files /dev/null and b/docs/_static/pycharm-run-config.png differ diff --git a/docs/_static/shortcut-icon.png b/docs/_static/shortcut-icon.png new file mode 100644 index 0000000..4d3e6c3 Binary files /dev/null and b/docs/_static/shortcut-icon.png differ diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..1aa8048 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,717 @@ +API +=== + +.. module:: flask + +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. + + +Application Object +------------------ + +.. autoclass:: Flask + :members: + :inherited-members: + + +Blueprint Objects +----------------- + +.. autoclass:: Blueprint + :members: + :inherited-members: + +Incoming Request Data +--------------------- + +.. autoclass:: Request + :members: + :inherited-members: + :exclude-members: json_module + +.. attribute:: 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:`~flask.Request`. + + +Response Objects +---------------- + +.. autoclass:: flask.Response + :members: + :inherited-members: + :exclude-members: json_module + +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. + +To access the current session you can use the :class:`session` object: + +.. class:: session + + The session object works pretty much like an ordinary dict, with the + difference that it keeps track of modifications. + + This is a proxy. See :ref:`notes-on-proxies` for more information. + + The following attributes are interesting: + + .. attribute:: new + + ``True`` if the session is new, ``False`` otherwise. + + .. 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 + + .. 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 +----------------- + +.. versionadded:: 0.8 + +The session interface provides a simple way to replace the session +implementation that Flask is using. + +.. currentmodule:: flask.sessions + +.. autoclass:: SessionInterface + :members: + +.. autoclass:: SecureCookieSessionInterface + :members: + +.. autoclass:: SecureCookieSession + :members: + +.. autoclass:: NullSession + :members: + +.. autoclass:: SessionMixin + :members: + +.. 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``. + + +Test Client +----------- + +.. currentmodule:: flask.testing + +.. autoclass:: FlaskClient + :members: + + +Test CLI Runner +--------------- + +.. currentmodule:: flask.testing + +.. autoclass:: FlaskCliRunner + :members: + + +Application Globals +------------------- + +.. currentmodule:: flask + +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 +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 :class:`request` and :class:`session`. + +.. data:: g + + A namespace object that can store data during an + :doc:`application context `. This is an instance of + :attr:`Flask.app_ctx_globals_class`, which defaults to + :class:`ctx._AppCtxGlobals`. + + This is a good place to store resources during a request. For + example, a ``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 a proxy. See :ref:`notes-on-proxies` for more information. + + .. versionchanged:: 0.10 + Bound to the application context instead of the request context. + +.. autoclass:: flask.ctx._AppCtxGlobals + :members: + + +Useful Functions and Classes +---------------------------- + +.. data:: current_app + + A proxy to the application handling the current request. 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:`application context ` is pushed. This happens + automatically during requests and CLI commands. It can be controlled + manually with :meth:`~flask.Flask.app_context`. + + 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 + +.. autofunction:: redirect + +.. autofunction:: make_response + +.. autofunction:: after_this_request + +.. autofunction:: send_file + +.. autofunction:: send_from_directory + + +Message Flashing +---------------- + +.. autofunction:: flash + +.. 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 `` + +.. autofunction:: jsonify + +.. autofunction:: dumps + +.. autofunction:: dump + +.. autofunction:: loads + +.. autofunction:: load + +.. autoclass:: flask.json.provider.JSONProvider + :members: + :member-order: bysource + +.. autoclass:: flask.json.provider.DefaultJSONProvider + :members: + :member-order: bysource + +.. automodule:: flask.json.tag + + +Template Rendering +------------------ + +.. currentmodule:: flask + +.. autofunction:: render_template + +.. autofunction:: render_template_string + +.. autofunction:: stream_template + +.. autofunction:: stream_template_string + +.. autofunction:: get_template_attribute + +Configuration +------------- + +.. autoclass:: Config + :members: + + +Stream Helpers +-------------- + +.. autofunction:: stream_with_context + +Useful Internals +---------------- + +.. autoclass:: flask.ctx.RequestContext + :members: + +.. data:: flask.globals.request_ctx + + The current :class:`~flask.ctx.RequestContext`. If a request context + is not active, accessing attributes on this proxy will raise a + ``RuntimeError``. + + 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:`request` and :data:`session` instead. + +.. autoclass:: flask.ctx.AppContext + :members: + +.. data:: flask.globals.app_ctx + + The current :class:`~flask.ctx.AppContext`. If an app context is not + active, accessing attributes on this proxy will raise a + ``RuntimeError``. + + 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` and :data:`g` instead. + +.. autoclass:: flask.blueprints.BlueprintSetupState + :members: + +.. _core-signals-list: + +Signals +------- + +Signals are provided by the `Blinker`_ library. See :doc:`signals` for an introduction. + +.. _blinker: https://blinker.readthedocs.io/ + +.. data:: template_rendered + + 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 + 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) + +.. 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. + + Example subscriber:: + + def close_db_connection(sender, **extra): + session.close() + + from flask import request_tearing_down + request_tearing_down.connect(close_db_connection, app) + + 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. + +.. data:: appcontext_tearing_down + + 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. + + 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 +----------------- + +.. versionadded:: 0.7 + +.. currentmodule:: None + +.. autoclass:: flask.views.View + :members: + +.. autoclass:: flask.views.MethodView + :members: + +.. _url-route-registrations: + +URL Route Registrations +----------------------- + +Generally there are three ways to define rules for the routing system: + +1. You can use the :meth:`flask.Flask.route` decorator. +2. You can use the :meth:`flask.Flask.add_url_rule` function. +3. You can directly access the underlying Werkzeug 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 +string without a slash however a different converter can be specified as +well by using ````. + +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) +`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('/') + def index(): + pass + + @app.route('/') + def show_user(username): + pass + + @app.route('/post/') + def show_post(post_id): + 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 +apply: + +1. If a rule ends with a slash and is requested without a slash by the + user, the user is automatically redirected to the same page with a + trailing slash attached. +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 +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 +definition for a URL that accepts an optional page:: + + @app.route('/users/', defaults={'page': 1}) + @app.route('/users/page/') + def show_users(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 301 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 + +Here are the parameters that :meth:`~flask.Flask.route` and +: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 + 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 + 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 + 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 + 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 + implicitly added and handled by the standard request + handling. They have to be specified as keyword arguments. +=============== ========================================================== + + +View Function Options +--------------------- + +For internal usage the view functions can have some attributes attached to +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 + 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 + 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 + 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(): + if request.method == 'OPTIONS': + # custom options handling here + ... + return 'Hello World!' + index.provide_automatic_options = False + index.methods = ['GET', 'OPTIONS'] + + app.add_url_rule('/', index) + +.. 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 new file mode 100644 index 0000000..5509a9a --- /dev/null +++ b/docs/appcontext.rst @@ -0,0 +1,147 @@ +.. currentmodule:: flask + +The Application Context +======================= + +The application context keeps track of the application-level data during +a request, CLI command, or other activity. Rather than passing the +application around to each function, the :data:`current_app` and +:data:`g` proxies are accessed instead. + +This is similar to :doc:`/reqcontext`, which keeps track of +request-level data during a request. A corresponding application context +is pushed when a request context is pushed. + +Purpose of the Context +---------------------- + +The :class:`Flask` application object has attributes, such as +:attr:`~Flask.config`, that are useful to access within views and +:doc:`CLI commands `. 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. + +Flask solves this issue with the *application context*. Rather than +referring to an ``app`` directly, you use the :data:`current_app` +proxy, which points to the application handling the current activity. + +Flask automatically *pushes* an application context when handling a +request. View functions, error handlers, and other functions that run +during a request will have access to :data:`current_app`. + +Flask will also automatically push an app context when running CLI +commands registered with :attr:`Flask.cli` using ``@app.cli.command()``. + + +Lifetime of the Context +----------------------- + +The application context is created and destroyed as necessary. When a +Flask application begins handling a request, it pushes an application +context and a :doc:`request context `. When the request +ends it pops the request context then the application context. +Typically, an application context will have the same lifetime as a +request. + +See :doc:`/reqcontext` for more information about how the contexts work +and the full life cycle of a request. + + +Manually Push a Context +----------------------- + +If you try to access :data:`current_app`, or anything that uses it, +outside an application context, you'll get this error message: + +.. code-block:: pytb + + RuntimeError: Working outside of application context. + + This typically means that you attempted to use functionality that + needed to interface with the current application object in some way. + To solve this, set up an application context with app.app_context(). + +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, and everything that runs in the block will have access +to :data:`current_app`. :: + + 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 +configuring the application, it most likely indicates that you should +move that code into a view function or CLI command. + + +Storing Data +------------ + +The application context is a good place to store common data during a +request or CLI command. Flask provides the :data:`g object ` for this +purpose. It is a simple namespace object that has the same lifetime as +an application context. + +.. note:: + The ``g`` name stands for "global", but that is referring to the + data being global *within a context*. The data on ``g`` is lost + after the context ends, and it is not an appropriate place to store + data between requests. Use the :data:`session` or a database to + store data across requests. + +A common use for :data:`g` is to manage resources during a request. + +1. ``get_X()`` creates resource ``X`` if it does not exist, caching it + as ``g.X``. +2. ``teardown_X()`` closes or otherwise deallocates the resource if it + exists. It is registered as a :meth:`~Flask.teardown_appcontext` + handler. + +For example, you can manage a database connection using this pattern:: + + from flask import g + + def get_db(): + if 'db' not in g: + g.db = connect_to_database() + + return g.db + + @app.teardown_appcontext + def teardown_db(exception): + db = g.pop('db', None) + + if db is not None: + db.close() + +During a request, every call to ``get_db()`` will return the same +connection, and it will be closed automatically at the end of the +request. + +You can use :class:`~werkzeug.local.LocalProxy` to make a new context +local from ``get_db()``:: + + from werkzeug.local import LocalProxy + db = LocalProxy(get_db) + +Accessing ``db`` will call ``get_db`` internally, in the same way that +:data:`current_app` works. + + +Events and Signals +------------------ + +The application will call functions registered with :meth:`~Flask.teardown_appcontext` +when the application context is popped. + +The following signals are sent: :data:`appcontext_pushed`, +:data:`appcontext_tearing_down`, and :data:`appcontext_popped`. diff --git a/docs/async-await.rst b/docs/async-await.rst new file mode 100644 index 0000000..06a29fc --- /dev/null +++ b/docs/async-await.rst @@ -0,0 +1,131 @@ +.. _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. + +.. admonition:: Using ``async`` on Windows on Python 3.8 + + Python 3.8 has a bug related to asyncio on Windows. If you encounter + something like ``ValueError: set_wakeup_fd only works in main thread``, + please upgrade to Python 3.9. + +.. admonition:: Using ``async`` with greenlet + + When using gevent or eventlet to serve an application or patch the + runtime, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is + required. + + +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 run Flask with Gevent or Eventlet +to get many of the benefits of async request handling. These libraries +patch low-level Python functions to accomplish this, whereas ``async``/ +``await`` and ASGI use standard, modern Python capabilities. Deciding +whether you should use Flask, Quart, or something else is ultimately up +to understanding the specific needs of your project. + +.. _Quart: https://github.com/pallets/quart +.. _ASGI: https://asgi.readthedocs.io/en/latest/ + + +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. diff --git a/docs/blueprints.rst b/docs/blueprints.rst new file mode 100644 index 0000000..d5cf3d8 --- /dev/null +++ b/docs/blueprints.rst @@ -0,0 +1,315 @@ +Modular Applications with Blueprints +==================================== + +.. currentmodule:: flask + +.. versionadded:: 0.7 + +Flask uses a concept of *blueprints* for making application components and +supporting common patterns within an application or across applications. +Blueprints can greatly simplify how large applications work and provide a +central means for Flask extensions to register operations on applications. +A :class:`Blueprint` object works similarly to a :class:`Flask` +application object, but it is not actually an application. Rather it is a +*blueprint* of how to construct or extend an application. + +Why Blueprints? +--------------- + +Blueprints in Flask are intended for these cases: + +* Factor an application into a set of blueprints. This is ideal for + larger applications; a project could instantiate an application object, + initialize several extensions, and register a collection of blueprints. +* Register a blueprint on an application at a URL prefix and/or subdomain. + Parameters in the URL prefix/subdomain become common view arguments + (with defaults) across all view functions in the blueprint. +* Register a blueprint multiple times on an application with different URL + rules. +* Provide template filters, static files, templates, and other utilities + through blueprints. A blueprint does not have to implement applications + or view functions. +* Register a blueprint on an application for any of these cases when + initializing a Flask extension. + +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. + +Blueprints instead provide separation at the Flask level, share +application config, and can change an application object as necessary with +being registered. The downside is that you cannot unregister a blueprint +once an application was created without having to destroy the whole +application object. + +The Concept of Blueprints +------------------------- + +The basic concept of blueprints is that they record operations to execute +when registered on an application. Flask associates view functions with +blueprints when dispatching requests and generating URLs from one endpoint +to another. + +My First Blueprint +------------------ + +This is what a very basic blueprint looks like. In this case we want to +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.route('/', defaults={'page': 'index'}) + @simple_page.route('/') + def show(page): + try: + return render_template(f'pages/{page}.html') + 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. +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. + +Registering Blueprints +---------------------- + +So how do you register that blueprint? Like this:: + + from flask import Flask + from yourapplication.simple_page import simple_page + + app = Flask(__name__) + app.register_blueprint(simple_page) + +If you check the rules registered on the application, you will find +these:: + + >>> app.url_map + Map([' (HEAD, OPTIONS, GET) -> static>, + ' (HEAD, OPTIONS, GET) -> simple_page.show>, + simple_page.show>]) + +The first one is obviously from the application itself 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 (``.``). + +Blueprints however can also be mounted at different locations:: + + app.register_blueprint(simple_page, url_prefix='/pages') + +And sure enough, these are the generated rules:: + + >>> app.url_map + Map([' (HEAD, OPTIONS, GET) -> static>, + ' (HEAD, OPTIONS, GET) -> 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 +------------------- + +Blueprints can provide resources as well. Sometimes you might want to +introduce a blueprint only for the resources it provides. + +Blueprint Resource Folder +````````````````````````` + +Like for regular applications, blueprints are considered to be contained +in a folder. While multiple blueprints can originate from the same folder, +it does not have to be the case and it's usually not recommended. + +The folder is inferred from the second argument to :class:`Blueprint` which +is usually `__name__`. This argument specifies what logical Python +module or package corresponds to the blueprint. If it points to an actual +Python package that package (which is a folder on the filesystem) is the +resource folder. If it's a module, the package the module is contained in +will be the resource folder. You can access the +:attr:`Blueprint.root_path` property to see what the resource folder is:: + + >>> simple_page.root_path + '/Users/username/TestProject/yourapplication' + +To quickly open sources from this folder you can use the +:meth:`~Blueprint.open_resource` function:: + + with simple_page.open_resource('static/style.css') as f: + code = f.read() + +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:: + + 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``. + +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:: + + 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 +````````` + +If you want the blueprint to expose templates you can do that by providing +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. + + +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. + +Building URLs +------------- + +If you want to link from one page to another you can use the +:func:`url_for` function just like you normally would do just that you +prefix the URL endpoint with the name of the blueprint and a dot (``.``):: + + url_for('admin.index') + +Additionally if you are in a view function of a blueprint or a rendered +template and you want to link to another endpoint of the same blueprint, +you can use relative redirects by prefixing the endpoint with a dot only:: + + url_for('.index') + +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/changes.rst b/docs/changes.rst new file mode 100644 index 0000000..955deaf --- /dev/null +++ b/docs/changes.rst @@ -0,0 +1,4 @@ +Changes +======= + +.. include:: ../CHANGES.rst diff --git a/docs/cli.rst b/docs/cli.rst new file mode 100644 index 0000000..a72e6d5 --- /dev/null +++ b/docs/cli.rst @@ -0,0 +1,556 @@ +.. 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 new file mode 100644 index 0000000..25b8f00 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,97 @@ +import packaging.version +from pallets_sphinx_themes import get_version +from pallets_sphinx_themes import ProjectLink + +# Project -------------------------------------------------------------- + +project = "Flask" +copyright = "2010 Pallets" +author = "Pallets" +release, version = get_version("Flask") + +# General -------------------------------------------------------------- + +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"), +} +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), +} + +# HTML ----------------------------------------------------------------- + +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"), + ] +} +html_sidebars = { + "index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"], + "**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"], +} +singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]} +html_static_path = ["_static"] +html_favicon = "_static/shortcut-icon.png" +html_logo = "_static/flask-vertical.png" +html_title = f"Flask Documentation ({version})" +html_show_sourcelink = False + +# Local Extensions ----------------------------------------------------- + + +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/" + + if text.endswith(">"): + words, text = text[:-1].rsplit("<", 1) + words = words.strip() + else: + words = None + + if packaging.version.parse(release).is_devrelease: + url = f"{base_url}main/{text}" + else: + url = f"{base_url}{release}/{text}" + + if words is None: + words = url + + from docutils.nodes import reference + from docutils.parsers.rst.roles import set_classes + + options = options or {} + set_classes(options) + node = reference(rawtext, words, refuri=url, **options) + return [node], [] + + +def setup(app): + app.add_role("gh", github_link) diff --git a/docs/config.rst b/docs/config.rst new file mode 100644 index 0000000..7828fb9 --- /dev/null +++ b/docs/config.rst @@ -0,0 +1,721 @@ +Configuration Handling +====================== + +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 +configuration in the code, which for many small applications is not +actually that bad, but there are better ways. + +Independent of how you load your config, there is a config object +available which holds the loaded configuration values: +The :attr:`~flask.Flask.config` attribute of the :class:`~flask.Flask` +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 +-------------------- + +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 + +Certain configuration values are also forwarded to the +:attr:`~flask.Flask` object so you can read and write them from there:: + + app.testing = True + +To update multiple keys at once you can use the :meth:`dict.update` +method:: + + app.config.update( + TESTING=True, + SECRET_KEY='192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' + ) + + +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 + 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:: 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`` + + .. 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_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:: SERVER_NAME + + Inform the application what host and port it is bound to. Required + for subdomain route matching support. + + If set, ``url_for`` can generate external URLs with only an application + context instead of a request context. + + Default: ``None`` + + .. 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 + + Don't read more than this many bytes from the incoming request data. If not + set and the request does not specify a ``CONTENT_LENGTH``, no data will be + read for security. + + Default: ``None`` + +.. 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. + +.. versionadded:: 0.4 + ``LOGGER_NAME`` + +.. versionadded:: 0.5 + ``SERVER_NAME`` + +.. versionadded:: 0.6 + ``MAX_CONTENT_LENGTH`` + +.. versionadded:: 0.7 + ``PROPAGATE_EXCEPTIONS``, ``PRESERVE_CONTEXT_ON_EXCEPTION`` + +.. versionadded:: 0.8 + ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS``, + ``APPLICATION_ROOT``, ``SESSION_COOKIE_DOMAIN``, + ``SESSION_COOKIE_PATH``, ``SESSION_COOKIE_HTTPONLY``, + ``SESSION_COOKIE_SECURE`` + +.. versionadded:: 0.9 + ``PREFERRED_URL_SCHEME`` + +.. versionadded:: 0.10 + ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_PRETTYPRINT_REGULAR`` + +.. 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. + + +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:: + + app = Flask(__name__) + app.config.from_object('yourapplication.default_settings') + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + +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: + +.. tabs:: + + .. group-tab:: Bash + + .. 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/ + +The configuration files themselves are actual Python files. Only values +in uppercase are actually stored in the config object later on. So make +sure to use uppercase letters for your config keys. + +Here is an example of a configuration file:: + + # Example configuration + SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' + +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 +methods on the config object as well to load from individual files. For a +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 +---------------------------- + +The downside with the approach mentioned earlier is that it makes testing +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. + That way you can create multiple instances of your application with + different configurations attached which makes unit testing 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 +------------------------ + +Most applications need more than one configuration. There should be at +least separate configurations for the production server and the one used +during development. The easiest way to handle this is to use a default +configuration that is always loaded and part of the version control, and a +separate configuration that overrides the values as necessary as mentioned +in the example above:: + + app = Flask(__name__) + 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 +``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 +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. + +An interesting pattern is also to use classes and inheritance for +configuration:: + + class Config(object): + TESTING = False + + class ProductionConfig(Config): + DATABASE_URI = 'mysql://user@localhost/foo' + + class DevelopmentConfig(Config): + DATABASE_URI = "sqlite:////tmp/foo.db" + + class TestingConfig(Config): + DATABASE_URI = 'sqlite:///:memory:' + TESTING = True + +To enable such a config you just have to call into +:meth:`~flask.Config.from_object`:: + + 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 + 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. + 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). + +.. _fabric: https://www.fabfile.org/ + + +.. _instance-folders: + +Instance Folders +---------------- + +.. versionadded:: 0.8 + +Flask 0.8 introduces instance folders. Flask for a long time made it +possible to refer to paths relative to the application's folder directly +(via :attr:`Flask.root_path`). This was also how many developers loaded +configurations stored next to the application. Unfortunately however this +only works well if applications are not packages in which case the root +path refers to the contents of the package. + +With Flask 0.8 a new attribute was introduced: +:attr:`Flask.instance_path`. It refers to a new concept called the +“instance folder”. The instance folder is designed to not be under +version control and be deployment specific. It's the perfect place to +drop things that either change at runtime or configuration files. + +You can either explicitly provide the path of the instance folder when +creating the Flask application or you can let Flask autodetect the +instance folder. For explicit configuration use the `instance_path` +parameter:: + + app = Flask(__name__, instance_path='/path/to/instance/folder') + +Please keep in mind that this path *must* be absolute when provided. + +If the `instance_path` parameter is not provided the following default +locations are used: + +- Uninstalled module:: + + /myapp.py + /instance + +- Uninstalled package:: + + /myapp + /__init__.py + /instance + +- Installed module or package:: + + $PREFIX/lib/pythonX.Y/site-packages/myapp + $PREFIX/var/myapp-instance + + ``$PREFIX`` is the prefix of your Python installation. This can be + ``/usr`` or the path to your virtualenv. You can print the value of + ``sys.prefix`` to see what the prefix is set to. + +Since the config object provided loading of configuration files from +relative filenames we made it possible to change the loading via filenames +to be relative to the instance path if wanted. The behavior of relative +paths in config files can be flipped between “relative to the application +root” (the default) to “relative to instance folder” via the +`instance_relative_config` switch to the application constructor:: + + 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 +folder if it exists:: + + app = Flask(__name__, instance_relative_config=True) + app.config.from_object('yourapplication.default_settings') + app.config.from_pyfile('application.cfg', silent=True) + +The path to the instance folder can be found via the +:attr:`Flask.instance_path`. Flask also provides a shortcut to open a +file from the instance folder with :meth:`Flask.open_instance_resource`. + +Example usage for both:: + + filename = os.path.join(app.instance_path, 'application.cfg') + with open(filename) as f: + config = f.read() + + # or via open_instance_resource: + with app.open_instance_resource('application.cfg') as f: + config = f.read() diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..e582053 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/docs/debugging.rst b/docs/debugging.rst new file mode 100644 index 0000000..f6b56ca --- /dev/null +++ b/docs/debugging.rst @@ -0,0 +1,99 @@ +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 new file mode 100644 index 0000000..bdeaf62 --- /dev/null +++ b/docs/deploying/apache-httpd.rst @@ -0,0 +1,66 @@ +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 new file mode 100644 index 0000000..1dc0aa2 --- /dev/null +++ b/docs/deploying/asgi.rst @@ -0,0 +1,27 @@ +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/eventlet.rst b/docs/deploying/eventlet.rst new file mode 100644 index 0000000..8a718b2 --- /dev/null +++ b/docs/deploying/eventlet.rst @@ -0,0 +1,80 @@ +eventlet +======== + +Prefer using :doc:`gunicorn` with eventlet workers rather than using +`eventlet`_ directly. Gunicorn provides a much more configurable and +production-tested server. + +`eventlet`_ 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``. + +:doc:`gevent` is another library that does the same thing. Certain +dependencies you have, or other considerations, may affect which of the +two you choose to use. + +eventlet provides a WSGI server that can handle many connections at once +instead of one per worker process. You must actually use eventlet in +your own code to see any benefit to using the server. + +.. _eventlet: https://eventlet.net/ +.. _greenlet: https://greenlet.readthedocs.io/en/latest/ + + +Installing +---------- + +When using eventlet, greenlet>=1.0 is required, otherwise context locals +such as ``request`` will not work as expected. When using PyPy, +PyPy>=7.3.7 is required. + +Create a virtualenv, install your application, then install +``eventlet``. + +.. code-block:: text + + $ cd hello-app + $ python -m venv .venv + $ . .venv/bin/activate + $ pip install . # install your application + $ pip install eventlet + + +Running +------- + +To use eventlet to serve your application, write a script that imports +its ``wsgi.server``, as well as your app or app factory. + +.. code-block:: python + :caption: ``wsgi.py`` + + import eventlet + from eventlet import wsgi + from hello import create_app + + app = create_app() + wsgi.server(eventlet.listen(("127.0.0.1", 8000)), app) + +.. code-block:: text + + $ python wsgi.py + (x) wsgi starting up on http://127.0.0.1:8000 + + +Binding Externally +------------------ + +eventlet 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 eventlet. + +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/gevent.rst b/docs/deploying/gevent.rst new file mode 100644 index 0000000..448b93e --- /dev/null +++ b/docs/deploying/gevent.rst @@ -0,0 +1,80 @@ +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``. + +:doc:`eventlet` is another library that does the same thing. Certain +dependencies you have, or other considerations, may affect which of the +two you choose to use. + +gevent provides a WSGI server that can handle many connections at once +instead of one per worker process. You must actually use gevent in your +own code to see any benefit to using the server. + +.. _gevent: https://www.gevent.org/ +.. _greenlet: https://greenlet.readthedocs.io/en/latest/ + + +Installing +---------- + +When using gevent, greenlet>=1.0 is required, otherwise context locals +such as ``request`` will not work as expected. 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 new file mode 100644 index 0000000..c50edc2 --- /dev/null +++ b/docs/deploying/gunicorn.rst @@ -0,0 +1,130 @@ +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 or eventlet. + +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 or eventlet +----------------------------- + +The default sync worker is appropriate for many use cases. If you need +asynchronous support, Gunicorn provides workers using either `gevent`_ +or `eventlet`_. This is not the same as Python's ``async/await``, or the +ASGI server spec. You must actually use gevent/eventlet in your own code +to see any benefit to using the workers. + +When using either gevent or eventlet, greenlet>=1.0 is required, +otherwise context locals such as ``request`` will not work as expected. +When using PyPy, PyPy>=7.3.7 is required. + +To use gevent: + +.. 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 + +To use eventlet: + +.. code-block:: text + + $ gunicorn -k eventlet 'hello:create_app()' + Starting gunicorn 20.1.0 + Listening at: http://127.0.0.1:8000 (x) + Using worker: eventlet + Booting worker with pid: x + +.. _gevent: https://www.gevent.org/ +.. _eventlet: https://eventlet.net/ diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst new file mode 100644 index 0000000..4135596 --- /dev/null +++ b/docs/deploying/index.rst @@ -0,0 +1,79 @@ +Deploying to Production +======================= + +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. + +"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.** + +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. + +.. toctree:: + :maxdepth: 1 + + gunicorn + waitress + mod_wsgi + uwsgi + gevent + eventlet + 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. diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst new file mode 100644 index 0000000..23e8227 --- /dev/null +++ b/docs/deploying/mod_wsgi.rst @@ -0,0 +1,94 @@ +mod_wsgi +======== + +`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. + +* 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. + +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. + +.. _mod_wsgi-express: https://pypi.org/project/mod-wsgi/ +.. _mod_wsgi: https://modwsgi.readthedocs.io/ +.. _Apache httpd: https://httpd.apache.org/ + + +Installing +---------- + +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. + +Create a virtualenv, install your application, then install +``mod_wsgi``. + +.. code-block:: text + + $ cd hello-app + $ python -m venv .venv + $ . .venv/bin/activate + $ pip install . # install your application + $ pip install mod_wsgi + + +Running +------- + +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. + +.. code-block:: python + :caption: ``wsgi.py`` + + from hello import app + + application = app + +.. code-block:: python + :caption: ``wsgi.py`` + + from hello import create_app + + application = create_app() + +Now run the ``mod_wsgi-express start-server`` command. + +.. code-block:: text + + $ 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 +------------------ + +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. + +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. + +.. code-block:: text + + $ sudo /home/hello/.venv/bin/mod_wsgi-express start-server \ + /home/hello/wsgi.py \ + --user hello --group hello --port 80 --processes 4 diff --git a/docs/deploying/nginx.rst b/docs/deploying/nginx.rst new file mode 100644 index 0000000..6b25c07 --- /dev/null +++ b/docs/deploying/nginx.rst @@ -0,0 +1,69 @@ +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/proxy_fix.rst b/docs/deploying/proxy_fix.rst new file mode 100644 index 0000000..e2c42e8 --- /dev/null +++ b/docs/deploying/proxy_fix.rst @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000..1f9d5ec --- /dev/null +++ b/docs/deploying/uwsgi.rst @@ -0,0 +1,145 @@ +uWSGI +===== + +`uWSGI`_ is a fast, compiled server suite with extensive configuration +and capabilities beyond a basic server. + +* 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. + +This page outlines the basics of running uWSGI. Be sure to read its +documentation to understand what features are available. + +.. _uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/ + + +Installing +---------- + +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. + +Create a virtualenv, install your application, then install ``pyuwsgi``. + +.. code-block:: text + + $ cd hello-app + $ python -m venv .venv + $ . .venv/bin/activate + $ pip install . # install your application + $ pip install pyuwsgi + +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. + +.. code-block:: text + + $ 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 +----------------- + +The default sync worker is appropriate for many use cases. If you need +asynchronous support, uWSGI provides a `gevent`_ worker. This is not the +same as Python's ``async/await``, or the ASGI server spec. You must +actually use gevent in your own code to see any benefit to using the +worker. + +When using gevent, greenlet>=1.0 is required, otherwise context locals +such as ``request`` will not work as expected. When using PyPy, +PyPy>=7.3.7 is required. + +.. code-block:: text + + $ 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] *** + + +.. _gevent: https://www.gevent.org/ diff --git a/docs/deploying/waitress.rst b/docs/deploying/waitress.rst new file mode 100644 index 0000000..aeafb9f --- /dev/null +++ b/docs/deploying/waitress.rst @@ -0,0 +1,75 @@ +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 revers +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 new file mode 100644 index 0000000..066cf10 --- /dev/null +++ b/docs/design.rst @@ -0,0 +1,228 @@ +Design Decisions in Flask +========================= + +If you are curious why Flask does certain things the way it does and not +differently, this section is for you. This should give you an idea about +some of the design decisions that may appear arbitrary and surprising at +first, especially in direct comparison with other frameworks. + + +The Explicit Application Object +------------------------------- + +A Python web application based on WSGI has to have one central callable +object that implements the actual application. In Flask this is an +instance of the :class:`~flask.Flask` class. Each Flask application has +to create an instance of this class itself and pass it the name of the +module, but why can't Flask do that itself? + +Without such an explicit application object the following code:: + + from flask import Flask + app = Flask(__name__) + + @app.route('/') + def index(): + return 'Hello World!' + +Would look like this instead:: + + from hypothetical_flask import route + + @route('/') + def index(): + return 'Hello World!' + +There are three major reasons for this. The most important one is that +implicit application objects require that there may only be one instance at +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 +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 +possible without hacks if the object were created ahead of time for you +based on a class that is not exposed to you. + +But there is another very important reason why Flask depends on an +explicit instantiation of that class: the package name. Whenever you +create a Flask instance you usually pass it `__name__` as package name. +Flask depends on that information to properly load resources relative +to your module. With Python's outstanding support for reflection it can +then access the package to figure out where the templates and static files +are stored (see :meth:`~flask.Flask.open_resource`). Now obviously there +are frameworks around that do not need any configuration and will still be +able to load templates relative to your application module. But they have +to use the current working directory for that, which is a very unreliable +way to determine where the application is. The current working directory +is process-wide and if you are running multiple applications in one +process (which could happen in a webserver without you knowing) the paths +will be off. Worse: many webservers do not set the working directory to +the directory of your application but to the document root which does not +have to be the same folder. + +The third reason is "explicit is better than implicit". That object is +your WSGI application, you don't have to remember anything else. If you +want to apply a WSGI middleware, just wrap it and you're done (though +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`). + +The Routing System +------------------ + +Flask uses the Werkzeug routing system which 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 +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. + + +One Template Engine +------------------- + +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 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 +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. 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, 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 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 +undertaking for a microframework like Flask. + +Furthermore extensions can then easily depend on one template language +being present. You can easily use your own templating language, but an +extension could still depend on Jinja itself. + + +What does "micro" mean? +----------------------- + +“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 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 +applications in Ruby land do not work with Rack directly, but on top of a +library with the same name. This Rack library has two equivalents in +Python: WebOb (formerly Paste) and Werkzeug. Paste is still around but +from my understanding it's sort of deprecated in favour of WebOb. The +development of WebOb and Werkzeug started side by side with similar ideas +in mind: be a good implementation of WSGI for other applications to take +advantage. + +Flask is a framework that takes advantage of the work already done by +Werkzeug to properly interface WSGI (which can be a complex task at +times). Thanks to recent developments in the Python package +infrastructure, packages with dependencies are no longer an issue and +there are very few reasons against having libraries that depend on others. + + +Thread Locals +------------- + +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? + +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. + + +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 Jinja2 to handle templating. +It also binds to a few common standard library packages such as logging. +Everything else is up for extensions. + +Why is this the case? Because people have different preferences and +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 new file mode 100644 index 0000000..faca58c --- /dev/null +++ b/docs/errorhandling.rst @@ -0,0 +1,523 @@ +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 +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 +- 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 + +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`. + +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. diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst new file mode 100644 index 0000000..c9dee5f --- /dev/null +++ b/docs/extensiondev.rst @@ -0,0 +1,303 @@ +Flask Extension Development +=========================== + +.. currentmodule:: flask + +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. + +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. + +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`_. + +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. + + +Naming +------ + +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. + +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. + +Here are some example install and import names: + +- ``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`` + + +The Extension Class and Initialization +-------------------------------------- + +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. + +.. code-block:: python + + class HelloExtension: + def __init__(self, app=None): + if app is not None: + self.init_app(app) + + def init_app(self, app): + app.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`. + +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. + +.. code-block:: python + + hello = HelloExtension() + + def create_app(): + app = Flask(__name__) + hello.init_app(app) + return app + +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. + +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. + + +Adding Behavior +--------------- + +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. + +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. + +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. + +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. + + +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 when a request context is, or when a CLI command is +run. If you're storing something that should be closed, use +:meth:`~flask.Flask.teardown_appcontext` to ensure that it gets closed +when the application 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 itself supports Python >=3.8 as of April 2023, but this will update over time. + +.. _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 diff --git a/docs/extensions.rst b/docs/extensions.rst new file mode 100644 index 0000000..4713ec8 --- /dev/null +++ b/docs/extensions.rst @@ -0,0 +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. + + +Finding Extensions +------------------ + +Flask extensions are usually named "Flask-Foo" or "Foo-Flask". You can +search PyPI for packages tagged with `Framework :: Flask `_. + + +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:: + + from flask_foo import Foo + + foo = Foo() + + app = Flask(__name__) + app.config.update( + FOO_BAR='baz', + FOO_SPAM='eggs', + ) + + foo.init_app(app) + + +Building Extensions +------------------- + +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. + + +.. _pypi: https://pypi.org/search/?c=Framework+%3A%3A+Flask diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..f9ab9bd --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,88 @@ +.. rst-class:: hide-header + +Welcome to Flask +================ + +.. image:: _static/flask-horizontal.png + :align: center + +Welcome to Flask's documentation. 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. + +Get started with :doc:`installation` +and then get an overview with the :doc:`quickstart`. There is also a +more detailed :doc:`tutorial/index` that shows how to create a small but +complete application with Flask. Common patterns are described in the +:doc:`patterns/index` section. The rest of the docs describe each +component of Flask in detail, with a full reference in the :doc:`api` +section. + +Flask depends on the `Werkzeug`_ WSGI toolkit, the `Jinja`_ template engine, and the +`Click`_ CLI toolkit. Be sure to check their documentation as well as Flask's when +looking for information. + +.. _Werkzeug: https://werkzeug.palletsprojects.com +.. _Jinja: https://jinja.palletsprojects.com +.. _Click: https://click.palletsprojects.com + + +User's Guide +------------ + +Flask provides configuration and conventions, with sensible defaults, to get started. +This section of the documentation explains the different parts of the Flask framework +and how they can be used, customized, and extended. Beyond Flask itself, look for +community-maintained extensions to add even more functionality. + +.. toctree:: + :maxdepth: 2 + + installation + quickstart + tutorial/index + templating + testing + errorhandling + debugging + logging + config + signals + views + lifecycle + appcontext + reqcontext + blueprints + extensions + cli + server + shell + patterns/index + web-security + deploying/index + async-await + + +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 +---------------- + +.. toctree:: + :maxdepth: 2 + + design + extensiondev + contributing + license + changes diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..aeb00ce --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,144 @@ +Installation +============ + + +Python Version +-------------- + +We recommend using the latest version of Python. Flask supports Python 3.8 and newer. + + +Dependencies +------------ + +These distributions will be installed automatically when installing Flask. + +* `Werkzeug`_ implements WSGI, the standard Python interface between + applications and servers. +* `Jinja`_ is a template language that renders the pages your application + serves. +* `MarkupSafe`_ comes with Jinja. It escapes untrusted input when rendering + templates to avoid injection attacks. +* `ItsDangerous`_ securely signs data to ensure its integrity. This is used + to protect Flask's session cookie. +* `Click`_ is a framework for writing command line applications. It provides + the ``flask`` command and allows adding custom management commands. +* `Blinker`_ provides support for :doc:`signals`. + +.. _Werkzeug: https://palletsprojects.com/p/werkzeug/ +.. _Jinja: https://palletsprojects.com/p/jinja/ +.. _MarkupSafe: https://palletsprojects.com/p/markupsafe/ +.. _ItsDangerous: https://palletsprojects.com/p/itsdangerous/ +.. _Click: https://palletsprojects.com/p/click/ +.. _Blinker: https://blinker.readthedocs.io/ + + +Optional dependencies +~~~~~~~~~~~~~~~~~~~~~ + +These distributions will not be installed automatically. Flask will detect and +use them if you install them. + +* `python-dotenv`_ enables support for :ref:`dotenv` when running ``flask`` + commands. +* `Watchdog`_ provides a faster, more efficient reloader for the development + server. + +.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme +.. _watchdog: https://pythonhosted.org/watchdog/ + + +greenlet +~~~~~~~~ + +You may choose to use gevent or eventlet with your application. In this +case, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is +required. + +These are not minimum supported versions, they only indicate the first +versions that added necessary features. You should use the latest +versions of each. + + +Virtual environments +-------------------- + +Use a virtual environment to manage the dependencies for your project, both in +development and in production. + +What problem does a virtual environment solve? The more Python projects you +have, the more likely it is that you need to work with different versions of +Python libraries, or even Python itself. Newer versions of libraries for one +project can break compatibility in another project. + +Virtual environments are independent groups of Python libraries, one for each +project. Packages installed for one project will not affect other projects or +the operating system's packages. + +Python comes bundled with the :mod:`venv` module to create virtual +environments. + + +.. _install-create-env: + +Create an environment +~~~~~~~~~~~~~~~~~~~~~ + +Create a project folder and a :file:`.venv` folder within: + +.. tabs:: + + .. group-tab:: macOS/Linux + + .. code-block:: text + + $ mkdir myproject + $ cd myproject + $ python3 -m venv .venv + + .. group-tab:: Windows + + .. code-block:: text + + > mkdir myproject + > cd myproject + > py -3 -m venv .venv + + +.. _install-activate-env: + +Activate the environment +~~~~~~~~~~~~~~~~~~~~~~~~ + +Before you work on your project, activate the corresponding environment: + +.. tabs:: + + .. group-tab:: macOS/Linux + + .. code-block:: text + + $ . .venv/bin/activate + + .. group-tab:: Windows + + .. code-block:: text + + > .venv\Scripts\activate + +Your shell prompt will change to show the name of the activated +environment. + + +Install Flask +------------- + +Within the activated environment, use the following command to install +Flask: + +.. code-block:: sh + + $ pip install Flask + +Flask is now installed. Check out the :doc:`/quickstart` or go to the +:doc:`Documentation Overview `. diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 0000000..2a445f9 --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,5 @@ +BSD-3-Clause License +==================== + +.. literalinclude:: ../LICENSE.txt + :language: text diff --git a/docs/lifecycle.rst b/docs/lifecycle.rst new file mode 100644 index 0000000..2344d98 --- /dev/null +++ b/docs/lifecycle.rst @@ -0,0 +1,168 @@ +Application Structure and Lifecycle +=================================== + +Flask makes it pretty easy to write a web application. But there are quite a few +different parts to an application and to each request it handles. Knowing what happens +during application setup, serving, and handling requests will help you know what's +possible in Flask and how to structure your application. + + +Application Setup +----------------- + +The first step in creating a Flask application is creating the application object. Each +Flask application is an instance of the :class:`.Flask` class, which collects all +configuration, extensions, and views. + +.. code-block:: python + + from flask import Flask + + app = Flask(__name__) + app.config.from_mapping( + SECRET_KEY="dev", + ) + app.config.from_prefixed_env() + + @app.route("/") + def index(): + return "Hello, World!" + +This is known as the "application setup phase", it's the code you write that's outside +any view functions or other handlers. It can be split up between different modules and +sub-packages, but all code that you want to be part of your application must be imported +in order for it to be registered. + +All application setup must be completed before you start serving your application and +handling requests. This is because WSGI servers divide work between multiple workers, or +can be distributed across multiple machines. If the configuration changed in one worker, +there's no way for Flask to ensure consistency between other workers. + +Flask tries to help developers catch some of these setup ordering issues by showing an +error if setup-related methods are called after requests are handled. In that case +you'll see this error: + + The setup method 'route' can no longer be called on the application. It has already + handled its first request, any changes will not be applied consistently. + Make sure all imports, decorators, functions, etc. needed to set up the application + are done before running it. + +However, it is not possible for Flask to detect all cases of out-of-order setup. In +general, don't do anything to modify the ``Flask`` app object and ``Blueprint`` objects +from within view functions that run during requests. This includes: + +- Adding routes, view functions, and other request handlers with ``@app.route``, + ``@app.errorhandler``, ``@app.before_request``, etc. +- Registering blueprints. +- Loading configuration with ``app.config``. +- Setting up the Jinja template environment with ``app.jinja_env``. +- Setting a session interface, instead of the default itsdangerous cookie. +- Setting a JSON provider with ``app.json``, instead of the default provider. +- Creating and initializing Flask extensions. + + +Serving the Application +----------------------- + +Flask is a WSGI application framework. The other half of WSGI is the WSGI server. During +development, Flask, through Werkzeug, provides a development WSGI server with the +``flask run`` CLI command. When you are done with development, use a production server +to serve your application, see :doc:`deploying/index`. + +Regardless of what server you're using, it will follow the :pep:`3333` WSGI spec. The +WSGI server will be told how to access your Flask application object, which is the WSGI +application. Then it will start listening for HTTP requests, translate the request data +into a WSGI environ, and call the WSGI application with that data. The WSGI application +will return data that is translated into an HTTP response. + +#. Browser or other client makes HTTP request. +#. WSGI server receives request. +#. WSGI server converts HTTP data to WSGI ``environ`` dict. +#. WSGI server calls WSGI application with the ``environ``. +#. Flask, the WSGI application, does all its internal processing to route the request + to a view function, handle errors, etc. +#. Flask translates View function return into WSGI response data, passes it to WSGI + server. +#. WSGI server creates and send an HTTP response. +#. Client receives the HTTP response. + + +Middleware +~~~~~~~~~~ + +The WSGI application above is a callable that behaves in a certain way. Middleware +is a WSGI application that wraps another WSGI application. It's a similar concept to +Python decorators. The outermost middleware will be called by the server. It can modify +the data passed to it, then call the WSGI application (or further middleware) that it +wraps, and so on. And it can take the return value of that call and modify it further. + +From the WSGI server's perspective, there is one WSGI application, the one it calls +directly. Typically, Flask is the "real" application at the end of the chain of +middleware. But even Flask can call further WSGI applications, although that's an +advanced, uncommon use case. + +A common middleware you'll see used with Flask is Werkzeug's +:class:`~werkzeug.middleware.proxy_fix.ProxyFix`, which modifies the request to look +like it came directly from a client even if it passed through HTTP proxies on the way. +There are other middleware that can handle serving static files, authentication, etc. + + +How a Request is Handled +------------------------ + +For us, the interesting part of the steps above is when Flask gets called by the WSGI +server (or middleware). At that point, it will do quite a lot to handle the request and +generate the response. At the most basic, it will match the URL to a view function, call +the view function, and pass the return value back to the server. But there are many more +parts that you can use to customize its behavior. + +#. WSGI server calls the Flask object, which calls :meth:`.Flask.wsgi_app`. +#. A :class:`.RequestContext` object is created. This converts the WSGI ``environ`` + dict into a :class:`.Request` object. It also creates an :class:`AppContext` object. +#. The :doc:`app context ` is pushed, which makes :data:`.current_app` and + :data:`.g` available. +#. The :data:`.appcontext_pushed` signal is sent. +#. The :doc:`request context ` is pushed, which makes :attr:`.request` and + :class:`.session` available. +#. The session is opened, loading any existing session data using the app's + :attr:`~.Flask.session_interface`, an instance of :class:`.SessionInterface`. +#. The URL is matched against the URL rules registered with the :meth:`~.Flask.route` + decorator during application setup. If there is no match, the error - usually a 404, + 405, or redirect - is stored to be handled later. +#. The :data:`.request_started` signal is sent. +#. Any :meth:`~.Flask.url_value_preprocessor` decorated functions are called. +#. Any :meth:`~.Flask.before_request` decorated functions are called. If any of + these function returns a value it is treated as the response immediately. +#. If the URL didn't match a route a few steps ago, that error is raised now. +#. The :meth:`~.Flask.route` decorated view function associated with the matched URL + is called and returns a value to be used as the response. +#. If any step so far raised an exception, and there is an :meth:`~.Flask.errorhandler` + decorated function that matches the exception class or HTTP error code, it is + called to handle the error and return a response. +#. Whatever returned a response value - a before request function, the view, or an + error handler, that value is converted to a :class:`.Response` object. +#. Any :func:`~.after_this_request` decorated functions are called, then cleared. +#. Any :meth:`~.Flask.after_request` decorated functions are called, which can modify + the response object. +#. The session is saved, persisting any modified session data using the app's + :attr:`~.Flask.session_interface`. +#. The :data:`.request_finished` signal is sent. +#. If any step so far raised an exception, and it was not handled by an error handler + function, it is handled now. HTTP exceptions are treated as responses with their + corresponding status code, other exceptions are converted to a generic 500 response. + The :data:`.got_request_exception` signal is sent. +#. The response object's status, headers, and body are returned to the WSGI server. +#. Any :meth:`~.Flask.teardown_request` decorated functions are called. +#. The :data:`.request_tearing_down` signal is sent. +#. The request context is popped, :attr:`.request` and :class:`.session` are no longer + available. +#. Any :meth:`~.Flask.teardown_appcontext` decorated functions are called. +#. The :data:`.appcontext_tearing_down` signal is sent. +#. The app context is popped, :data:`.current_app` and :data:`.g` are no longer + available. +#. The :data:`.appcontext_popped` signal is sent. + +There are even more decorators and customization points than this, but that aren't part +of every request lifecycle. They're more specific to certain things you might use during +a request, such as templates, building URLs, or handling JSON data. See the rest of this +documentation, as well as the :doc:`api` to explore further. diff --git a/docs/logging.rst b/docs/logging.rst new file mode 100644 index 0000000..3958824 --- /dev/null +++ b/docs/logging.rst @@ -0,0 +1,183 @@ +Logging +======= + +Flask uses standard Python :mod:`logging`. Messages about your Flask +application are logged with :meth:`app.logger `, +which takes the same name as :attr:`app.name `. This +logger can also be used to log your own messages. + +.. code-block:: python + + @app.route('/login', methods=['POST']) + def login(): + user = get_user(request.form['username']) + + if user.check_password(request.form['password']): + login_user(user) + app.logger.info('%s logged in successfully', user.username) + return redirect(url_for('index')) + else: + app.logger.info('%s failed to log in', user.username) + abort(401) + +If you don't configure logging, Python's default log level is usually +'warning'. Nothing below the configured level will be visible. + + +Basic Configuration +------------------- + +When you want to configure logging for your project, you should do it as soon +as possible when the program starts. If :meth:`app.logger ` +is accessed before logging is configured, it will add a default handler. If +possible, configure logging before creating the application object. + +This example uses :func:`~logging.config.dictConfig` to create a logging +configuration similar to Flask's default, except for all logs:: + + from logging.config import dictConfig + + dictConfig({ + 'version': 1, + 'formatters': {'default': { + 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', + }}, + 'handlers': {'wsgi': { + 'class': 'logging.StreamHandler', + 'stream': 'ext://flask.logging.wsgi_errors_stream', + 'formatter': 'default' + }}, + 'root': { + 'level': 'INFO', + 'handlers': ['wsgi'] + } + }) + + app = Flask(__name__) + + +Default Configuration +````````````````````` + +If you do not configure logging yourself, Flask will add a +:class:`~logging.StreamHandler` to :meth:`app.logger ` +automatically. During requests, it will write to the stream specified by the +WSGI server in ``environ['wsgi.errors']`` (which is usually +:data:`sys.stderr`). Outside a request, it will log to :data:`sys.stderr`. + + +Removing the Default Handler +```````````````````````````` + +If you configured logging after accessing +:meth:`app.logger `, and need to remove the default +handler, you can import and remove it:: + + from flask.logging import default_handler + + app.logger.removeHandler(default_handler) + + +Email Errors to Admins +---------------------- + +When running the application on a remote server for production, you probably +won't be looking at the log messages very often. The WSGI server will probably +send log messages to a file, and you'll only check that file if a user tells +you something went wrong. + +To be proactive about discovering and fixing bugs, you can configure a +:class:`logging.handlers.SMTPHandler` to send an email when errors and higher +are logged. :: + + import logging + from logging.handlers import SMTPHandler + + mail_handler = SMTPHandler( + mailhost='127.0.0.1', + fromaddr='server-error@example.com', + toaddrs=['admin@example.com'], + subject='Application Error' + ) + mail_handler.setLevel(logging.ERROR) + mail_handler.setFormatter(logging.Formatter( + '[%(asctime)s] %(levelname)s in %(module)s: %(message)s' + )) + + if not app.debug: + app.logger.addHandler(mail_handler) + +This requires that you have an SMTP server set up on the same server. See the +Python docs for more information about configuring the handler. + + +Injecting Request Information +----------------------------- + +Seeing more information about the request, such as the IP address, may help +debugging some errors. You can subclass :class:`logging.Formatter` to inject +your own fields that can be used in messages. You can change the formatter for +Flask's default handler, the mail handler defined above, or any other +handler. :: + + from flask import has_request_context, request + from flask.logging import default_handler + + class RequestFormatter(logging.Formatter): + def format(self, record): + if has_request_context(): + record.url = request.url + record.remote_addr = request.remote_addr + else: + record.url = None + record.remote_addr = None + + return super().format(record) + + formatter = RequestFormatter( + '[%(asctime)s] %(remote_addr)s requested %(url)s\n' + '%(levelname)s in %(module)s: %(message)s' + ) + default_handler.setFormatter(formatter) + mail_handler.setFormatter(formatter) + + +Other Libraries +--------------- + +Other libraries may use logging extensively, and you want to see relevant +messages from those logs too. The simplest way to do this is to add handlers +to the root logger instead of only the app logger. :: + + from flask.logging import default_handler + + root = logging.getLogger() + root.addHandler(default_handler) + root.addHandler(mail_handler) + +Depending on your project, it may be more useful to configure each logger you +care about separately, instead of configuring only the root logger. :: + + for logger in ( + logging.getLogger(app.name), + logging.getLogger('sqlalchemy'), + logging.getLogger('other_package'), + ): + logger.addHandler(default_handler) + logger.addHandler(mail_handler) + + +Werkzeug +```````` + +Werkzeug logs basic request/response information to the ``'werkzeug'`` logger. +If the root logger has no handlers configured, Werkzeug adds a +:class:`~logging.StreamHandler` to its logger. + + +Flask Extensions +```````````````` + +Depending on the situation, an extension may choose to log to +:meth:`app.logger ` or its own named logger. Consult each +extension's documentation for details. diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..922152e --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst new file mode 100644 index 0000000..f22c806 --- /dev/null +++ b/docs/patterns/appdispatch.rst @@ -0,0 +1,189 @@ +Application Dispatching +======================= + +Application dispatching is the process of combining multiple Flask +applications on the WSGI level. You can combine not only Flask +applications but any WSGI application. This would allow you to run a +Django and a Flask application in the same interpreter side by side if +you want. The usefulness of this depends on how the applications work +internally. + +The fundamental difference from :doc:`packages` is that in this case you +are running the same or different Flask applications that are entirely +isolated from each other. They run different configurations and are +dispatched on the WSGI level. + + +Working with this Document +-------------------------- + +Each of the techniques and examples below results in an ``application`` +object that can be run with any WSGI server. For development, use the +``flask run`` command to start a development server. For production, see +:doc:`/deploying/index`. + +.. code-block:: python + + from flask import Flask + + app = Flask(__name__) + + @app.route('/') + def hello_world(): + return 'Hello World!' + + +Combining Applications +---------------------- + +If you have entirely separated applications and you want them to work next +to each other in the same Python interpreter process you can take +advantage of the :class:`werkzeug.wsgi.DispatcherMiddleware`. The idea +here is that each Flask application is a valid WSGI application and they +are combined by the dispatcher middleware into a larger one that is +dispatched based on prefix. + +For example you could have your main application run on ``/`` and your +backend interface on ``/backend``. + +.. code-block:: python + + from werkzeug.middleware.dispatcher import DispatcherMiddleware + from frontend_app import application as frontend + from backend_app import application as backend + + application = DispatcherMiddleware(frontend, { + '/backend': backend + }) + + +Dispatch by Subdomain +--------------------- + +Sometimes you might want to use multiple instances of the same application +with different configurations. Assuming the application is created inside +a function and you can call that function to instantiate it, that is +really easy to implement. In order to develop your application to support +creating new instances in functions have a look at the +:doc:`appfactories` pattern. + +A very common example would be creating applications per subdomain. For +instance you configure your webserver to dispatch all requests for all +subdomains to your application and you then use the subdomain information +to create user-specific instances. Once you have your server set up to +listen on all subdomains you can use a very simple WSGI application to do +the dynamic application creation. + +The perfect level for abstraction in that regard is the WSGI layer. You +write your own WSGI application that looks at the request that comes and +delegates it to your Flask application. If that application does not +exist yet, it is dynamically created and remembered. + +.. code-block:: python + + from threading import Lock + + class SubdomainDispatcher: + + def __init__(self, domain, create_app): + self.domain = domain + self.create_app = create_app + self.lock = Lock() + self.instances = {} + + def get_application(self, host): + host = host.split(':')[0] + assert host.endswith(self.domain), 'Configuration error' + subdomain = host[:-len(self.domain)].rstrip('.') + with self.lock: + app = self.instances.get(subdomain) + if app is None: + app = self.create_app(subdomain) + self.instances[subdomain] = app + return app + + def __call__(self, environ, start_response): + app = self.get_application(environ['HTTP_HOST']) + return app(environ, start_response) + + +This dispatcher can then be used like this: + +.. code-block:: python + + from myapplication import create_app, get_user_for_subdomain + from werkzeug.exceptions import NotFound + + def make_app(subdomain): + user = get_user_for_subdomain(subdomain) + if user is None: + # if there is no user for that subdomain we still have + # to return a WSGI application that handles that request. + # We can then just return the NotFound() exception as + # application which will render a default 404 page. + # You might also redirect the user to the main page then + return NotFound() + + # otherwise create the application for the specific user + return create_app(user) + + application = SubdomainDispatcher('example.com', make_app) + + +Dispatch by Path +---------------- + +Dispatching by a path on the URL is very similar. Instead of looking at +the ``Host`` header to figure out the subdomain one simply looks at the +request path up to the first slash. + +.. code-block:: python + + from threading import Lock + from wsgiref.util import shift_path_info + + class PathDispatcher: + + def __init__(self, default_app, create_app): + self.default_app = default_app + self.create_app = create_app + self.lock = Lock() + self.instances = {} + + def get_application(self, prefix): + with self.lock: + app = self.instances.get(prefix) + if app is None: + app = self.create_app(prefix) + if app is not None: + self.instances[prefix] = app + return app + + def __call__(self, environ, start_response): + app = self.get_application(_peek_path_info(environ)) + if app is not None: + shift_path_info(environ) + else: + app = self.default_app + return app(environ, start_response) + + def _peek_path_info(environ): + segments = environ.get("PATH_INFO", "").lstrip("/").split("/", 1) + if segments: + return segments[0] + + return None + +The big difference between this and the subdomain one is that this one +falls back to another application if the creator function returns ``None``. + +.. code-block:: python + + from myapplication import create_app, default_app, get_user_for_prefix + + def make_app(prefix): + user = get_user_for_prefix(prefix) + if user is not None: + return create_app(user) + + application = PathDispatcher(default_app, make_app) diff --git a/docs/patterns/appfactories.rst b/docs/patterns/appfactories.rst new file mode 100644 index 0000000..32fd062 --- /dev/null +++ b/docs/patterns/appfactories.rst @@ -0,0 +1,118 @@ +Application Factories +===================== + +If you are already using packages and blueprints for your application +(:doc:`/blueprints`) there are a couple of really nice ways to further improve +the experience. A common pattern is creating the application object when +the blueprint is imported. But if you move the creation of this object +into a function, you can then create multiple instances of this app later. + +So why would you want to do this? + +1. Testing. You can have instances of the application with different + settings to test every case. +2. Multiple instances. Imagine you want to run different versions of the + same application. Of course you could have multiple instances with + different configs set up in your webserver, but if you use factories, + you can have multiple instances of the same application running in the + same application process which can be handy. + +So how would you then actually implement that? + +Basic Factories +--------------- + +The idea is to set up the application in a function. Like this:: + + def create_app(config_filename): + app = Flask(__name__) + app.config.from_pyfile(config_filename) + + from yourapplication.model import db + db.init_app(app) + + from yourapplication.views.admin import admin + from yourapplication.views.frontend import frontend + app.register_blueprint(admin) + app.register_blueprint(frontend) + + return app + +The downside is that you cannot use the application object in the blueprints +at import time. You can however use it from within a request. How do you +get access to the application with the config? Use +:data:`~flask.current_app`:: + + from flask import current_app, Blueprint, render_template + admin = Blueprint('admin', __name__, url_prefix='/admin') + + @admin.route('/') + def index(): + return render_template(current_app.config['INDEX_TEMPLATE']) + +Here we look up the name of a template in the config. + +Factories & Extensions +---------------------- + +It's preferable to create your extensions and app factories so that the +extension object does not initially get bound to the application. + +Using `Flask-SQLAlchemy `_, +as an example, you should not do something along those lines:: + + def create_app(config_filename): + app = Flask(__name__) + app.config.from_pyfile(config_filename) + + db = SQLAlchemy(app) + +But, rather, in model.py (or equivalent):: + + db = SQLAlchemy() + +and in your application.py (or equivalent):: + + def create_app(config_filename): + app = Flask(__name__) + app.config.from_pyfile(config_filename) + + from yourapplication.model import db + db.init_app(app) + +Using this design pattern, no application-specific state is stored on the +extension object, so one extension object can be used for multiple apps. +For more information about the design of extensions refer to :doc:`/extensiondev`. + +Using Applications +------------------ + +To run such an application, you can use the :command:`flask` command: + +.. code-block:: text + + $ flask --app hello run + +Flask will automatically detect the factory if it is named +``create_app`` or ``make_app`` in ``hello``. You can also pass arguments +to the factory like this: + +.. code-block:: text + + $ flask --app hello:create_app(local_auth=True) run + +Then the ``create_app`` factory in ``myapp`` is called with the keyword +argument ``local_auth=True``. See :doc:`/cli` for more detail. + +Factory Improvements +-------------------- + +The factory function above is not very clever, but you can improve it. +The following changes are straightforward to implement: + +1. Make it possible to pass in configuration values for unit tests so that + you don't have to create config files on the filesystem. +2. Call a function from a blueprint when the application is setting up so + that you have a place to modify attributes of the application (like + hooking in before/after request handlers etc.) +3. Add in WSGI middlewares when the application is being created if necessary. diff --git a/docs/patterns/caching.rst b/docs/patterns/caching.rst new file mode 100644 index 0000000..9bf7b72 --- /dev/null +++ b/docs/patterns/caching.rst @@ -0,0 +1,16 @@ +Caching +======= + +When your application runs slow, throw some caches in. Well, at least +it's the easiest way to speed up things. What does a cache do? Say you +have a function that takes some time to complete but the results would +still be good enough if they were 5 minutes old. So then the idea is that +you actually put the result of that calculation into a cache for some +time. + +Flask itself does not provide caching for you, but `Flask-Caching`_, an +extension for Flask does. Flask-Caching supports various backends, and it is +even possible to develop your own caching backend. + + +.. _Flask-Caching: https://flask-caching.readthedocs.io/en/latest/ diff --git a/docs/patterns/celery.rst b/docs/patterns/celery.rst new file mode 100644 index 0000000..2e9a43a --- /dev/null +++ b/docs/patterns/celery.rst @@ -0,0 +1,242 @@ +Background Tasks with Celery +============================ + +If your application has a long running task, such as processing some uploaded data or +sending email, you don't want to wait for it to finish during a request. Instead, use a +task queue to send the necessary data to another process that will run the task in the +background while the request returns immediately. + +`Celery`_ is a powerful task queue that can be used for simple background tasks as well +as complex multi-stage programs and schedules. This guide will show you how to configure +Celery using Flask. Read Celery's `First Steps with Celery`_ guide to learn how to use +Celery itself. + +.. _Celery: https://celery.readthedocs.io +.. _First Steps with Celery: https://celery.readthedocs.io/en/latest/getting-started/first-steps-with-celery.html + +The Flask repository contains `an example `_ +based on the information on this page, which also shows how to use JavaScript to submit +tasks and poll for progress and results. + + +Install +------- + +Install Celery from PyPI, for example using pip: + +.. code-block:: text + + $ pip install celery + + +Integrate Celery with Flask +--------------------------- + +You can use Celery without any integration with Flask, but it's convenient to configure +it through Flask's config, and to let tasks access the Flask application. + +Celery uses similar ideas to Flask, with a ``Celery`` app object that has configuration +and registers tasks. While creating a Flask app, use the following code to create and +configure a Celery app as well. + +.. code-block:: python + + from celery import Celery, Task + + 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 + +This creates and returns a ``Celery`` app object. Celery `configuration`_ is taken from +the ``CELERY`` key in the Flask configuration. The Celery app is set as the default, so +that it is seen during each request. The ``Task`` subclass automatically runs task +functions with a Flask app context active, so that services like your database +connections are available. + +.. _configuration: https://celery.readthedocs.io/en/stable/userguide/configuration.html + +Here's a basic ``example.py`` that configures Celery to use Redis for communication. We +enable a result backend, but ignore results by default. This allows us to store results +only for tasks where we care about the result. + +.. code-block:: python + + from flask import Flask + + app = Flask(__name__) + app.config.from_mapping( + CELERY=dict( + broker_url="redis://localhost", + result_backend="redis://localhost", + task_ignore_result=True, + ), + ) + celery_app = celery_init_app(app) + +Point the ``celery worker`` command at this and it will find the ``celery_app`` object. + +.. code-block:: text + + $ celery -A example worker --loglevel INFO + +You can also run the ``celery beat`` command to run tasks on a schedule. See Celery's +docs for more information about defining schedules. + +.. code-block:: text + + $ celery -A example beat --loglevel INFO + + +Application Factory +------------------- + +When using the Flask application factory pattern, call the ``celery_init_app`` function +inside the factory. It sets ``app.extensions["celery"]`` to the Celery app object, which +can be used to get the Celery app from the Flask app returned by the factory. + +.. code-block:: python + + 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) + return app + +To use ``celery`` commands, Celery needs an app object, but that's no longer directly +available. Create a ``make_celery.py`` file that calls the Flask app factory and gets +the Celery app from the returned Flask app. + +.. code-block:: python + + from example import create_app + + flask_app = create_app() + celery_app = flask_app.extensions["celery"] + +Point the ``celery`` command to this file. + +.. code-block:: text + + $ celery -A make_celery worker --loglevel INFO + $ celery -A make_celery beat --loglevel INFO + + +Defining Tasks +-------------- + +Using ``@celery_app.task`` to decorate task functions requires access to the +``celery_app`` object, which won't be available when using the factory pattern. It also +means that the decorated tasks are tied to the specific Flask and Celery app instances, +which could be an issue during testing if you change configuration for a test. + +Instead, use Celery's ``@shared_task`` decorator. This creates task objects that will +access whatever the "current app" is, which is a similar concept to Flask's blueprints +and app context. This is why we called ``celery_app.set_default()`` above. + +Here's an example task that adds two numbers together and returns the result. + +.. code-block:: python + + from celery import shared_task + + @shared_task(ignore_result=False) + def add_together(a: int, b: int) -> int: + return a + b + +Earlier, we configured Celery to ignore task results by default. Since we want to know +the return value of this task, we set ``ignore_result=False``. On the other hand, a task +that didn't need a result, such as sending an email, wouldn't set this. + + +Calling Tasks +------------- + +The decorated function becomes a task object with methods to call it in the background. +The simplest way is to use the ``delay(*args, **kwargs)`` method. See Celery's docs for +more methods. + +A Celery worker must be running to run the task. Starting a worker is shown in the +previous sections. + +.. code-block:: python + + from flask import request + + @app.post("/add") + def start_add() -> dict[str, object]: + a = request.form.get("a", type=int) + b = request.form.get("b", type=int) + result = add_together.delay(a, b) + return {"result_id": result.id} + +The route doesn't get the task's result immediately. That would defeat the purpose by +blocking the response. Instead, we return the running task's result id, which we can use +later to get the result. + + +Getting Results +--------------- + +To fetch the result of the task we started above, we'll add another route that takes the +result id we returned before. We return whether the task is finished (ready), whether it +finished successfully, and what the return value (or error) was if it is finished. + +.. code-block:: python + + from celery.result import AsyncResult + + @app.get("/result/") + def task_result(id: str) -> dict[str, object]: + result = AsyncResult(id) + return { + "ready": result.ready(), + "successful": result.successful(), + "value": result.result if result.ready() else None, + } + +Now you can start the task using the first route, then poll for the result using the +second route. This keeps the Flask request workers from being blocked waiting for tasks +to finish. + +The Flask repository contains `an example `_ +using JavaScript to submit tasks and poll for progress and results. + + +Passing Data to Tasks +--------------------- + +The "add" task above took two integers as arguments. To pass arguments to tasks, Celery +has to serialize them to a format that it can pass to other processes. Therefore, +passing complex objects is not recommended. For example, it would be impossible to pass +a SQLAlchemy model object, since that object is probably not serializable and is tied to +the session that queried it. + +Pass the minimal amount of data necessary to fetch or recreate any complex data within +the task. Consider a task that will run when the logged in user asks for an archive of +their data. The Flask request knows the logged in user, and has the user object queried +from the database. It got that by querying the database for a given id, so the task can +do the same thing. Pass the user's id rather than the user object. + +.. code-block:: python + + @shared_task + def generate_user_archive(user_id: str) -> None: + user = db.session.get(User, user_id) + ... + + generate_user_archive.delay(current_user.id) diff --git a/docs/patterns/deferredcallbacks.rst b/docs/patterns/deferredcallbacks.rst new file mode 100644 index 0000000..4ff8814 --- /dev/null +++ b/docs/patterns/deferredcallbacks.rst @@ -0,0 +1,44 @@ +Deferred Request Callbacks +========================== + +One of the design principles of Flask is that response objects are created and +passed down a chain of potential callbacks that can modify them or replace +them. When the request handling starts, there is no response object yet. It is +created as necessary either by a view function or by some other component in +the system. + +What happens if you want to modify the response at a point where the response +does not exist yet? A common example for that would be a +:meth:`~flask.Flask.before_request` callback that wants to set a cookie on the +response object. + +One way is to avoid the situation. Very often that is possible. For instance +you can try to move that logic into a :meth:`~flask.Flask.after_request` +callback instead. However, sometimes moving code there makes it +more complicated or awkward to reason about. + +As an alternative, you can use :func:`~flask.after_this_request` to register +callbacks that will execute after only the current request. This way you can +defer code execution from anywhere in the application, based on the current +request. + +At any time during a request, we can register a function to be called at the +end of the request. For example you can remember the current language of the +user in a cookie in a :meth:`~flask.Flask.before_request` callback:: + + from flask import request, after_this_request + + @app.before_request + def detect_user_language(): + language = request.cookies.get('user_lang') + + if language is None: + language = guess_language_from_request() + + # when the response exists, set a cookie with the language + @after_this_request + def remember_language(response): + response.set_cookie('user_lang', language) + return response + + g.language = language diff --git a/docs/patterns/favicon.rst b/docs/patterns/favicon.rst new file mode 100644 index 0000000..21ea767 --- /dev/null +++ b/docs/patterns/favicon.rst @@ -0,0 +1,53 @@ +Adding a favicon +================ + +A "favicon" is an icon used by browsers for tabs and bookmarks. This helps +to distinguish your website and to give it a unique brand. + +A common question is how to add a favicon to a Flask application. First, of +course, you need an icon. It should be 16 × 16 pixels and in the ICO file +format. This is not a requirement but a de-facto standard supported by all +relevant browsers. Put the icon in your static directory as +:file:`favicon.ico`. + +Now, to get browsers to find your icon, the correct way is to add a link +tag in your HTML. So, for example: + +.. sourcecode:: html+jinja + + + +That's all you need for most browsers, however some really old ones do not +support this standard. The old de-facto standard is to serve this file, +with this name, at the website root. If your application is not mounted at +the root path of the domain you either need to configure the web server to +serve the icon at the root or if you can't do that you're out of luck. If +however your application is the root you can simply route a redirect:: + + app.add_url_rule('/favicon.ico', + redirect_to=url_for('static', filename='favicon.ico')) + +If you want to save the extra redirect request you can also write a view +using :func:`~flask.send_from_directory`:: + + import os + from flask import send_from_directory + + @app.route('/favicon.ico') + def favicon(): + return send_from_directory(os.path.join(app.root_path, 'static'), + 'favicon.ico', mimetype='image/vnd.microsoft.icon') + +We can leave out the explicit mimetype and it will be guessed, but we may +as well specify it to avoid the extra guessing, as it will always be the +same. + +The above will serve the icon via your application and if possible it's +better to configure your dedicated web server to serve it; refer to the +web server's documentation. + +See also +-------- + +* The `Favicon `_ article on + Wikipedia diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst new file mode 100644 index 0000000..304f57d --- /dev/null +++ b/docs/patterns/fileuploads.rst @@ -0,0 +1,182 @@ +Uploading Files +=============== + +Ah yes, the good old problem of file uploads. The basic idea of file +uploads is actually quite simple. It basically works like this: + +1. A ``

`` tag is marked with ``enctype=multipart/form-data`` + and an ```` is placed in that form. +2. The application accesses the file from the :attr:`~flask.request.files` + dictionary on the request object. +3. use the :meth:`~werkzeug.datastructures.FileStorage.save` method of the file to save + the file permanently somewhere on the filesystem. + +A Gentle Introduction +--------------------- + +Let's start with a very basic application that uploads a file to a +specific upload folder and displays a file to the user. Let's look at the +bootstrapping code for our application:: + + import os + from flask import Flask, flash, request, redirect, url_for + from werkzeug.utils import secure_filename + + UPLOAD_FOLDER = '/path/to/the/uploads' + ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'} + + app = Flask(__name__) + app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER + +So first we need a couple of imports. Most should be straightforward, the +:func:`werkzeug.secure_filename` is explained a little bit later. The +``UPLOAD_FOLDER`` is where we will store the uploaded files and the +``ALLOWED_EXTENSIONS`` is the set of allowed file extensions. + +Why do we limit the extensions that are allowed? You probably don't want +your users to be able to upload everything there if the server is directly +sending out the data to the client. That way you can make sure that users +are not able to upload HTML files that would cause XSS problems (see +:ref:`security-xss`). Also make sure to disallow ``.php`` files if the server +executes them, but who has PHP installed on their server, right? :) + +Next the functions that check if an extension is valid and that uploads +the file and redirects the user to the URL for the uploaded file:: + + def allowed_file(filename): + return '.' in filename and \ + filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + + @app.route('/', methods=['GET', 'POST']) + def upload_file(): + if request.method == 'POST': + # check if the post request has the file part + if 'file' not in request.files: + flash('No file part') + return redirect(request.url) + file = request.files['file'] + # If the user does not select a file, the browser submits an + # empty file without a filename. + if file.filename == '': + flash('No selected file') + return redirect(request.url) + if file and allowed_file(file.filename): + filename = secure_filename(file.filename) + file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) + return redirect(url_for('download_file', name=filename)) + return ''' + + Upload new File +

Upload new File

+ + + +
+ ''' + +So what does that :func:`~werkzeug.utils.secure_filename` function actually do? +Now the problem is that there is that principle called "never trust user +input". This is also true for the filename of an uploaded file. All +submitted form data can be forged, and filenames can be dangerous. For +the moment just remember: always use that function to secure a filename +before storing it directly on the filesystem. + +.. admonition:: Information for the Pros + + So you're interested in what that :func:`~werkzeug.utils.secure_filename` + function does and what the problem is if you're not using it? So just + imagine someone would send the following information as `filename` to + your application:: + + filename = "../../../../home/username/.bashrc" + + Assuming the number of ``../`` is correct and you would join this with + the ``UPLOAD_FOLDER`` the user might have the ability to modify a file on + the server's filesystem he or she should not modify. This does require some + knowledge about how the application looks like, but trust me, hackers + are patient :) + + Now let's look how that function works: + + >>> secure_filename('../../../../home/username/.bashrc') + 'home_username_.bashrc' + +We want to be able to serve the uploaded files so they can be downloaded +by users. We'll define a ``download_file`` view to serve files in the +upload folder by name. ``url_for("download_file", name=name)`` generates +download URLs. + +.. code-block:: python + + from flask import send_from_directory + + @app.route('/uploads/') + def download_file(name): + return send_from_directory(app.config["UPLOAD_FOLDER"], name) + +If you're using middleware or the HTTP server to serve files, you can +register the ``download_file`` endpoint as ``build_only`` so ``url_for`` +will work without a view function. + +.. code-block:: python + + app.add_url_rule( + "/uploads/", endpoint="download_file", build_only=True + ) + + +Improving Uploads +----------------- + +.. versionadded:: 0.6 + +So how exactly does Flask handle uploads? Well it will store them in the +webserver's memory if the files are reasonably small, otherwise in a +temporary location (as returned by :func:`tempfile.gettempdir`). But how +do you specify the maximum file size after which an upload is aborted? By +default Flask will happily accept file uploads with an unlimited amount of +memory, but you can limit that by setting the ``MAX_CONTENT_LENGTH`` +config key:: + + from flask import Flask, Request + + app = Flask(__name__) + app.config['MAX_CONTENT_LENGTH'] = 16 * 1000 * 1000 + +The code above will limit the maximum allowed payload to 16 megabytes. +If a larger file is transmitted, Flask will raise a +:exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception. + +.. admonition:: Connection Reset Issue + + When using the local development server, you may get a connection + reset error instead of a 413 response. You will get the correct + status response when running the app with a production WSGI server. + +This feature was added in Flask 0.6 but can be achieved in older versions +as well by subclassing the request object. For more information on that +consult the Werkzeug documentation on file handling. + + +Upload Progress Bars +-------------------- + +A while ago many developers had the idea to read the incoming file in +small chunks and store the upload progress in the database to be able to +poll the progress with JavaScript from the client. The client asks the +server every 5 seconds how much it has transmitted, but this is +something it should already know. + +An Easier Solution +------------------ + +Now there are better solutions that work faster and are more reliable. There +are JavaScript libraries like jQuery_ that have form plugins to ease the +construction of progress bar. + +Because the common pattern for file uploads exists almost unchanged in all +applications dealing with uploads, there are also some Flask extensions that +implement a full fledged upload mechanism that allows controlling which +file extensions are allowed to be uploaded. + +.. _jQuery: https://jquery.com/ diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst new file mode 100644 index 0000000..8eb6b3a --- /dev/null +++ b/docs/patterns/flashing.rst @@ -0,0 +1,148 @@ +Message Flashing +================ + +Good applications and user interfaces are all about feedback. If the user +does not get enough feedback they will probably end up hating the +application. Flask provides a really simple way to give feedback to a +user with the flashing system. The flashing system basically makes it +possible to record a message at the end of a request and access it next +request and only next request. This is usually combined with a layout +template that does this. Note that browsers and sometimes web servers enforce +a limit on cookie sizes. This means that flashing messages that are too +large for session cookies causes message flashing to fail silently. + +Simple Flashing +--------------- + +So here is a full example:: + + from flask import Flask, flash, redirect, render_template, \ + request, url_for + + app = Flask(__name__) + app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' + + @app.route('/') + def index(): + return render_template('index.html') + + @app.route('/login', methods=['GET', 'POST']) + def login(): + error = None + if request.method == 'POST': + if request.form['username'] != 'admin' or \ + request.form['password'] != 'secret': + error = 'Invalid credentials' + else: + flash('You were successfully logged in') + return redirect(url_for('index')) + return render_template('login.html', error=error) + +And here is the :file:`layout.html` template which does the magic: + +.. sourcecode:: html+jinja + + + My Application + {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + {% block body %}{% endblock %} + +Here is the :file:`index.html` template which inherits from :file:`layout.html`: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block body %} +

Overview

+

Do you want to log in? + {% endblock %} + +And here is the :file:`login.html` template which also inherits from +:file:`layout.html`: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block body %} +

Login

+ {% if error %} +

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

+
+
Username: +
+
Password: +
+
+

+

+ {% endblock %} + +Flashing With Categories +------------------------ + +.. versionadded:: 0.3 + +It is also possible to provide categories when flashing a message. The +default category if nothing is provided is ``'message'``. Alternative +categories can be used to give the user better feedback. For example +error messages could be displayed with a red background. + +To flash a message with a different category, just use the second argument +to the :func:`~flask.flash` function:: + + flash('Invalid password provided', 'error') + +Inside the template you then have to tell the +:func:`~flask.get_flashed_messages` function to also return the +categories. The loop looks slightly different in that situation then: + +.. sourcecode:: html+jinja + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
    + {% for category, message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + +This is just one example of how to render these flashed messages. One +might also use the category to add a prefix such as +``Error:`` to the message. + +Filtering Flash Messages +------------------------ + +.. versionadded:: 0.9 + +Optionally you can pass a list of categories which filters the results of +:func:`~flask.get_flashed_messages`. This is useful if you wish to +render each category in a separate block. + +.. sourcecode:: html+jinja + + {% with errors = get_flashed_messages(category_filter=["error"]) %} + {% if errors %} +
+ × +
    + {%- for msg in errors %} +
  • {{ msg }}
  • + {% endfor -%} +
+
+ {% endif %} + {% endwith %} diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst new file mode 100644 index 0000000..1f2c07d --- /dev/null +++ b/docs/patterns/index.rst @@ -0,0 +1,40 @@ +Patterns for Flask +================== + +Certain features and interactions are common enough that you will find +them in most web applications. For example, many applications use a +relational database and user authentication. They will open a database +connection at the beginning of the request and get the information for +the logged in user. At the end of the request, the database connection +is closed. + +These types of patterns may be a bit outside the scope of Flask itself, +but Flask makes it easy to implement them. Some common patterns are +collected in the following pages. + +.. toctree:: + :maxdepth: 2 + + packages + appfactories + appdispatch + urlprocessors + sqlite3 + sqlalchemy + fileuploads + caching + viewdecorators + wtforms + templateinheritance + flashing + javascript + lazyloading + mongoengine + favicon + streaming + deferredcallbacks + methodoverrides + requestchecksum + celery + subclassing + singlepageapplications diff --git a/docs/patterns/javascript.rst b/docs/patterns/javascript.rst new file mode 100644 index 0000000..d58a3eb --- /dev/null +++ b/docs/patterns/javascript.rst @@ -0,0 +1,259 @@ +JavaScript, ``fetch``, and JSON +=============================== + +You may want to make your HTML page dynamic, by changing data without +reloading the entire page. Instead of submitting an HTML ``
`` and +performing a redirect to re-render the template, you can add +`JavaScript`_ that calls |fetch|_ and replaces content on the page. + +|fetch|_ is the modern, built-in JavaScript solution to making +requests from a page. You may have heard of other "AJAX" methods and +libraries, such as |XHR|_ or `jQuery`_. These are no longer needed in +modern browsers, although you may choose to use them or another library +depending on your application's requirements. These docs will only focus +on built-in JavaScript features. + +.. _JavaScript: https://developer.mozilla.org/Web/JavaScript +.. |fetch| replace:: ``fetch()`` +.. _fetch: https://developer.mozilla.org/Web/API/Fetch_API +.. |XHR| replace:: ``XMLHttpRequest()`` +.. _XHR: https://developer.mozilla.org/Web/API/XMLHttpRequest +.. _jQuery: https://jquery.com/ + + +Rendering Templates +------------------- + +It is important to understand the difference between templates and +JavaScript. Templates are rendered on the server, before the response is +sent to the user's browser. JavaScript runs in the user's browser, after +the template is rendered and sent. Therefore, it is impossible to use +JavaScript to affect how the Jinja template is rendered, but it is +possible to render data into the JavaScript that will run. + +To provide data to JavaScript when rendering the template, use the +:func:`~jinja-filters.tojson` filter in a `` + +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 400 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, or the ``Content-Type`` header is not set to +``application/json``, a 400 Bad Request 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 new file mode 100644 index 0000000..7ac6856 --- /dev/null +++ b/docs/patterns/jquery.rst @@ -0,0 +1,6 @@ +:orphan: + +AJAX with jQuery +================ + +Obsolete, see :doc:`/patterns/javascript` instead. diff --git a/docs/patterns/lazyloading.rst b/docs/patterns/lazyloading.rst new file mode 100644 index 0000000..658a1cd --- /dev/null +++ b/docs/patterns/lazyloading.rst @@ -0,0 +1,109 @@ +Lazily Loading Views +==================== + +Flask is usually used with the decorators. Decorators are simple and you +have the URL right next to the function that is called for that specific +URL. However there is a downside to this approach: it means all your code +that uses decorators has to be imported upfront or Flask will never +actually find your function. + +This can be a problem if your application has to import quick. It might +have to do that on systems like Google's App Engine or other systems. So +if you suddenly notice that your application outgrows this approach you +can fall back to a centralized URL mapping. + +The system that enables having a central URL map is the +:meth:`~flask.Flask.add_url_rule` function. Instead of using decorators, +you have a file that sets up the application with all URLs. + +Converting to Centralized URL Map +--------------------------------- + +Imagine the current application looks somewhat like this:: + + from flask import Flask + app = Flask(__name__) + + @app.route('/') + def index(): + pass + + @app.route('/user/') + def user(username): + pass + +Then, with the centralized approach you would have one file with the views +(:file:`views.py`) but without any decorator:: + + def index(): + pass + + def user(username): + pass + +And then a file that sets up an application which maps the functions to +URLs:: + + from flask import Flask + from yourapplication import views + app = Flask(__name__) + app.add_url_rule('/', view_func=views.index) + app.add_url_rule('/user/', view_func=views.user) + +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. +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 + + class LazyView(object): + + def __init__(self, import_name): + self.__module__, self.__name__ = import_name.rsplit('.', 1) + self.import_name = import_name + + @cached_property + def view(self): + return import_string(self.import_name) + + def __call__(self, *args, **kwargs): + return self.view(*args, **kwargs) + +What's important here is is that `__module__` and `__name__` are properly +set. This is used by Flask internally to figure out how to name the +URL rules in case you don't provide a name for the rule yourself. + +Then you can define your central place to combine the views like this:: + + from flask import Flask + from yourapplication.helpers import LazyView + app = Flask(__name__) + app.add_url_rule('/', + view_func=LazyView('yourapplication.views.index')) + app.add_url_rule('/user/', + view_func=LazyView('yourapplication.views.user')) + +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. :: + + 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) + + # 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) + +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 +request. The same goes for any kind of remaining decorator. diff --git a/docs/patterns/methodoverrides.rst b/docs/patterns/methodoverrides.rst new file mode 100644 index 0000000..45dbb87 --- /dev/null +++ b/docs/patterns/methodoverrides.rst @@ -0,0 +1,42 @@ +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 new file mode 100644 index 0000000..015e7b6 --- /dev/null +++ b/docs/patterns/mongoengine.rst @@ -0,0 +1,103 @@ +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://flask-mongoengine.readthedocs.io + + +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 = Movies.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/packages.rst b/docs/patterns/packages.rst new file mode 100644 index 0000000..90fa8a8 --- /dev/null +++ b/docs/patterns/packages.rst @@ -0,0 +1,133 @@ +Large Applications as Packages +============================== + +Imagine a simple flask application structure that looks like this:: + + /yourapplication + yourapplication.py + /static + 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) + +You should then end up with something like that:: + + /yourapplication + /yourapplication + __init__.py + /static + style.css + /templates + layout.html + index.html + login.html + ... + +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: + +.. 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 + +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 + `__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. + 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`:: + + from flask import Flask + app = Flask(__name__) + + import yourapplication.views + +And this is what :file:`views.py` would look like:: + + from yourapplication import app + + @app.route('/') + def index(): + return 'Hello World!' + +You should then end up with something like that:: + + /yourapplication + pyproject.toml + /yourapplication + __init__.py + views.py + /static + style.css + /templates + layout.html + index.html + login.html + ... + +.. admonition:: Circular Imports + + 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 + 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 + ensuring the module is imported and we are doing that at the bottom of + the file. + + +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. diff --git a/docs/patterns/requestchecksum.rst b/docs/patterns/requestchecksum.rst new file mode 100644 index 0000000..25bc38b --- /dev/null +++ b/docs/patterns/requestchecksum.rst @@ -0,0 +1,55 @@ +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 new file mode 100644 index 0000000..1cb779b --- /dev/null +++ b/docs/patterns/singlepageapplications.rst @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000..7e4108d --- /dev/null +++ b/docs/patterns/sqlalchemy.rst @@ -0,0 +1,214 @@ +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. + +There are four very common ways to use SQLAlchemy. I will outline each +of them here: + +Flask-SQLAlchemy Extension +-------------------------- + +Because SQLAlchemy is a common database abstraction layer and object +relational mapper that requires a little bit of configuration effort, +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/ + + +Declarative +----------- + +The declarative extension in SQLAlchemy is the most recent method of using +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:: + + from sqlalchemy import create_engine + from sqlalchemy.orm import scoped_session, sessionmaker, declarative_base + + engine = create_engine('sqlite:////tmp/test.db') + db_session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) + Base = declarative_base() + Base.query = db_session.query_property() + + def init_db(): + # import all modules here that might define models so that + # they will be registered properly on the metadata. Otherwise + # you will have to import them first before calling init_db() + import yourapplication.models + Base.metadata.create_all(bind=engine) + +To define your models, just subclass the `Base` class that was created by +the code above. If you are wondering why we don't have to care about +threads here (like we did in the SQLite3 example above with the +:data:`~flask.g` object): that's because SQLAlchemy does that for us +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:: + + from yourapplication.database import db_session + + @app.teardown_appcontext + def shutdown_session(exception=None): + db_session.remove() + +Here is an example model (put this into :file:`models.py`, e.g.):: + + from sqlalchemy import Column, Integer, String + from yourapplication.database import Base + + class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(String(50), unique=True) + email = Column(String(120), unique=True) + + def __init__(self, name=None, email=None): + self.name = name + self.email = email + + def __repr__(self): + return f'' + +To create the database you can use the `init_db` function: + +>>> from yourapplication.database import init_db +>>> init_db() + +You can insert entries into the database like this: + +>>> from yourapplication.database import db_session +>>> from yourapplication.models import User +>>> u = User('admin', 'admin@localhost') +>>> db_session.add(u) +>>> db_session.commit() + +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/ + +Manual Object Relational Mapping +-------------------------------- + +Manual object relational mapping has a few upsides and a few downsides +versus the declarative approach from above. The main difference is that +you define tables and classes separately and map them together. It's more +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:: + + from sqlalchemy import create_engine, MetaData + from sqlalchemy.orm import scoped_session, sessionmaker + + engine = create_engine('sqlite:////tmp/test.db') + metadata = MetaData() + db_session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) + def init_db(): + metadata.create_all(bind=engine) + +As in the declarative approach, you need to close the session after +each request or application context shutdown. Put this into your +application module:: + + from yourapplication.database import db_session + + @app.teardown_appcontext + def shutdown_session(exception=None): + db_session.remove() + +Here is an example table and model (put this into :file:`models.py`):: + + from sqlalchemy import Table, Column, Integer, String + from sqlalchemy.orm import mapper + from yourapplication.database import metadata, db_session + + class User(object): + query = db_session.query_property() + + def __init__(self, name=None, email=None): + self.name = name + self.email = email + + def __repr__(self): + return f'' + + users = Table('users', metadata, + Column('id', Integer, primary_key=True), + Column('name', String(50), unique=True), + Column('email', String(120), unique=True) + ) + mapper(User, users) + +Querying and inserting works exactly the same as in the example above. + + +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 + + engine = create_engine('sqlite:////tmp/test.db') + 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') + +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') + +These results are also dict-like tuples: + +>>> r = users.select(users.c.id == 1).execute().first() +>>> r['name'] +'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') + +For more information about SQLAlchemy, head over to the +`website `_. diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst new file mode 100644 index 0000000..5932589 --- /dev/null +++ b/docs/patterns/sqlite3.rst @@ -0,0 +1,147 @@ +Using SQLite 3 with Flask +========================= + +In Flask you can easily implement the opening of database connections on +demand and closing them when the context dies (usually at the end of the +request). + +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 + + @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() + ... + + +.. 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. + +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:: + + with app.app_context(): + # now you can use get_db() + + +Easy Querying +------------- + +Now in each request handling function you can access `get_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:: + + def query_db(query, args=(), one=False): + cur = get_db().execute(query, args) + rv = cur.fetchall() + cur.close() + 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. + +Here is how you can use it:: + + for user in query_db('select * from users'): + 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') + else: + 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 +--------------- + +Relational databases need schemas, so applications often ship a +`schema.sql` file that creates the database. It's a good idea to provide +a function that creates the database based on that schema. This function +can do that for you:: + + def init_db(): + with app.app_context(): + db = get_db() + with app.open_resource('schema.sql', mode='r') as f: + db.cursor().executescript(f.read()) + db.commit() + +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 new file mode 100644 index 0000000..c9e6ef2 --- /dev/null +++ b/docs/patterns/streaming.rst @@ -0,0 +1,85 @@ +Streaming Contents +================== + +Sometimes you want to send an enormous amount of data to the client, much +more than you want to keep in memory. When you are generating the data on +the fly though, how do you send that back to the client without the +roundtrip to the filesystem? + +The answer is by using generators and direct responses. + +Basic Usage +----------- + +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:: + + @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"} + +Each ``yield`` expression is directly sent to the browser. Note 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 Jinja2 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") + +The parts yielded by the render stream tend to match statement blocks in +the template. + + +Streaming with Context +---------------------- + +The :data:`~flask.request` will not be active while the generator is +running, because the view has already returned 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:`~flask.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. diff --git a/docs/patterns/subclassing.rst b/docs/patterns/subclassing.rst new file mode 100644 index 0000000..d8de233 --- /dev/null +++ b/docs/patterns/subclassing.rst @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..bb5cba2 --- /dev/null +++ b/docs/patterns/templateinheritance.rst @@ -0,0 +1,68 @@ +Template Inheritance +==================== + +The most powerful part of Jinja is template inheritance. Template inheritance +allows you to build a base "skeleton" template that contains all the common +elements of your site and defines **blocks** that child templates can override. + +Sounds complicated but is very basic. It's easiest to understand it by starting +with an example. + + +Base Template +------------- + +This template, which we'll call :file:`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: + +.. sourcecode:: html+jinja + + + + + {% block head %} + + {% block title %}{% endblock %} - My Webpage + {% 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 +child template may override those portions of the template. + +Child Template +-------------- + +A child template might look like this: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block title %}Index{% endblock %} + {% block head %} + {{ super() }} + + {% endblock %} + {% block content %} +

Index

+

+ Welcome on my awesome homepage. + {% endblock %} + +The ``{% extends %}`` tag is the key here. It tells the template engine that +this template "extends" another template. When the template system evaluates +this template, first it locates the parent. The extends tag must be the +first tag in the template. To render the contents of a block defined in +the parent template, use ``{{ super() }}``. diff --git a/docs/patterns/urlprocessors.rst b/docs/patterns/urlprocessors.rst new file mode 100644 index 0000000..0d74320 --- /dev/null +++ b/docs/patterns/urlprocessors.rst @@ -0,0 +1,126 @@ +Using URL Processors +==================== + +.. versionadded:: 0.7 + +Flask 0.7 introduces the concept of URL processors. The idea is that you +might have a bunch of resources with common parts in the URL that you +don't always explicitly want to provide. For instance you might have a +bunch of URLs that have the language code in it but you don't want to have +to handle it in every single function yourself. + +URL processors are especially helpful when combined with blueprints. We +will handle both application specific URL processors here as well as +blueprint specifics. + +Internationalized Application URLs +---------------------------------- + +Consider an application like this:: + + from flask import Flask, g + + app = Flask(__name__) + + @app.route('//') + def index(lang_code): + g.lang_code = lang_code + ... + + @app.route('//about') + def about(lang_code): + g.lang_code = lang_code + ... + +This is an awful lot of repetition as you have to handle the language code +setting on the :data:`~flask.g` object yourself in every single function. +Sure, a decorator could be used to simplify this, but if you want to +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 +language code is not yet in the dictionary of URL values and if the +endpoint wants a value named ``'lang_code'``:: + + @app.url_defaults + def add_language_code(endpoint, values): + if 'lang_code' in values or not g.lang_code: + return + if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): + values['lang_code'] = g.lang_code + +The method :meth:`~werkzeug.routing.Map.is_endpoint_expecting` of the URL +map can be used to figure out if it would make sense to provide a language +code for the given endpoint. + +The reverse of that function are +:meth:`~flask.Flask.url_value_preprocessor`\s. They are executed right +after the request was matched and can execute code based on the URL +values. The idea is that they pull information out of the values +dictionary and put it somewhere else:: + + @app.url_value_preprocessor + 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 +: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 +``'lang_code'`` is popped from the values dictionary and it will no longer +be forwarded to the view function reducing the code to this:: + + from flask import Flask, g + + app = Flask(__name__) + + @app.url_defaults + def add_language_code(endpoint, values): + if 'lang_code' in values or not g.lang_code: + return + if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): + values['lang_code'] = g.lang_code + + @app.url_value_preprocessor + def pull_lang_code(endpoint, values): + g.lang_code = values.pop('lang_code', None) + + @app.route('//') + def index(): + ... + + @app.route('//about') + def about(): + ... + +Internationalized Blueprint URLs +-------------------------------- + +Because blueprints can automatically prefix all URLs with a common string +it's easy to automatically do that for every function. Furthermore +blueprints can have per-blueprint URL processors which removes a whole lot +of logic from the :meth:`~flask.Flask.url_defaults` function because it no +longer has to check if the URL is really interested in a ``'lang_code'`` +parameter:: + + from flask import Blueprint, g + + bp = Blueprint('frontend', __name__, url_prefix='/') + + @bp.url_defaults + def add_language_code(endpoint, values): + values.setdefault('lang_code', g.lang_code) + + @bp.url_value_preprocessor + def pull_lang_code(endpoint, values): + g.lang_code = values.pop('lang_code') + + @bp.route('/') + def index(): + ... + + @bp.route('/about') + def about(): + ... diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst new file mode 100644 index 0000000..0b0479e --- /dev/null +++ b/docs/patterns/viewdecorators.rst @@ -0,0 +1,171 @@ +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 +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 +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. + +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. + +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. :: + + from functools import wraps + from flask import g, request, redirect, url_for + + def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if g.user is None: + return redirect(url_for('login', next=request.url)) + 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. :: + + @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`. + +Here is 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 +it is a little bit more complex, but the code should still be +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 + path. +2. get the value for that key from the cache. If the cache returned + something we will return that value. +3. otherwise the original function is called and the return value is + stored in the cache for the timeout provided (by default 5 minutes). + +Here the code:: + + from functools import wraps + from flask import request + + def cached(timeout=5 * 60, key='view/{}'): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + cache_key = key.format(request.path) + rv = cache.get(cache_key) + if rv is not None: + return rv + rv = f(*args, **kwargs) + cache.set(cache_key, rv, timeout=timeout) + return rv + return decorated_function + return decorator + +Notice that this assumes an instantiated ``cache`` object is available, see +:doc:`caching`. + + +Templating Decorator +-------------------- + +A common pattern invented by the TurboGears guys a while back is a +templating decorator. The idea of that decorator is that you return a +dictionary with the values passed to the template from the view function +and the template is automatically rendered. With that, the following +three examples do exactly the same:: + + @app.route('/') + def index(): + return render_template('index.html', value=42) + + @app.route('/') + @templated('index.html') + def index(): + return dict(value=42) + + @app.route('/') + @templated() + def index(): + return dict(value=42) + +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 +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:: + + from functools import wraps + from flask import request, render_template + + def templated(template=None): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + template_name = template + if template_name is None: + template_name = f"{request.endpoint.replace('.', '/')}.html" + ctx = f(*args, **kwargs) + if ctx is None: + ctx = {} + elif not isinstance(ctx, dict): + return ctx + return render_template(template_name, **ctx) + return decorated_function + return decorator + + +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:: + + from flask import Flask + from werkzeug.routing import Rule + + app = Flask(__name__) + app.url_map.add(Rule('/', endpoint='index')) + + @app.endpoint('index') + def my_index(): + return "Hello world" diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst new file mode 100644 index 0000000..3d626f5 --- /dev/null +++ b/docs/patterns/wtforms.rst @@ -0,0 +1,126 @@ +Form Validation with WTForms +============================ + +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 +forms. + +.. admonition:: Getting the most out 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 + fun. You can get it from `PyPI + `_. + +.. _Flask-WTF: https://flask-wtf.readthedocs.io/ + +The Forms +--------- + +This is an example form for a typical registration page:: + + from wtforms import Form, BooleanField, StringField, PasswordField, validators + + class RegistrationForm(Form): + username = StringField('Username', [validators.Length(min=4, max=25)]) + email = StringField('Email Address', [validators.Length(min=6, max=35)]) + password = PasswordField('New Password', [ + validators.DataRequired(), + validators.EqualTo('confirm', message='Passwords must match') + ]) + confirm = PasswordField('Repeat Password') + accept_tos = BooleanField('I accept the TOS', [validators.DataRequired()]) + +In the View +----------- + +In the view function, the usage of this form looks like this:: + + @app.route('/register', methods=['GET', 'POST']) + def register(): + form = RegistrationForm(request.form) + if request.method == 'POST' and form.validate(): + user = User(form.username.data, form.email.data, + form.password.data) + db_session.add(user) + flash('Thanks for registering') + 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 +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``. +2. to validate the data, call the :func:`~wtforms.form.Form.validate` + 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 +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: + +.. sourcecode:: html+jinja + + {% macro render_field(field) %} +

{{ field.label }} +
{{ field(**kwargs)|safe }} + {% if field.errors %} +
    + {% 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 +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 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: + +.. sourcecode:: html+jinja + + {% from "_formhelpers.html" import render_field %} + +
+ {{ render_field(form.username) }} + {{ render_field(form.email) }} + {{ render_field(form.password) }} + {{ render_field(form.confirm) }} + {{ render_field(form.accept_tos) }} +
+

+

+ +For more information about WTForms, head over to the `WTForms +website`_. + +.. _WTForms: https://wtforms.readthedocs.io/ +.. _WTForms website: https://wtforms.readthedocs.io/ diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 0000000..0d7ad3f --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,907 @@ +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. + + +A Minimal Application +--------------------- + +A minimal Flask application looks something like this: + +.. code-block:: python + + from flask import Flask + + app = Flask(__name__) + + @app.route("/") + def hello_world(): + return "

Hello, World!

" + +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. + +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. + +.. _public-server: + +.. admonition:: Externally Visible Server + + If you run the server you will notice that the server is only accessible + 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. + + 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:: + + $ flask run --host=0.0.0.0 + + This tells your operating system to listen on all public IPs. + + +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. + +.. image:: _static/debugger.png + :align: center + :class: screenshot + :alt: The interactive 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. + +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 markupsafe import escape + + @app.route("/") + def hello(name): + return f"Hello, {escape(name)}!" + +If a user managed to submit the name ````, +escaping causes it to be rendered as text, rather than running the +script in the user's browser. + +```` in the route captures a value from the URL and passes it to +the view function. These variable rules are explained below. + + +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. + +Use the :meth:`~flask.Flask.route` decorator to bind a function to a URL. :: + + @app.route('/') + def index(): + return 'Index Page' + + @app.route('/hello') + def hello(): + return 'Hello, World' + +You can do more! You can make 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 + + @app.route('/user/') + def show_user_profile(username): + # show the user profile for that user + return f'User {escape(username)}' + + @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}' + + @app.route('/path/') + def show_subpath(subpath): + # show the subpath after /path/ + return f'Subpath {escape(subpath)}' + +Converter types: + +========== ========================================== +``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 +========== ========================================== + + +Unique URLs / Redirection Behavior +`````````````````````````````````` + +The following two rules differ in their use of a trailing slash. :: + + @app.route('/projects/') + def projects(): + return 'The project page' + + @app.route('/about') + def about(): + return 'The about page' + +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/``). + +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. + + +.. _url-building: + +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. + +Why would you want to build URLs using the URL reversing function +:func:`~flask.url_for` instead of hard-coding them into your templates? + +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. + +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 :ref:`context-locals`. + +.. 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 + + +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 + + @app.route('/login', methods=['GET', 'POST']) + def login(): + if request.method == 'POST': + return do_the_login() + else: + return 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. + +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. + +.. code-block:: python + + @app.get('/login') + def login_get(): + return show_the_login_form() + + @app.post('/login') + def login_post(): + return do_the_login() + +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. + +.. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt + +Static Files +------------ + +Dynamic web applications also need static files. 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. + +To generate URLs for static files, use the special ``'static'`` endpoint name:: + + url_for('static', filename='style.css') + +The file has to be stored on the filesystem as :file:`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 `Jinja2 +`_ 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/ + +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 +variables you want to pass to the template engine as keyword arguments. +Here's a simple example of how to render a template:: + + from flask import render_template + + @app.route('/hello/') + @app.route('/hello/') + def hello(name=None): + 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 +package it's actually inside your package: + +**Case 1**: a module:: + + /application.py + /templates + /hello.html + +**Case 2**: a package:: + + /application + /__init__.py + /templates + /hello.html + +For templates you can use the full power of Jinja2 templates. Head over +to the official `Jinja2 Template Documentation +`_ for more information. + +Here is an example template: + +.. sourcecode:: html+jinja + + + Hello from Flask + {% if name %} +

Hello {{ name }}!

+ {% else %} +

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. + +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). + +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 +markup to HTML) you can mark it as safe by using the +:class:`~markupsafe.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:: + + >>> 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' + +.. versionchanged:: 0.5 + + Autoescaping is no longer enabled for all templates. The following + extensions for templates trigger autoescaping: ``.html``, ``.htm``, + ``.xml``, ``.xhtml``. Templates loaded from a string will have + 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`. + + +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 +: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 is context locals: + + +.. _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 +web server decides to spawn a new thread (or something else, the +underlying object is capable of dealing with concurrency systems other +than threads). 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 so 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 unit testing. You +will notice that code which 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 +unit testing is to use 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 + + 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 other possibility is passing a whole WSGI environment to the +:meth:`~flask.Flask.request_context` method:: + + 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 log_the_user_in(request.form['username']) + else: + error = 'Invalid username/password' + # the code below is 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. + +To access parameters submitted in the URL (``?key=value``) you can use the +:attr:`~flask.Request.args` attribute:: + + searchword = request.args.get('key', '') + +We recommend accessing URL parameters with `get` or by catching the +:exc:`KeyError` because users might change the URL and presenting them a 400 +bad request page in that case is not user friendly. + +For a full list of methods and attributes of the request object, head over +to the :class:`~flask.Request` documentation. + + +File Uploads +```````````` + +You can handle uploaded files with Flask easily. Just make sure not to +forget to set the ``enctype="multipart/form-data"`` attribute on your HTML +form, otherwise the browser will not transmit your files at all. + +Uploaded files are stored in memory or at a temporary location on the +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:: + + from flask import request + + @app.route('/upload', methods=['GET', 'POST']) + def upload_file(): + if request.method == 'POST': + f = request.files['the_file'] + f.save('/var/www/uploads/uploaded_file.txt') + ... + +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 +Werkzeug provides for you:: + + from werkzeug.utils 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)}") + ... + +For some better examples, see :doc:`patterns/fileuploads`. + +Cookies +``````` + +To access cookies you can use the :attr:`~flask.Request.cookies` +attribute. To set cookies you can use the +:attr:`~flask.Response.set_cookie` method of response objects. The +:attr:`~flask.Request.cookies` attribute of request objects is a +dictionary with all the cookies the client transmits. If you want to use +sessions, do not use the cookies directly but instead use the +:ref:`sessions` in Flask that add some security on top of cookies for you. + +Reading cookies:: + + from flask import request + + @app.route('/') + def index(): + username = request.cookies.get('username') + # use cookies.get(key) instead of cookies[key] to not get a + # KeyError if the cookie is missing. + +Storing cookies:: + + from flask import make_response + + @app.route('/') + def index(): + resp = make_response(render_template(...)) + resp.set_cookie('username', 'the username') + return resp + +Note that cookies are set on response objects. Since you normally +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. + +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:: + + from flask import abort, redirect, url_for + + @app.route('/') + def index(): + return redirect(url_for('login')) + + @app.route('/login') + def login(): + abort(401) + this_is_never_executed() + +This is a rather pointless example because a user will be redirected from +the index to a page they cannot access (401 means access denied) but it +shows how that works. + +By default a black and white error page is shown for each error code. If +you want to customize the error page, you can use the +:meth:`~flask.Flask.errorhandler` decorator:: + + from flask import render_template + + @app.errorhandler(404) + def page_not_found(error): + return render_template('page_not_found.html'), 404 + +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: + +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. + +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:: + + from flask import render_template + + @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:: + + from flask import make_response + + @app.errorhandler(404) + def not_found(error): + resp = make_response(render_template('error.html'), 404) + 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 +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, +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 + + # Set the secret key to some random bytes. Keep this really secret! + app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' + + @app.route('/') + def index(): + if 'username' in session: + return f'Logged in as {session["username"]}' + return 'You are not logged in' + + @app.route('/login', methods=['GET', 'POST']) + def login(): + if request.method == 'POST': + session['username'] = request.form['username'] + return redirect(url_for('index')) + return ''' +
+

+

+

+ ''' + + @app.route('/logout') + def logout(): + # remove the username from the session if it's there + session.pop('username', None) + return redirect(url_for('index')) + +.. 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`):: + + $ python -c 'import secrets; print(secrets.token_hex())' + '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' + +A note on cookie-based sessions: Flask will take the values you put into the +session object and serialize them into a cookie. If you are finding some +values do not persist across requests, cookies are indeed enabled, and you are +not getting a clear error message, check the size of the cookie in your page +responses compared to the size supported by web browsers. + +Besides the default client-side based sessions, if you want to handle +sessions on the server-side instead, there are several +Flask extensions that support this. + +Message Flashing +---------------- + +Good applications and user interfaces are all about feedback. If the user +does not get enough feedback they will probably end up hating the +application. Flask provides a really simple way to give feedback to a +user with the flashing system. The flashing system basically makes it +possible to record a message at the end of a request and access it on the next +(and only the next) request. This is usually combined with a layout +template to expose the message. + +To flash a message use the :func:`~flask.flash` method, to get hold of the +messages you can use :func:`~flask.get_flashed_messages` which is also +available in the templates. See :doc:`patterns/flashing` for a full +example. + +Logging +------- + +.. versionadded:: 0.3 + +Sometimes you might be in a situation where you deal with data that +should be correct, but actually is not. For example you may have +some client-side code that sends an HTTP request to the server +but it's obviously malformed. This might be caused by a user tampering +with the data, or the client code failing. Most of the time it's okay +to reply with ``400 Bad Request`` in that situation, but sometimes +that won't do and the code has to continue working. + +You may still want to log that something fishy happened. This is where +loggers come in handy. As of Flask 0.3 a logger is preconfigured for you +to use. + +Here are some example log calls:: + + app.logger.debug('A value for debugging') + app.logger.warning('A warning occurred (%d apples)', 42) + app.logger.error('An error occurred') + +The attached :attr:`~flask.Flask.logger` is a standard logging +:class:`~logging.Logger`, so head over to the official :mod:`logging` +docs for more information. + +See :doc:`errorhandling`. + + +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`. diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst new file mode 100644 index 0000000..4f1846a --- /dev/null +++ b/docs/reqcontext.rst @@ -0,0 +1,243 @@ +.. currentmodule:: flask + +The Request Context +=================== + +The request context keeps track of the request-level data during a +request. Rather than passing the request object to each function that +runs during a request, the :data:`request` and :data:`session` proxies +are accessed instead. + +This is similar to :doc:`/appcontext`, which keeps track of the +application-level data independent of a request. A corresponding +application context is pushed when a request context is pushed. + + +Purpose of the Context +---------------------- + +When the :class:`Flask` application handles a request, it creates a +:class:`Request` object based on the environment it received from the +WSGI server. Because a *worker* (thread, process, or coroutine depending +on the server) handles only one request at a time, the request data can +be considered global to that worker during that request. Flask uses the +term *context local* for this. + +Flask automatically *pushes* a request context when handling a request. +View functions, error handlers, and other functions that run during a +request will have access to the :data:`request` proxy, which points to +the request object for the current request. + + +Lifetime of the Context +----------------------- + +When a Flask application begins handling a request, it pushes a request +context, which also pushes an :doc:`app context `. When the +request ends it pops the request context then the application context. + +The context is unique to each thread (or other worker type). +:data:`request` cannot be passed to another thread, the other thread has +a different context space and will not know about the request the parent +thread was pointing to. + +Context locals are implemented using Python's :mod:`contextvars` and +Werkzeug's :class:`~werkzeug.local.LocalProxy`. Python manages the +lifetime of context vars automatically, and local proxy wraps that +low-level interface to make the data easier to work with. + + +Manually Push a Context +----------------------- + +If you try to access :data:`request`, or anything that uses it, outside +a request context, you'll get this error message: + +.. code-block:: pytb + + RuntimeError: Working outside of request context. + + This typically means that you attempted to use functionality that + needed an active HTTP request. Consult the documentation on testing + for information about how to avoid this problem. + +This should typically only happen when testing code that expects an +active request. One option is to use the +:meth:`test client ` to simulate a full request. Or +you can use :meth:`~Flask.test_request_context` in a ``with`` block, and +everything that runs in the block will have access to :data:`request`, +populated with your test data. :: + + def generate_report(year): + format = request.args.get("format") + ... + + with app.test_request_context( + "/make_report/2017", query_string={"format": "short"} + ): + generate_report() + +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. + +For information on how to use the request context from the interactive +Python shell, see :doc:`/shell`. + + +How the Context Works +--------------------- + +The :meth:`Flask.wsgi_app` method is called to handle each request. It +manages the contexts during the request. Internally, the request and +application contexts work like stacks. When contexts are pushed, the +proxies that depend on them are available and point at information from +the top item. + +When the request starts, a :class:`~ctx.RequestContext` is created and +pushed, which creates and pushes an :class:`~ctx.AppContext` first if +a context for that application is not already the top context. While +these contexts are pushed, the :data:`current_app`, :data:`g`, +:data:`request`, and :data:`session` proxies are available to the +original thread handling the request. + +Other contexts may be pushed to change the proxies during a request. +While this is not a common pattern, it can be used in advanced +applications to, for example, do internal redirects or chain different +applications together. + +After the request is dispatched and a response is generated and sent, +the request context is popped, which then pops the application context. +Immediately before they are popped, the :meth:`~Flask.teardown_request` +and :meth:`~Flask.teardown_appcontext` functions are executed. These +execute even if an unhandled exception occurred during dispatch. + + +.. _callbacks-and-errors: + +Callbacks and Errors +-------------------- + +Flask dispatches a request in multiple stages which can affect the +request, response, and how errors are handled. The contexts are active +during all of these stages. + +A :class:`Blueprint` can add handlers for these events that are specific +to the blueprint. The handlers for a blueprint will run if the blueprint +owns the route that matches the request. + +#. Before each request, :meth:`~Flask.before_request` functions are + called. If one of these functions return a value, the other + functions are skipped. The return value is treated as the response + and the view function is not called. + +#. If the :meth:`~Flask.before_request` functions did not return a + response, the view function for the matched route is called and + returns a response. + +#. The return value of the view is converted into an actual response + object and passed to the :meth:`~Flask.after_request` + functions. Each function returns a modified or new response object. + +#. After the response is returned, the contexts are popped, which calls + the :meth:`~Flask.teardown_request` and + :meth:`~Flask.teardown_appcontext` functions. These functions are + called even if an unhandled exception was raised at any point above. + +If an exception is raised before the teardown functions, Flask tries to +match it with an :meth:`~Flask.errorhandler` function to handle the +exception and return a response. If no error handler is found, or the +handler itself raises an exception, Flask returns a generic +``500 Internal Server Error`` response. The teardown functions are still +called, and are passed the exception object. + +If debug mode is enabled, unhandled exceptions are not converted to a +``500`` response and instead are propagated to the WSGI server. This +allows the development server to present the interactive debugger with +the traceback. + + +Teardown Callbacks +~~~~~~~~~~~~~~~~~~ + +The teardown callbacks are independent of the request dispatch, and are +instead called by the contexts when they are popped. The functions are +called even if there is an unhandled exception during dispatch, and for +manually pushed contexts. This means there is no guarantee that any +other parts of the request dispatch have run first. Be sure to write +these functions in a way that does not depend on other callbacks and +will not fail. + +During testing, it can be useful to defer popping the contexts after the +request ends, so that their data can be accessed in the test function. +Use the :meth:`~Flask.test_client` as a ``with`` block to preserve the +contexts until the ``with`` block exits. + +.. code-block:: python + + from flask import Flask, request + + app = Flask(__name__) + + @app.route('/') + def hello(): + print('during view') + return 'Hello, World!' + + @app.teardown_request + def show_teardown(exception): + print('after with block') + + with app.test_request_context(): + print('during with block') + + # teardown functions are called after the context with block exits + + with app.test_client() as client: + client.get('/') + # the contexts are not popped even though the request ended + print(request.path) + + # the contexts are popped and teardown functions are called after + # the client with block exits + +Signals +~~~~~~~ + +The following signals are sent: + +#. :data:`request_started` is sent before the :meth:`~Flask.before_request` functions + are called. +#. :data:`request_finished` is sent after the :meth:`~Flask.after_request` functions + are called. +#. :data:`got_request_exception` is sent when an exception begins to be handled, but + before an :meth:`~Flask.errorhandler` is looked up or called. +#. :data:`request_tearing_down` is sent after the :meth:`~Flask.teardown_request` + functions are called. + + +.. _notes-on-proxies: + +Notes On Proxies +---------------- + +Some of the objects provided by Flask are proxies to other objects. The +proxies are accessed in the same way for each worker thread, but +point to the unique object bound to each worker behind the scenes as +described on this page. + +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 actually a proxy: + +- The proxy objects cannot fake their type as the actual object types. + If you want to perform instance checks, you have to do that on the + object being proxied. +- The reference to the proxied object is needed in some situations, + such as sending :doc:`signals` or passing data to a background + thread. + +If you need to access the underlying object that is proxied, use the +:meth:`~werkzeug.local.LocalProxy._get_current_object` method:: + + app = current_app._get_current_object() + my_signal.send(app) diff --git a/docs/server.rst b/docs/server.rst new file mode 100644 index 0000000..11e976b --- /dev/null +++ b/docs/server.rst @@ -0,0 +1,115 @@ +.. 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 Preferences 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 new file mode 100644 index 0000000..7e42e28 --- /dev/null +++ b/docs/shell.rst @@ -0,0 +1,100 @@ +Working with the Shell +====================== + +.. versionadded:: 0.3 + +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. + +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 unit testing and other situations that require a faked request +context. + +Generally it's recommended that you read :doc:`reqcontext` first. + +Command Line Interface +---------------------- + +Starting with Flask 0.11 the recommended way to work with the shell is the +``flask shell`` command which does a lot of this automatically for you. +For instance the shell is automatically initialized with a loaded +application context. + +For more information see :doc:`/cli`. + +Creating a Request Context +-------------------------- + +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 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`: + +>>> ctx.pop() + +Firing Before/After Request +--------------------------- + +By just creating a request context, you still don't have run the code that +is normally run before a request. This might result in your database +being unavailable if you are connecting to the database in a +before-request callback or the current user not being stored on the +:data:`~flask.g` object etc. + +This however can easily be done yourself. Just call +:meth:`~flask.Flask.preprocess_request`: + +>>> ctx = app.test_request_context() +>>> ctx.push() +>>> app.preprocess_request() + +Keep in mind that the :meth:`~flask.Flask.preprocess_request` function +might return a response object, in that case just ignore it. + +To shutdown a request, you need to trick a bit before the after request +functions (triggered by :meth:`~flask.Flask.process_response`) operate on +a response object: + +>>> app.process_response(app.response_class()) + +>>> ctx.pop() + +The functions registered as :meth:`~flask.Flask.teardown_request` are +automatically called when the context is popped. So this is the perfect +place to automatically tear down resources that were needed by the request +context (such as database connections). + + +Further Improving the Shell Experience +-------------------------------------- + +If you like the idea of experimenting in a shell, create yourself a module +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: + +>>> from shelltools import * diff --git a/docs/signals.rst b/docs/signals.rst new file mode 100644 index 0000000..739bb0b --- /dev/null +++ b/docs/signals.rst @@ -0,0 +1,167 @@ +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. + +Signals are implemented by the `Blinker`_ library. See its documentation for detailed +information. Flask provides some built-in signals. Extensions may provide their own. + +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. + + +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. + + +Subscribing to Signals +---------------------- + +To subscribe to a signal, you can use the +:meth:`~blinker.base.Signal.connect` method of a signal. The first +argument is the function that should be called when the signal is emitted, +the optional second argument specifies a sender. To unsubscribe from a +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 +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 +to the template:: + + from flask import template_rendered + from contextlib import contextmanager + + @contextmanager + def captured_templates(app): + recorded = [] + def record(sender, template, context, **extra): + recorded.append((template, context)) + template_rendered.connect(record, app) + try: + yield recorded + finally: + template_rendered.disconnect(record, app) + +This can now easily be paired with a test client:: + + with captured_templates(app) as templates: + rv = app.test_client().get('/') + assert rv.status_code == 200 + assert len(templates) == 1 + template, context = templates[0] + 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` +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 +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:: + + from flask import template_rendered + + def captured_templates(app, recorded, **extra): + def record(sender, template, context): + recorded.append((template, context)) + return template_rendered.connected_to(record, app) + +The example above would then look like this:: + + templates = [] + with captured_templates(app, templates, **extra): + ... + template, context = templates[0] + +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 +most of the time:: + + from blinker import Namespace + my_signals = Namespace() + +Now you can create new signals like this:: + + model_saved = my_signals.signal('model-saved') + +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: + +Sending Signals +--------------- + +If you want to emit a signal, you can do so by calling the +:meth:`~blinker.base.Signal.send` method. It accepts a sender as first +argument and optionally some keyword arguments that are forwarded to the +signal subscribers:: + + class Model(object): + ... + + def save(self): + 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 +function, you can pass ``current_app._get_current_object()`` as sender. + +.. admonition:: Passing Proxies as Senders + + Never pass :data:`~flask.current_app` as sender to a signal. Use + ``current_app._get_current_object()`` instead. The reason for this is + that :data:`~flask.current_app` is a proxy and not the real application + object. + + +Signals and Flask's Request Context +----------------------------------- + +Signals fully support :doc:`reqcontext` when receiving signals. +Context-local variables are consistently 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 +: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}') + + +.. _blinker: https://pypi.org/project/blinker/ diff --git a/docs/templating.rst b/docs/templating.rst new file mode 100644 index 0000000..23cfee4 --- /dev/null +++ b/docs/templating.rst @@ -0,0 +1,229 @@ +Templates +========= + +Flask leverages Jinja2 as its 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 Jinja2 being present. + +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 `Jinja2 Template +Documentation `_ for +more information. + +Jinja Setup +----------- + +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`. +- 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 + Jinja2 context, additionally to the values that are present by + default. + +Standard Context +---------------- + +The following global variables are available within Jinja2 templates +by default: + +.. data:: config + :noindex: + + The current configuration object (:data:`flask.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. + +.. data:: session + :noindex: + + The current session object (:class:`flask.session`). This variable + is unavailable if the template was rendered without an active request + context. + +.. 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. + +.. function:: url_for + :noindex: + + The :func:`flask.url_for` function. + +.. function:: get_flashed_messages + :noindex: + + The :func:`flask.get_flashed_messages` function. + +.. admonition:: The Jinja Context Behavior + + These variables are added to the context of variables, they are not + global variables. The difference is that by default these will not + show up in the context of imported templates. This is partially caused + by performance considerations, partially to keep things explicit. + + What does this mean for you? If you have a macro you want to import, + that needs to access the request object you have two possibilities: + + 1. you explicitly pass the request to the macro as parameter, or + the attribute of the request object you are interested in. + 2. you import the macro "with context". + + Importing with context looks like this: + + .. sourcecode:: jinja + + {% from '_helpers.html' import my_macro with context %} + + +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) +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`) + +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 +markdown to HTML converter. + +There are three ways to accomplish that: + +- In the Python code, wrap the HTML string in a :class:`~markupsafe.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 + string as safe HTML (``{{ myvariable|safe }}``) +- Temporarily disable the autoescape system altogether. + +To disable the autoescape system in templates, you can use the ``{% +autoescape %}`` block: + +.. sourcecode:: html+jinja + + {% autoescape false %} +

autoescaping is disabled here +

{{ will_not_be_escaped }} + {% endautoescape %} + +Whenever you do this, please be very cautious about the variables you are +using in this block. + +.. _registering-filters: + +Registering Filters +------------------- + +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 two following examples work the same and both reverse an object:: + + @app.template_filter('reverse') + def reverse_filter(s): + return s[::-1] + + def reverse_filter(s): + 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. Once registered, you can use the filter +in your templates in the same way as Jinja2's builtin filters, for example if +you have a Python list in context called `mylist`:: + + {% for x in mylist | reverse %} + {% endfor %} + + +Context Processors +------------------ + +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:: + + @app.context_processor + def inject_user(): + return dict(user=g.user) + +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 Jinja2 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 it remains available in the template. diff --git a/docs/testing.rst b/docs/testing.rst new file mode 100644 index 0000000..b1d52f9 --- /dev/null +++ b/docs/testing.rst @@ -0,0 +1,319 @@ +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. + +We will use the `pytest`_ framework to set up and run our tests. + +.. code-block:: text + + $ pip install pytest + +.. _pytest: https://docs.pytest.org/ + +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. + + +Identifying Tests +----------------- + +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``. + +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. + + +Fixtures +-------- + +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 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. + +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. + +.. code-block:: python + + import pytest + from my_project import create_app + + @pytest.fixture() + def app(): + app = create_app() + app.config.update({ + "TESTING": True, + }) + + # other setup can go here + + yield app + + # clean up / reset resources here + + + @pytest.fixture() + def client(app): + return app.test_client() + + + @pytest.fixture() + def runner(app): + return app.test_cli_runner() + + +Sending Requests with the Test Client +------------------------------------- + +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. + +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``. + +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)``. + +.. code-block:: python + + def test_request_example(client): + response = client.get("/posts") + assert b"

Hello, World!

" in response.data + + +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. + +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. + + +Form Data +~~~~~~~~~ + +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. + +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. + +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. + +.. code-block:: python + + from pathlib import Path + + # get the resources folder in the tests folder + resources = Path(__file__).parent / "resources" + + 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 + + +JSON Data +~~~~~~~~~ + +To send JSON data, pass an object to ``json``. The ``Content-Type`` +header will be set to ``application/json`` automatically. + +Similarly, if the response contains JSON data, the ``response.json`` +attribute will contain the deserialized object. + +.. code-block:: python + + 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" + + +Following Redirects +------------------- + +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. + +: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. + +.. code-block:: python + + 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" + + +Accessing and Modifying the Session +----------------------------------- + +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. + +.. code-block:: python + + 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 + + # session is no longer accessible + +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. + +.. code-block:: python + + from flask import session + + 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 + + # session is saved now + + response = client.get("/users/me") + assert response.json["username"] == "flask" + + +.. _testing-cli: + +Running Commands with the CLI Runner +------------------------------------ + +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. + +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. + +.. code-block:: python + + 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:`application context ` or +:doc:`request context ` because they access ``request``, +``session``, or ``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" diff --git a/docs/tutorial/blog.rst b/docs/tutorial/blog.rst new file mode 100644 index 0000000..b06329e --- /dev/null +++ b/docs/tutorial/blog.rst @@ -0,0 +1,336 @@ +.. 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/database.rst b/docs/tutorial/database.rst new file mode 100644 index 0000000..934f600 --- /dev/null +++ b/docs/tutorial/database.rst @@ -0,0 +1,209 @@ +.. 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 + + 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.') + +: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. + + +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/deploy.rst b/docs/tutorial/deploy.rst new file mode 100644 index 0000000..eb3a53a --- /dev/null +++ b/docs/tutorial/deploy.rst @@ -0,0 +1,111 @@ +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 new file mode 100644 index 0000000..39febd1 --- /dev/null +++ b/docs/tutorial/factory.rst @@ -0,0 +1,162 @@ +.. 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 + try: + os.makedirs(app.instance_path) + except OSError: + pass + + # 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 new file mode 100644 index 0000000..6cd6e39 Binary files /dev/null and b/docs/tutorial/flaskr_edit.png differ diff --git a/docs/tutorial/flaskr_index.png b/docs/tutorial/flaskr_index.png new file mode 100644 index 0000000..aa2b50f Binary files /dev/null and b/docs/tutorial/flaskr_index.png differ diff --git a/docs/tutorial/flaskr_login.png b/docs/tutorial/flaskr_login.png new file mode 100644 index 0000000..d482c64 Binary files /dev/null and b/docs/tutorial/flaskr_login.png differ diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst new file mode 100644 index 0000000..d5dc5b3 --- /dev/null +++ b/docs/tutorial/index.rst @@ -0,0 +1,64 @@ +Tutorial +======== + +.. toctree:: + :caption: Contents: + :maxdepth: 1 + + 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`. diff --git a/docs/tutorial/install.rst b/docs/tutorial/install.rst new file mode 100644 index 0000000..db83e10 --- /dev/null +++ b/docs/tutorial/install.rst @@ -0,0 +1,89 @@ +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/layout.rst b/docs/tutorial/layout.rst new file mode 100644 index 0000000..6f8e59f --- /dev/null +++ b/docs/tutorial/layout.rst @@ -0,0 +1,110 @@ +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 + └── MANIFEST.in + +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/ + + dist/ + build/ + *.egg-info/ + +Continue to :doc:`factory`. diff --git a/docs/tutorial/next.rst b/docs/tutorial/next.rst new file mode 100644 index 0000000..d41e8ef --- /dev/null +++ b/docs/tutorial/next.rst @@ -0,0 +1,38 @@ +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/static.rst b/docs/tutorial/static.rst new file mode 100644 index 0000000..8e76c40 --- /dev/null +++ b/docs/tutorial/static.rst @@ -0,0 +1,72 @@ +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 new file mode 100644 index 0000000..1a5535c --- /dev/null +++ b/docs/tutorial/templates.rst @@ -0,0 +1,187 @@ +.. currentmodule:: flask + +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. + +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. + +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. + +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. + +.. _Jinja: https://jinja.palletsprojects.com/templates/ +.. _HTML: https://developer.mozilla.org/docs/Web/HTML + + +The Base Layout +--------------- + +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`` + + + {% block title %}{% endblock %} - Flaskr + + +
+
+ {% block header %}{% endblock %} +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block content %}{% 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. + +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. + +There are three blocks defined here that will be overridden in the other +templates: + +#. ``{% 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 %}

+ {% endblock %} + + {% block content %} +
+ + + + + +
+ {% 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`. diff --git a/docs/tutorial/tests.rst b/docs/tutorial/tests.rst new file mode 100644 index 0000000..f4744cd --- /dev/null +++ b/docs/tutorial/tests.rst @@ -0,0 +1,559 @@ +.. 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 new file mode 100644 index 0000000..7092dbc --- /dev/null +++ b/docs/tutorial/views.rst @@ -0,0 +1,305 @@ +.. currentmodule:: flask + +Blueprints and Views +==================== + +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. + + +Create a Blueprint +------------------ + +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. + +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. + +.. code-block:: python + :caption: ``flaskr/auth.py`` + + import functools + + from flask import ( + Blueprint, flash, g, redirect, render_template, request, session, url_for + ) + from werkzeug.security import check_password_hash, generate_password_hash + + from flaskr.db import get_db + + bp = Blueprint('auth', __name__, url_prefix='/auth') + +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. + +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 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')) + def login(): + 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: + 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') + def logout(): + session.clear() + return redirect(url_for('index')) + + +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`. diff --git a/docs/views.rst b/docs/views.rst new file mode 100644 index 0000000..f221027 --- /dev/null +++ b/docs/views.rst @@ -0,0 +1,324 @@ +Class-based Views +================= + +.. currentmodule:: flask.views + +This page introduces using the :class:`View` and :class:`MethodView` +classes to write class-based views. + +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. + +An example of where this is useful is defining a class that creates an +API based on the database model it is initialized with. + +For more complex API behavior and customization, look into the various +API extensions for Flask. + + +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(): + users = User.query.all() + 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. + +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): + def dispatch_request(self): + users = User.query.all() + return render_template("users.html", objects=users) + + app.add_url_rule("/users/", view_func=UserList.as_view("user_list")) + +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`. + +.. 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 + + class ListView(View): + def __init__(self, model, template): + self.model = model + self.template = template + + def dispatch_request(self): + items = self.model.query.all() + return render_template(self.template, items=items) + +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. + +.. code-block:: python + + 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"), + ) + + +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 + + def dispatch_request(self): + items = self.model.query.all() + return render_template(self.template, items=items) + +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(): + ... + + +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 + + class MyView(View): + methods = ["GET", "POST"] + + def dispatch_request(self): + if request.method == "POST": + ... + ... + + app.add_url_rule('/my-view', view_func=MyView.as_view('my-view')) + +This is equivalent to the following, except further subclasses can +inherit or change the methods. + +.. 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 + + 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) + + def get(self): + items = self.model.query.all() + return jsonify([item.to_json() for item in items]) + + def post(self): + errors = self.validator.validate(request.json) + + if errors: + return jsonify(errors), 400 + + db.session.add(self.model.from_json(request.json)) + db.session.commit() + return jsonify(item.to_json()) + + 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) + + register_api(app, User, "users") + register_api(app, Story, "stories") + +This produces the following views, a standard REST API! + +================= ========== =================== +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 +================= ========== =================== diff --git a/docs/web-security.rst b/docs/web-security.rst new file mode 100644 index 0000000..3992e8d --- /dev/null +++ b/docs/web-security.rst @@ -0,0 +1,274 @@ +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. + +.. _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 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:`~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 +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 + + + +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/GoogleCloudPlatform/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 + + +HTTP Public Key Pinning (HPKP) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This tells the browser to authenticate with the server using only the specific +certificate key to prevent MITM attacks. + +.. warning:: + Be careful when enabling this, as it is very difficult to undo if you set up + or upgrade your key incorrectly. + +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning + + +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 new file mode 100644 index 0000000..038eb51 --- /dev/null +++ b/examples/celery/README.md @@ -0,0 +1,27 @@ +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 new file mode 100644 index 0000000..f7d138e --- /dev/null +++ b/examples/celery/make_celery.py @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000..25887ca --- /dev/null +++ b/examples/celery/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "flask-example-celery" +version = "1.0.0" +description = "Example Flask application with Celery background tasks." +readme = "README.md" +requires-python = ">=3.8" +dependencies = ["flask>=2.2.2", "celery[redis]>=5.2.7"] + +[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 new file mode 100644 index 0000000..29075ab --- /dev/null +++ b/examples/celery/requirements.txt @@ -0,0 +1,58 @@ +# +# 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 new file mode 100644 index 0000000..dafff8a --- /dev/null +++ b/examples/celery/src/task_app/__init__.py @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000..b6b3595 --- /dev/null +++ b/examples/celery/src/task_app/tasks.py @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..4e1145c --- /dev/null +++ b/examples/celery/src/task_app/templates/index.html @@ -0,0 +1,108 @@ + + + + + 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 new file mode 100644 index 0000000..99cf92d --- /dev/null +++ b/examples/celery/src/task_app/views.py @@ -0,0 +1,38 @@ +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/javascript/.gitignore b/examples/javascript/.gitignore new file mode 100644 index 0000000..a306afb --- /dev/null +++ b/examples/javascript/.gitignore @@ -0,0 +1,14 @@ +.venv/ +*.pyc +__pycache__/ +instance/ +.cache/ +.pytest_cache/ +.coverage +htmlcov/ +dist/ +build/ +*.egg-info/ +.idea/ +*.swp +*~ diff --git a/examples/javascript/LICENSE.rst b/examples/javascript/LICENSE.rst new file mode 100644 index 0000000..9d227a0 --- /dev/null +++ b/examples/javascript/LICENSE.rst @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..f5f6691 --- /dev/null +++ b/examples/javascript/README.rst @@ -0,0 +1,48 @@ +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 new file mode 100644 index 0000000..0ec3ca2 --- /dev/null +++ b/examples/javascript/js_example/__init__.py @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..a4d35bd --- /dev/null +++ b/examples/javascript/js_example/templates/base.html @@ -0,0 +1,33 @@ + +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 new file mode 100644 index 0000000..e2944b8 --- /dev/null +++ b/examples/javascript/js_example/templates/fetch.html @@ -0,0 +1,33 @@ +{% 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 new file mode 100644 index 0000000..48f0c11 --- /dev/null +++ b/examples/javascript/js_example/templates/jquery.html @@ -0,0 +1,27 @@ +{% 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 new file mode 100644 index 0000000..1672d4d --- /dev/null +++ b/examples/javascript/js_example/templates/xhr.html @@ -0,0 +1,29 @@ +{% 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 new file mode 100644 index 0000000..9f0d26c --- /dev/null +++ b/examples/javascript/js_example/views.py @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000..f584e5c --- /dev/null +++ b/examples/javascript/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "js_example" +version = "1.1.0" +description = "Demonstrates making AJAX requests to Flask." +readme = "README.rst" +license = {file = "LICENSE.rst"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +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 new file mode 100644 index 0000000..e0cabbf --- /dev/null +++ b/examples/javascript/tests/conftest.py @@ -0,0 +1,15 @@ +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 new file mode 100644 index 0000000..d155ad5 --- /dev/null +++ b/examples/javascript/tests/test_js_example.py @@ -0,0 +1,27 @@ +import pytest +from flask import template_rendered + + +@pytest.mark.parametrize( + ("path", "template_name"), + ( + ("/", "xhr.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/tutorial/.gitignore b/examples/tutorial/.gitignore new file mode 100644 index 0000000..a306afb --- /dev/null +++ b/examples/tutorial/.gitignore @@ -0,0 +1,14 @@ +.venv/ +*.pyc +__pycache__/ +instance/ +.cache/ +.pytest_cache/ +.coverage +htmlcov/ +dist/ +build/ +*.egg-info/ +.idea/ +*.swp +*~ diff --git a/examples/tutorial/LICENSE.rst b/examples/tutorial/LICENSE.rst new file mode 100644 index 0000000..9d227a0 --- /dev/null +++ b/examples/tutorial/LICENSE.rst @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..653c216 --- /dev/null +++ b/examples/tutorial/README.rst @@ -0,0 +1,68 @@ +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 new file mode 100644 index 0000000..e35934d --- /dev/null +++ b/examples/tutorial/flaskr/__init__.py @@ -0,0 +1,51 @@ +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 + try: + os.makedirs(app.instance_path) + except OSError: + pass + + @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 new file mode 100644 index 0000000..34c03a2 --- /dev/null +++ b/examples/tutorial/flaskr/auth.py @@ -0,0 +1,116 @@ +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 new file mode 100644 index 0000000..be0d92c --- /dev/null +++ b/examples/tutorial/flaskr/blog.py @@ -0,0 +1,125 @@ +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 new file mode 100644 index 0000000..acaa4ae --- /dev/null +++ b/examples/tutorial/flaskr/db.py @@ -0,0 +1,52 @@ +import sqlite3 + +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.") + + +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 new file mode 100644 index 0000000..dd4c866 --- /dev/null +++ b/examples/tutorial/flaskr/schema.sql @@ -0,0 +1,20 @@ +-- 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 new file mode 100644 index 0000000..2f1f4d0 --- /dev/null +++ b/examples/tutorial/flaskr/static/style.css @@ -0,0 +1,134 @@ +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 new file mode 100644 index 0000000..b326b5a --- /dev/null +++ b/examples/tutorial/flaskr/templates/auth/login.html @@ -0,0 +1,15 @@ +{% 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 new file mode 100644 index 0000000..4320e17 --- /dev/null +++ b/examples/tutorial/flaskr/templates/auth/register.html @@ -0,0 +1,15 @@ +{% 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 new file mode 100644 index 0000000..f09e926 --- /dev/null +++ b/examples/tutorial/flaskr/templates/base.html @@ -0,0 +1,24 @@ + +{% 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 new file mode 100644 index 0000000..88e31e4 --- /dev/null +++ b/examples/tutorial/flaskr/templates/blog/create.html @@ -0,0 +1,15 @@ +{% 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 new file mode 100644 index 0000000..3481b8e --- /dev/null +++ b/examples/tutorial/flaskr/templates/blog/index.html @@ -0,0 +1,28 @@ +{% 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 new file mode 100644 index 0000000..2c405e6 --- /dev/null +++ b/examples/tutorial/flaskr/templates/blog/update.html @@ -0,0 +1,19 @@ +{% 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 new file mode 100644 index 0000000..73a674c --- /dev/null +++ b/examples/tutorial/pyproject.toml @@ -0,0 +1,39 @@ +[project] +name = "flaskr" +version = "1.0.0" +description = "The basic blog app built in the Flask tutorial." +readme = "README.rst" +license = {text = "BSD-3-Clause"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +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 new file mode 100644 index 0000000..6bf62f0 --- /dev/null +++ b/examples/tutorial/tests/conftest.py @@ -0,0 +1,62 @@ +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 new file mode 100644 index 0000000..9b68006 --- /dev/null +++ b/examples/tutorial/tests/data.sql @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..76db62f --- /dev/null +++ b/examples/tutorial/tests/test_auth.py @@ -0,0 +1,69 @@ +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 new file mode 100644 index 0000000..55c769d --- /dev/null +++ b/examples/tutorial/tests/test_blog.py @@ -0,0 +1,83 @@ +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 new file mode 100644 index 0000000..2363bf8 --- /dev/null +++ b/examples/tutorial/tests/test_db.py @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..9b7ca57 --- /dev/null +++ b/examples/tutorial/tests/test_factory.py @@ -0,0 +1,12 @@ +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/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..64d51c3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,120 @@ +[project] +name = "Flask" +version = "3.1.0.dev" +description = "A simple framework for building complex web applications." +readme = "README.md" +license = {file = "LICENSE.txt"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Flask", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "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.8" +dependencies = [ + "Werkzeug>=3.0.0", + "Jinja2>=3.1.2", + "itsdangerous>=2.1.2", + "click>=8.1.3", + "blinker>=1.6.2", + "importlib-metadata>=3.6.0; python_version < '3.10'", +] + +[project.urls] +Donate = "https://palletsprojects.com/donate" +Documentation = "https://flask.palletsprojects.com/" +Changes = "https://flask.palletsprojects.com/changes/" +Source = "https://github.com/pallets/flask/" +Chat = "https://discord.gg/pallets" + +[project.optional-dependencies] +async = ["asgiref>=3.2"] +dotenv = ["python-dotenv"] + +[project.scripts] +flask = "flask.cli:main" + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "flask" + +[tool.flit.sdist] +include = [ + "docs/", + "examples/", + "requirements/", + "tests/", + "CHANGES.rst", + "CONTRIBUTING.rst", + "tox.ini", +] +exclude = [ + "docs/_build/", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = [ + "error", +] + +[tool.coverage.run] +branch = true +source = ["flask", "tests"] + +[tool.coverage.paths] +source = ["src", "*/site-packages"] + +[tool.mypy] +python_version = "3.8" +files = ["src/flask", "tests/typing"] +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.8" +include = ["src/flask", "tests"] +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 +] +ignore-init-module-imports = true + +[tool.ruff.lint.isort] +force-single-line = true +order-by-type = false diff --git a/requirements-skip/README.md b/requirements-skip/README.md new file mode 100644 index 0000000..675ca4a --- /dev/null +++ b/requirements-skip/README.md @@ -0,0 +1,2 @@ +Dependabot will only update files in the `requirements` directory. This directory is +separate because the pins in here should not be updated automatically. diff --git a/requirements-skip/tests-dev.txt b/requirements-skip/tests-dev.txt new file mode 100644 index 0000000..3e7f028 --- /dev/null +++ b/requirements-skip/tests-dev.txt @@ -0,0 +1,6 @@ +https://github.com/pallets/werkzeug/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/itsdangerous/archive/refs/heads/main.tar.gz +https://github.com/pallets/click/archive/refs/heads/main.tar.gz +https://github.com/pallets-eco/blinker/archive/refs/heads/main.tar.gz diff --git a/requirements-skip/tests-min.in b/requirements-skip/tests-min.in new file mode 100644 index 0000000..c7ec996 --- /dev/null +++ b/requirements-skip/tests-min.in @@ -0,0 +1,6 @@ +werkzeug==3.0.0 +jinja2==3.1.2 +markupsafe==2.1.1 +itsdangerous==2.1.2 +click==8.1.3 +blinker==1.6.2 diff --git a/requirements-skip/tests-min.txt b/requirements-skip/tests-min.txt new file mode 100644 index 0000000..8a6cbf0 --- /dev/null +++ b/requirements-skip/tests-min.txt @@ -0,0 +1,21 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile tests-min.in +# +blinker==1.6.2 + # via -r tests-min.in +click==8.1.3 + # via -r tests-min.in +itsdangerous==2.1.2 + # via -r tests-min.in +jinja2==3.1.2 + # via -r tests-min.in +markupsafe==2.1.1 + # via + # -r tests-min.in + # jinja2 + # werkzeug +werkzeug==3.0.0 + # via -r tests-min.in diff --git a/requirements/build.in b/requirements/build.in new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/requirements/build.in @@ -0,0 +1 @@ +build diff --git a/requirements/build.txt b/requirements/build.txt new file mode 100644 index 0000000..9ecc489 --- /dev/null +++ b/requirements/build.txt @@ -0,0 +1,12 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile build.in +# +build==1.2.1 + # via -r build.in +packaging==24.0 + # via build +pyproject-hooks==1.0.0 + # via build diff --git a/requirements/dev.in b/requirements/dev.in new file mode 100644 index 0000000..1efde82 --- /dev/null +++ b/requirements/dev.in @@ -0,0 +1,5 @@ +-r docs.txt +-r tests.txt +-r typing.txt +pre-commit +tox diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..d0c9ec2 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,195 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile dev.in +# +alabaster==0.7.16 + # via + # -r docs.txt + # sphinx +asgiref==3.8.1 + # via + # -r tests.txt + # -r typing.txt +babel==2.14.0 + # via + # -r docs.txt + # sphinx +cachetools==5.3.3 + # via tox +certifi==2024.2.2 + # via + # -r docs.txt + # requests +cffi==1.16.0 + # via + # -r typing.txt + # cryptography +cfgv==3.4.0 + # via pre-commit +chardet==5.2.0 + # via tox +charset-normalizer==3.3.2 + # via + # -r docs.txt + # requests +colorama==0.4.6 + # via tox +cryptography==42.0.7 + # via -r typing.txt +distlib==0.3.8 + # via virtualenv +docutils==0.20.1 + # via + # -r docs.txt + # sphinx + # sphinx-tabs +filelock==3.13.3 + # via + # tox + # virtualenv +identify==2.5.35 + # via pre-commit +idna==3.6 + # via + # -r docs.txt + # requests +imagesize==1.4.1 + # via + # -r docs.txt + # sphinx +iniconfig==2.0.0 + # via + # -r tests.txt + # -r typing.txt + # pytest +jinja2==3.1.3 + # via + # -r docs.txt + # sphinx +markupsafe==2.1.5 + # via + # -r docs.txt + # jinja2 +mypy==1.10.0 + # via -r typing.txt +mypy-extensions==1.0.0 + # via + # -r typing.txt + # mypy +nodeenv==1.8.0 + # via + # -r typing.txt + # pre-commit + # pyright +packaging==24.0 + # via + # -r docs.txt + # -r tests.txt + # -r typing.txt + # pallets-sphinx-themes + # pyproject-api + # pytest + # sphinx + # tox +pallets-sphinx-themes==2.1.3 + # via -r docs.txt +platformdirs==4.2.0 + # via + # tox + # virtualenv +pluggy==1.5.0 + # via + # -r tests.txt + # -r typing.txt + # pytest + # tox +pre-commit==3.7.0 + # via -r dev.in +pycparser==2.22 + # via + # -r typing.txt + # cffi +pygments==2.17.2 + # via + # -r docs.txt + # sphinx + # sphinx-tabs +pyproject-api==1.6.1 + # via tox +pyright==1.1.361 + # via -r typing.txt +pytest==8.2.0 + # via + # -r tests.txt + # -r typing.txt +python-dotenv==1.0.1 + # via + # -r tests.txt + # -r typing.txt +pyyaml==6.0.1 + # via pre-commit +requests==2.31.0 + # via + # -r docs.txt + # sphinx +snowballstemmer==2.2.0 + # via + # -r docs.txt + # sphinx +sphinx==7.3.7 + # via + # -r docs.txt + # pallets-sphinx-themes + # sphinx-tabs + # sphinxcontrib-log-cabinet +sphinx-tabs==3.4.5 + # via -r docs.txt +sphinxcontrib-applehelp==1.0.8 + # via + # -r docs.txt + # sphinx +sphinxcontrib-devhelp==1.0.6 + # via + # -r docs.txt + # sphinx +sphinxcontrib-htmlhelp==2.0.5 + # via + # -r docs.txt + # sphinx +sphinxcontrib-jsmath==1.0.1 + # via + # -r docs.txt + # sphinx +sphinxcontrib-log-cabinet==1.0.1 + # via -r docs.txt +sphinxcontrib-qthelp==1.0.7 + # via + # -r docs.txt + # sphinx +sphinxcontrib-serializinghtml==1.1.10 + # via + # -r docs.txt + # sphinx +tox==4.15.0 + # via -r dev.in +types-contextvars==2.4.7.3 + # via -r typing.txt +types-dataclasses==0.6.6 + # via -r typing.txt +typing-extensions==4.11.0 + # via + # -r typing.txt + # mypy +urllib3==2.2.1 + # via + # -r docs.txt + # requests +virtualenv==20.25.1 + # via + # pre-commit + # tox + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/docs.in b/requirements/docs.in new file mode 100644 index 0000000..fd5708f --- /dev/null +++ b/requirements/docs.in @@ -0,0 +1,4 @@ +pallets-sphinx-themes +sphinx +sphinxcontrib-log-cabinet +sphinx-tabs diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 0000000..fe7501f --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1,64 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile docs.in +# +alabaster==0.7.16 + # via sphinx +babel==2.14.0 + # via sphinx +certifi==2024.2.2 + # via requests +charset-normalizer==3.3.2 + # via requests +docutils==0.20.1 + # via + # sphinx + # sphinx-tabs +idna==3.6 + # via requests +imagesize==1.4.1 + # via sphinx +jinja2==3.1.3 + # via sphinx +markupsafe==2.1.5 + # via jinja2 +packaging==24.0 + # via + # pallets-sphinx-themes + # sphinx +pallets-sphinx-themes==2.1.3 + # via -r docs.in +pygments==2.17.2 + # via + # sphinx + # sphinx-tabs +requests==2.31.0 + # via sphinx +snowballstemmer==2.2.0 + # via sphinx +sphinx==7.3.7 + # via + # -r docs.in + # pallets-sphinx-themes + # sphinx-tabs + # sphinxcontrib-log-cabinet +sphinx-tabs==3.4.5 + # via -r docs.in +sphinxcontrib-applehelp==1.0.8 + # via sphinx +sphinxcontrib-devhelp==1.0.6 + # via sphinx +sphinxcontrib-htmlhelp==2.0.5 + # via sphinx +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-log-cabinet==1.0.1 + # via -r docs.in +sphinxcontrib-qthelp==1.0.7 + # via sphinx +sphinxcontrib-serializinghtml==1.1.10 + # via sphinx +urllib3==2.2.1 + # via requests diff --git a/requirements/tests.in b/requirements/tests.in new file mode 100644 index 0000000..f4b3dad --- /dev/null +++ b/requirements/tests.in @@ -0,0 +1,4 @@ +pytest +asgiref +greenlet ; python_version < "3.11" +python-dotenv diff --git a/requirements/tests.txt b/requirements/tests.txt new file mode 100644 index 0000000..f4aa903 --- /dev/null +++ b/requirements/tests.txt @@ -0,0 +1,18 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile tests.in +# +asgiref==3.8.1 + # via -r tests.in +iniconfig==2.0.0 + # via pytest +packaging==24.0 + # via pytest +pluggy==1.5.0 + # via pytest +pytest==8.2.0 + # via -r tests.in +python-dotenv==1.0.1 + # via -r tests.in diff --git a/requirements/typing.in b/requirements/typing.in new file mode 100644 index 0000000..59128f3 --- /dev/null +++ b/requirements/typing.in @@ -0,0 +1,8 @@ +mypy +pyright +pytest +types-contextvars +types-dataclasses +asgiref +cryptography +python-dotenv diff --git a/requirements/typing.txt b/requirements/typing.txt new file mode 100644 index 0000000..02969e4 --- /dev/null +++ b/requirements/typing.txt @@ -0,0 +1,41 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile typing.in +# +asgiref==3.8.1 + # via -r typing.in +cffi==1.16.0 + # via cryptography +cryptography==42.0.7 + # via -r typing.in +iniconfig==2.0.0 + # via pytest +mypy==1.10.0 + # via -r typing.in +mypy-extensions==1.0.0 + # via mypy +nodeenv==1.8.0 + # via pyright +packaging==24.0 + # via pytest +pluggy==1.5.0 + # via pytest +pycparser==2.22 + # via cffi +pyright==1.1.361 + # via -r typing.in +pytest==8.2.0 + # via -r typing.in +python-dotenv==1.0.1 + # via -r typing.in +types-contextvars==2.4.7.3 + # via -r typing.in +types-dataclasses==0.6.6 + # via -r typing.in +typing-extensions==4.11.0 + # via mypy + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/src/flask/__init__.py b/src/flask/__init__.py new file mode 100644 index 0000000..e86eb43 --- /dev/null +++ b/src/flask/__init__.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import typing as t + +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 + + +def __getattr__(name: str) -> t.Any: + if name == "__version__": + import importlib.metadata + import warnings + + warnings.warn( + "The '__version__' attribute is deprecated and will be removed in" + " Flask 3.1. Use feature detection or" + " 'importlib.metadata.version(\"flask\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("flask") + + raise AttributeError(name) diff --git a/src/flask/__main__.py b/src/flask/__main__.py new file mode 100644 index 0000000..4e28416 --- /dev/null +++ b/src/flask/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +main() diff --git a/src/flask/app.py b/src/flask/app.py new file mode 100644 index 0000000..7622b5e --- /dev/null +++ b/src/flask/app.py @@ -0,0 +1,1498 @@ +from __future__ import annotations + +import collections.abc as cabc +import os +import sys +import typing as t +import weakref +from datetime import timedelta +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 . import cli +from . import typing as ft +from .ctx import AppContext +from .ctx import RequestContext +from .globals import _cv_app +from .globals import _cv_request +from .globals import current_app +from .globals import g +from .globals import request +from .globals import request_ctx +from .globals import session +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 .sansio.scaffold import _sentinel +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 + +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 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, + "PERMANENT_SESSION_LIFETIME": timedelta(days=31), + "USE_X_SENDFILE": False, + "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_SAMESITE": None, + "SESSION_REFRESH_EACH_REQUEST": True, + "MAX_CONTENT_LENGTH": None, + "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, + } + ) + + #: 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__( + 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 # noqa: B950 + ) + + 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") -> 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". + + Note this is a duplicate of the same method in the Flask + class. + + """ + if mode not in {"r", "rt", "rb"}: + raise ValueError("Resources can only be opened for reading.") + + return open(os.path.join(self.root_path, resource), mode) + + def open_instance_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]: + """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. + :param mode: resource file opening mode, default is 'rb'. + """ + return open(os.path.join(self.instance_path, resource), mode) + + 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. + + .. versionadded:: 0.6 + + .. versionchanged:: 0.9 + This can now also be called without a request object when the + URL adapter is created for the application context. + + .. versionchanged:: 1.0 + :data:`SERVER_NAME` no longer implicitly enables subdomain + matching. Use :attr:`subdomain_matching` instead. + """ + if request is not None: + # If subdomain matching is disabled (the default), use the + # default subdomain in all cases. This should be the default + # in Werkzeug but it currently does not have that feature. + if not self.subdomain_matching: + subdomain = self.url_map.default_subdomain or None + else: + subdomain = None + + return self.url_map.bind_to_environ( + request.environ, + server_name=self.config["SERVER_NAME"], + subdomain=subdomain, + ) + # We need at the very least the server name to be set for this + # to work. + 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, 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 request: + names = chain(names, reversed(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, 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, request.blueprints) + if handler is None: + return e + return self.ensure_sync(handler)(e) # type: ignore[no-any-return] + + def handle_user_exception( + self, 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(e) + + handler = self._find_error_handler(e, request.blueprints) + + if handler is None: + raise + + return self.ensure_sync(handler)(e) # type: ignore[no-any-return] + + def handle_exception(self, 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(exc_info) + server_error: InternalServerError | ft.ResponseReturnValue + server_error = InternalServerError(original_exception=e) + handler = self._find_error_handler(server_error, request.blueprints) + + if handler is not None: + server_error = self.ensure_sync(handler)(server_error) + + return self.finalize_request(server_error, from_error_handler=True) + + def log_exception( + self, + 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 {request.path} [{request.method}]", exc_info=exc_info + ) + + def dispatch_request(self) -> 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 = request_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() + # 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) -> 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 + """ + self._got_first_request = True + + try: + request_started.send(self, _async_wrapper=self.ensure_sync) + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + except Exception as e: + rv = self.handle_user_exception(e) + return self.finalize_request(rv) + + def finalize_request( + self, + 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(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) -> 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 + """ + adapter = request_ctx.url_adapter + methods = 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. + """ + req_ctx = _cv_request.get(None) + + if req_ctx is not None: + url_adapter = req_ctx.url_adapter + blueprint_name = req_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: + app_ctx = _cv_app.get(None) + + # 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 app_ctx is not None: + url_adapter = app_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 = headers = 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 + 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, + 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) # type: ignore[arg-type] + + return rv + + def preprocess_request(self) -> 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. + """ + names = (None, *reversed(request.blueprints)) + + for name in names: + if name in self.url_value_preprocessors: + for url_func in self.url_value_preprocessors[name]: + url_func(request.endpoint, request.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, 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`. + """ + ctx = request_ctx._get_current_object() # type: ignore[attr-defined] + + for func in ctx._after_request_functions: + response = self.ensure_sync(func)(response) + + for name in chain(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.session): + self.session_interface.save_session(self, ctx.session, response) + + return response + + def do_teardown_request( + self, + exc: BaseException | None = _sentinel, # type: ignore[assignment] + ) -> None: + """Called after the request is dispatched and the response is + returned, right before the request context is popped. + + 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. + + This is called by + :meth:`RequestContext.pop() `, + which may be delayed during testing to maintain access to + resources. + + :param exc: An unhandled exception raised while dispatching the + request. Detected from the current exception information if + not passed. Passed to each teardown function. + + .. versionchanged:: 0.9 + Added the ``exc`` argument. + """ + if exc is _sentinel: + exc = sys.exc_info()[1] + + for name in chain(request.blueprints, (None,)): + if name in self.teardown_request_funcs: + for func in reversed(self.teardown_request_funcs[name]): + self.ensure_sync(func)(exc) + + request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) + + def do_teardown_appcontext( + self, + exc: BaseException | None = _sentinel, # type: ignore[assignment] + ) -> None: + """Called right before the application context is popped. + + When handling a request, the application context is popped + after the request context. See :meth:`do_teardown_request`. + + This calls all functions decorated with + :meth:`teardown_appcontext`. Then the + :data:`appcontext_tearing_down` signal is sent. + + This is called by + :meth:`AppContext.pop() `. + + .. versionadded:: 0.9 + """ + if exc is _sentinel: + exc = sys.exc_info()[1] + + for func in reversed(self.teardown_appcontext_funcs): + self.ensure_sync(func)(exc) + + appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) + + def app_context(self) -> AppContext: + """Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` + block to push the context, which will make :data:`current_app` + point at this application. + + An application context is automatically pushed by + :meth:`RequestContext.push() ` + when handling a request, and when running a CLI command. Use + this to manually create a context outside of these situations. + + :: + + with app.app_context(): + init_db() + + See :doc:`/appcontext`. + + .. versionadded:: 0.9 + """ + return AppContext(self) + + def request_context(self, environ: WSGIEnvironment) -> RequestContext: + """Create a :class:`~flask.ctx.RequestContext` representing a + WSGI environment. Use a ``with`` block to push the context, + which will make :data:`request` point at this request. + + See :doc:`/reqcontext`. + + Typically you should not call this from your own code. A request + context is automatically pushed by the :meth:`wsgi_app` when + handling a request. Use :meth:`test_request_context` to create + an environment and context instead of this method. + + :param environ: a WSGI environment + """ + return RequestContext(self, environ) + + def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: + """Create a :class:`~flask.ctx.RequestContext` for a WSGI + environment created from the given values. This is mostly useful + during testing, where you may want to run a function that uses + request data without dispatching a full request. + + See :doc:`/reqcontext`. + + Use a ``with`` block to push the context, which will make + :data:`request` point at the request for the created + environment. :: + + with app.test_request_context(...): + generate_report() + + When using the shell, it may be easier to push and pop the + context manually to avoid indentation. :: + + ctx = app.test_request_context(...) + ctx.push() + ... + ctx.pop() + + 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 append to + :data:`SERVER_NAME`. + :param url_scheme: Scheme to use instead of + :data:`PREFERRED_URL_SCHEME`. + :param data: The request body, either as a string or a dict of + form keys and values. + :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: + return self.request_context(builder.get_environ()) + finally: + builder.close() + + 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. + 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. + """ + ctx = self.request_context(environ) + error: BaseException | None = None + try: + try: + ctx.push() + response = self.full_dispatch_request() + except Exception as e: + error = e + response = self.handle_exception(e) + except: # noqa: B001 + error = sys.exc_info()[1] + raise + return response(environ, start_response) + finally: + if "werkzeug.debug.preserve_context" in environ: + environ["werkzeug.debug.preserve_context"](_cv_app.get()) + environ["werkzeug.debug.preserve_context"](_cv_request.get()) + + if 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 new file mode 100644 index 0000000..aa9eacf --- /dev/null +++ b/src/flask/blueprints.py @@ -0,0 +1,129 @@ +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") -> 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". + + Note this is a duplicate of the same method in the Flask + class. + + """ + if mode not in {"r", "rt", "rb"}: + raise ValueError("Resources can only be opened for reading.") + + return open(os.path.join(self.root_path, resource), mode) diff --git a/src/flask/cli.py b/src/flask/cli.py new file mode 100644 index 0000000..ecb292a --- /dev/null +++ b/src/flask/cli.py @@ -0,0 +1,1109 @@ +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. + """ + + def __init__( + self, + app_import_path: str | None = None, + create_app: t.Callable[..., Flask] | None = None, + set_debug_flag: 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._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 + + if self.create_app is not None: + app: Flask | None = 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) # type: ignore[arg-type] + + 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: + if value is None: + return None + + import importlib + + try: + importlib.import_module("dotenv") + except ImportError: + raise click.BadParameter( + "python-dotenv must be installed to load an env file.", + ctx=ctx, + param=param, + ) from None + + # Don't check FLASK_SKIP_DOTENV, that only disables automatically + # loading .env and .flaskenv files. + load_dotenv(value) + 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. 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:: 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(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 + + if sys.version_info >= (3, 10): + from importlib import metadata + else: + # Use a backport on Python < 3.10. We technically have + # importlib.metadata on 3.8+, but the API changed in 3.10, + # so use the backport for consistency. + import importlib_metadata as metadata + + for ep in 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: # type: ignore[attr-defined] + 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" + + # Attempt to load .env and .flask env files. The --env-file + # option can cause another file to be loaded. + if get_load_dotenv(self.load_dotenv): + load_dotenv() + + 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 + ) + + 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: + # 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) -> bool: + """Load "dotenv" files in order of precedence to set environment variables. + + If an env var is already set it is not overwritten, so earlier files in the + list are preferred over later files. + + 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 instead of searching. + :return: ``True`` if a file was loaded. + + .. 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 or .flaskenv files present." + ' Do "pip install python-dotenv" to use them.', + fg="yellow", + err=True, + ) + + return False + + # Always return after attempting to load a given path, don't load + # the default files. + if path is not None: + if os.path.isfile(path): + return dotenv.load_dotenv(path, encoding="utf-8") + + return False + + loaded = False + + for name in (".env", ".flaskenv"): + path = dotenv.find_dotenv(name, usecwd=True) + + if not path: + continue + + dotenv.load_dotenv(path, encoding="utf-8") + loaded = True + + return loaded # True if at least one file was located and 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): + """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() + + 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() + 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 new file mode 100644 index 0000000..7e3ba17 --- /dev/null +++ b/src/flask/config.py @@ -0,0 +1,370 @@ +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}_" + len_prefix = len(prefix) + + for key in sorted(os.environ): + if not key.startswith(prefix): + continue + + value = os.environ[key] + + try: + value = loads(value) + except Exception: + # Keep the value as a string if loading failed. + pass + + # Change to key.removeprefix(prefix) on Python >= 3.9. + key = key[len_prefix:] + + 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 new file mode 100644 index 0000000..9b164d3 --- /dev/null +++ b/src/flask/ctx.py @@ -0,0 +1,449 @@ +from __future__ import annotations + +import contextvars +import sys +import typing as t +from functools import update_wrapper +from types import TracebackType + +from werkzeug.exceptions import HTTPException + +from . import typing as ft +from .globals import _cv_app +from .globals import _cv_request +from .signals import appcontext_popped +from .signals import appcontext_pushed + +if t.TYPE_CHECKING: # pragma: no cover + 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]: + """Executes a function after this request. This is useful to modify + response objects. The function is passed the response object and has + to return the same or a new one. + + Example:: + + @app.route('/') + def index(): + @after_this_request + def add_header(response): + response.headers['X-Foo'] = 'Parachute' + return response + return 'Hello World!' + + This is more useful if a function other than the view function wants to + modify a response. For instance think of a decorator that wants to add + some headers without converting the return value into a response object. + + .. versionadded:: 0.9 + """ + ctx = _cv_request.get(None) + + if ctx is None: + 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: + """A helper function that decorates a function to retain the current + request context. This is useful when working with greenlets. The moment + the function is decorated a copy of the request context is created and + then pushed when the function is called. The current session is also + included in the copied request context. + + Example:: + + 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 + """ + ctx = _cv_request.get(None) + + if ctx is None: + raise RuntimeError( + "'copy_current_request_context' can only be used when a" + " request context is active, such as in a view function." + ) + + ctx = ctx.copy() + + def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any: + with ctx: # type: ignore[union-attr] + return ctx.app.ensure_sync(f)(*args, **kwargs) # type: ignore[union-attr] + + return update_wrapper(wrapper, f) # type: ignore[return-value] + + +def has_request_context() -> bool: + """If you have code that wants to test if a request context is there or + not this function can be used. For instance, you may want to take advantage + of request information if the request object is available, but fail + silently if it 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 _cv_request.get(None) is not None + + +def has_app_context() -> bool: + """Works like :func:`has_request_context` but for the application + context. You can also just do a boolean check on the + :data:`current_app` object instead. + + .. versionadded:: 0.9 + """ + return _cv_app.get(None) is not None + + +class AppContext: + """The app context contains application-specific information. An app + context is created and pushed at the beginning of each request if + one is not already active. An app context is also pushed when + running CLI commands. + """ + + def __init__(self, app: Flask) -> None: + self.app = app + self.url_adapter = app.create_url_adapter(None) + self.g: _AppCtxGlobals = app.app_ctx_globals_class() + self._cv_tokens: list[contextvars.Token[AppContext]] = [] + + def push(self) -> None: + """Binds the app context to the current context.""" + self._cv_tokens.append(_cv_app.set(self)) + appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync) + + def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore + """Pops the app context.""" + try: + if len(self._cv_tokens) == 1: + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_appcontext(exc) + finally: + ctx = _cv_app.get() + _cv_app.reset(self._cv_tokens.pop()) + + if ctx is not self: + raise AssertionError( + f"Popped wrong app context. ({ctx!r} instead of {self!r})" + ) + + appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync) + + def __enter__(self) -> AppContext: + self.push() + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.pop(exc_value) + + +class RequestContext: + """The request context contains per-request information. The Flask + app creates and pushes it at the beginning of the request, then pops + it at the end of the request. 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. 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. + """ + + def __init__( + self, + app: Flask, + environ: WSGIEnvironment, + request: Request | None = None, + session: SessionMixin | None = None, + ) -> None: + self.app = app + if request is None: + request = app.request_class(environ) + request.json_module = app.json + self.request: Request = request + self.url_adapter = None + try: + self.url_adapter = app.create_url_adapter(self.request) + except HTTPException as e: + self.request.routing_exception = e + self.flashes: list[tuple[str, str]] | None = None + self.session: SessionMixin | None = session + # Functions that should be executed after the request on the response + # object. These will be called before the regular "after_request" + # functions. + self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = [] + + self._cv_tokens: list[ + tuple[contextvars.Token[RequestContext], AppContext | None] + ] = [] + + def copy(self) -> RequestContext: + """Creates a copy of this request context with the same request object. + This can be used to move a request context to a different greenlet. + Because the actual request object is the same this cannot be used to + move a request context to a different thread unless access to the + request object is locked. + + .. versionadded:: 0.10 + + .. versionchanged:: 1.1 + The current session object is used instead of reloading the original + data. This prevents `flask.session` pointing to an out-of-date object. + """ + return self.__class__( + self.app, + environ=self.request.environ, + request=self.request, + session=self.session, + ) + + def match_request(self) -> None: + """Can be overridden by a subclass to hook into the matching + of the request. + """ + try: + result = self.url_adapter.match(return_rule=True) # type: ignore + self.request.url_rule, self.request.view_args = result # type: ignore + except HTTPException as e: + self.request.routing_exception = e + + def push(self) -> None: + # Before we push the request context we have to ensure that there + # is an application context. + app_ctx = _cv_app.get(None) + + if app_ctx is None or app_ctx.app is not self.app: + app_ctx = self.app.app_context() + app_ctx.push() + else: + app_ctx = None + + self._cv_tokens.append((_cv_request.set(self), app_ctx)) + + # Open the session at the moment that the request context is available. + # This allows a custom open_session method to use the request context. + # Only open a new session if this is the first time the request was + # pushed, otherwise stream_with_context loses the session. + if self.session is None: + session_interface = self.app.session_interface + self.session = session_interface.open_session(self.app, self.request) + + if self.session is None: + self.session = session_interface.make_null_session(self.app) + + # 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 = _sentinel) -> None: # type: ignore + """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. + + .. versionchanged:: 0.9 + Added the `exc` argument. + """ + clear_request = len(self._cv_tokens) == 1 + + try: + if clear_request: + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_request(exc) + + request_close = getattr(self.request, "close", None) + if request_close is not None: + request_close() + finally: + ctx = _cv_request.get() + token, app_ctx = self._cv_tokens.pop() + _cv_request.reset(token) + + # get rid of circular dependencies at the end of the request + # so that we don't require the GC to be active. + if clear_request: + ctx.request.environ["werkzeug.request"] = None + + if app_ctx is not None: + app_ctx.pop(exc) + + if ctx is not self: + raise AssertionError( + f"Popped wrong request context. ({ctx!r} instead of {self!r})" + ) + + def __enter__(self) -> RequestContext: + self.push() + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.pop(exc_value) + + def __repr__(self) -> str: + return ( + f"<{type(self).__name__} {self.request.url!r}" + f" [{self.request.method}] of {self.app.name}>" + ) diff --git a/src/flask/debughelpers.py b/src/flask/debughelpers.py new file mode 100644 index 0000000..2c8c4c4 --- /dev/null +++ b/src/flask/debughelpers.py @@ -0,0 +1,178 @@ +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 request_ctx +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 request_ctx and request_ctx.request.blueprint is not None: + blueprint = request_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 new file mode 100644 index 0000000..e2c410c --- /dev/null +++ b/src/flask/globals.py @@ -0,0 +1,51 @@ +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 .ctx import RequestContext + from .sessions import SessionMixin + from .wrappers import Request + + +_no_app_msg = """\ +Working outside of application context. + +This typically means that you attempted to use functionality that needed +the current application. To solve this, set up an application context +with app.app_context(). See the documentation for more information.\ +""" +_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx") +app_ctx: AppContext = LocalProxy( # type: ignore[assignment] + _cv_app, unbound_message=_no_app_msg +) +current_app: Flask = LocalProxy( # type: ignore[assignment] + _cv_app, "app", unbound_message=_no_app_msg +) +g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment] + _cv_app, "g", unbound_message=_no_app_msg +) + +_no_req_msg = """\ +Working outside of request context. + +This typically means that you attempted to use functionality that needed +an active HTTP request. Consult the documentation on testing for +information about how to avoid this problem.\ +""" +_cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx") +request_ctx: RequestContext = LocalProxy( # type: ignore[assignment] + _cv_request, unbound_message=_no_req_msg +) +request: Request = LocalProxy( # type: ignore[assignment] + _cv_request, "request", unbound_message=_no_req_msg +) +session: SessionMixin = LocalProxy( # type: ignore[assignment] + _cv_request, "session", unbound_message=_no_req_msg +) diff --git a/src/flask/helpers.py b/src/flask/helpers.py new file mode 100644 index 0000000..00abe04 --- /dev/null +++ b/src/flask/helpers.py @@ -0,0 +1,621 @@ +from __future__ import annotations + +import importlib.util +import os +import sys +import typing as t +from datetime import datetime +from functools import lru_cache +from functools import update_wrapper + +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_request +from .globals import current_app +from .globals import request +from .globals import request_ctx +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") + + +def stream_with_context( + generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]], +) -> t.Iterator[t.AnyStr]: + """Request contexts disappear when the response is started on the server. + This is done for efficiency reasons and to make it less likely to encounter + memory leaks with badly written WSGI middlewares. The downside is that if + you are using streamed responses, the generator cannot access request bound + information any more. + + This function however can help you keep the context around for longer:: + + from flask import stream_with_context, request, Response + + @app.route('/stream') + def streamed_response(): + @stream_with_context + def generate(): + yield 'Hello ' + yield request.args['name'] + yield '!' + return Response(generate()) + + Alternatively it can also be used around a specific generator:: + + from flask import stream_with_context, request, Response + + @app.route('/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, return-value] + + def generator() -> t.Iterator[t.AnyStr | None]: + ctx = _cv_request.get(None) + if ctx 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: + # Dummy sentinel. Has to be inside the context block or we're + # not actually keeping the context around. + yield None + + # The try/finally is here so that if someone passes a WSGI level + # iterator in we're still running the cleanup logic. Generators + # don't need that because they are closed on their destruction + # automatically. + try: + yield from gen + finally: + if hasattr(gen, "close"): + gen.close() + + # The trick is to start the generator. Then the code execution runs until + # the first dummy None is yielded at which point the context was already + # pushed. This item is discarded. Then when the iteration continues the + # real generator is executed. + wrapped_g = generator() + next(wrapped_g) + return wrapped_g # type: ignore[return-value] + + +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 = 302, 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``. + + .. versionadded:: 2.2 + Calls ``current_app.redirect`` if available instead of always + using Werkzeug's default ``redirect``. + """ + if current_app: + return current_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 current_app: + current_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() # type: ignore + 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 = request_ctx.flashes + if flashes is None: + flashes = session.pop("_flashes") if "_flashes" in session else [] + request_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]: + if kwargs.get("max_age") is None: + kwargs["max_age"] = current_app.get_send_file_max_age + + kwargs.update( + environ=request.environ, + use_x_sendfile=current_app.config["USE_X_SENDFILE"], + response_class=current_app.response_class, + _root_path=current_app.root_path, # type: ignore + ) + return kwargs + + +def send_file( + path_or_file: os.PathLike[t.AnyStr] | str | t.BinaryIO, + 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. + :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) + 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] + + +@lru_cache(maxsize=None) +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 diff --git a/src/flask/json/__init__.py b/src/flask/json/__init__.py new file mode 100644 index 0000000..c0941d0 --- /dev/null +++ b/src/flask/json/__init__.py @@ -0,0 +1,170 @@ +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 request or application 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 new file mode 100644 index 0000000..f9b2e8f --- /dev/null +++ b/src/flask/json/provider.py @@ -0,0 +1,215 @@ +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) + + 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) # type: ignore[assignment] + """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 new file mode 100644 index 0000000..8dc3629 --- /dev/null +++ b/src/flask/json/tag.py @@ -0,0 +1,327 @@ +""" +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 new file mode 100644 index 0000000..0cb8f43 --- /dev/null +++ b/src/flask/logging.py @@ -0,0 +1,79 @@ +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/py.typed b/src/flask/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/flask/sansio/README.md b/src/flask/sansio/README.md new file mode 100644 index 0000000..623ac19 --- /dev/null +++ b/src/flask/sansio/README.md @@ -0,0 +1,6 @@ +# 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 new file mode 100644 index 0000000..01fd5db --- /dev/null +++ b/src/flask/sansio/app.py @@ -0,0 +1,964 @@ +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. + #: + #: In Flask 0.9 this property was called `request_globals_class` but it + #: was changed in 0.10 to :attr:`app_ctx_globals_class` because the + #: flask.g object is now application context scoped. + #: + #: .. versionadded:: 0.10 + 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[t.Union[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, + ): + 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: # type: ignore + """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 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 select_jinja_autoescape(self, filename: str) -> bool: + """Returns ``True`` if autoescaping should be active for the given + template name. If no template name is given, returns `True`. + + .. versionchanged:: 2.2 + Autoescaping is now enabled by default for ``.svg`` files. + + .. versionadded:: 0.5 + """ + if filename is None: + return True + return filename.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(getattr(view_func, "required_methods", ())) + + # starting with Flask 0.8 the view_func object can disable and + # force-enable the automatic options handling. + if provide_automatic_options is None: + provide_automatic_options = getattr( + view_func, "provide_automatic_options", None + ) + + if provide_automatic_options is None: + if "OPTIONS" not in methods: + provide_automatic_options = True + required_methods.add("OPTIONS") + else: + provide_automatic_options = False + + # 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 + + @setupmethod + def template_filter( + self, name: str | None = None + ) -> t.Callable[[T_template_filter], T_template_filter]: + """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: 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 custom template filter. Works exactly like the + :meth:`template_filter` decorator. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + self.jinja_env.filters[name or f.__name__] = f + + @setupmethod + def template_test( + self, name: str | None = None + ) -> t.Callable[[T_template_test], T_template_test]: + """A decorator that is used to register custom template test. + You can specify a name for the test, otherwise the function + name will be used. Example:: + + @app.template_test() + def is_prime(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 + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + 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 custom template test. Works exactly like the + :meth:`template_test` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + self.jinja_env.tests[name or f.__name__] = f + + @setupmethod + def template_global( + self, name: str | None = None + ) -> t.Callable[[T_template_global], T_template_global]: + """A decorator that is used to register a custom template global function. + You can specify a name for the global function, otherwise the function + name will be used. Example:: + + @app.template_global() + def double(n): + return 2 * n + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + + 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 custom template global function. Works exactly like the + :meth:`template_global` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + 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 application + context is popped. The application context is typically popped + after the request context for each request, at the end of CLI + commands, or after a manually pushed context ends. + + .. 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. Since a request context typically also manages an + application context it would also be called when you pop a + request context. + + 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 + + def should_ignore_error(self, error: BaseException | None) -> bool: + """This is called to figure out if an error should be ignored + or not as far as the teardown system is concerned. If this + function returns ``True`` then the teardown handlers will not be + passed the error. + + .. versionadded:: 0.10 + """ + return False + + def redirect(self, location: str, code: int = 302) -> 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. + + .. 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 new file mode 100644 index 0000000..4f912cc --- /dev/null +++ b/src/flask/sansio/blueprints.py @@ -0,0 +1,632 @@ +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, + ) + ) + + @setupmethod + def app_template_filter( + self, name: str | None = None + ) -> t.Callable[[T_template_filter], T_template_filter]: + """Register a template filter, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_filter`. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + 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 template filter, available in any template rendered by the + application. Works like the :meth:`app_template_filter` decorator. Equivalent to + :meth:`.Flask.add_template_filter`. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.filters[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def app_template_test( + self, name: str | None = None + ) -> t.Callable[[T_template_test], T_template_test]: + """Register a template test, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_test`. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + 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 template test, available in any template rendered by the + application. Works like the :meth:`app_template_test` decorator. Equivalent to + :meth:`.Flask.add_template_test`. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.tests[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def app_template_global( + self, name: str | None = None + ) -> t.Callable[[T_template_global], T_template_global]: + """Register a template global, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_global`. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + 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 template global, available in any template rendered by the + application. Works like the :meth:`app_template_global` decorator. Equivalent to + :meth:`.Flask.add_template_global`. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.globals[name or f.__name__] = f + + self.record_once(register_template) + + @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 new file mode 100644 index 0000000..69e33a0 --- /dev/null +++ b/src/flask/sansio/scaffold.py @@ -0,0 +1,801 @@ +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 # type: ignore + 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 as well 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 _path_is_relative_to(path: pathlib.PurePath, base: str) -> bool: + # Path.is_relative_to doesn't exist until Python 3.9 + try: + path.relative_to(base) + return True + except ValueError: + return False + + +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 _path_is_relative_to(package_path, 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 _path_is_relative_to(pathlib.PurePath(package_path), 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 new file mode 100644 index 0000000..05b367a --- /dev/null +++ b/src/flask/sessions.py @@ -0,0 +1,379 @@ +from __future__ import annotations + +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 + + +# TODO generic when Python > 3.8 +class SessionMixin(MutableMapping): # type: ignore[type-arg] + """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) + + @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 + + #: Some implementations can detect when session data is read or + #: written and set this when that happens. The mixin default is hard + #: coded to ``True``. + accessed = True + + +# TODO generic when Python > 3.8 +class SecureCookieSession(CallbackDict, SessionMixin): # type: ignore[type-arg] + """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 + + #: When data is read or written, this is set to ``True``. Used by + # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie`` + #: header, which allows caching proxies to cache different pages for + #: different users. + accessed = False + + def __init__(self, initial: t.Any = None) -> None: + def on_update(self: te.Self) -> None: + self.modified = True + self.accessed = True + + super().__init__(initial, on_update) + + def __getitem__(self, key: str) -> t.Any: + self.accessed = True + return super().__getitem__(key) + + def get(self, key: str, default: t.Any = None) -> t.Any: + self.accessed = True + return super().get(key, default) + + def setdefault(self, key: str, default: t.Any = None) -> t.Any: + self.accessed = True + return super().setdefault(key, default) + + +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 # type: ignore # noqa: B950 + 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_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 + signer_kwargs = dict( + key_derivation=self.key_derivation, digest_method=self.digest_method + ) + return URLSafeTimedSerializer( + app.secret_key, + salt=self.salt, + serializer=self.serializer, + signer_kwargs=signer_kwargs, + ) + + 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) + 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, + 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, + samesite=samesite, + ) + response.vary.add("Cookie") diff --git a/src/flask/signals.py b/src/flask/signals.py new file mode 100644 index 0000000..444fda9 --- /dev/null +++ b/src/flask/signals.py @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..618a3b3 --- /dev/null +++ b/src/flask/templating.py @@ -0,0 +1,219 @@ +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 .globals import _cv_app +from .globals import _cv_request +from .globals import current_app +from .globals import request +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 .app import Flask + from .sansio.app import App + from .sansio.scaffold import Scaffold + + +def _default_template_ctx_processor() -> dict[str, t.Any]: + """Default template context processor. Injects `request`, + `session` and `g`. + """ + appctx = _cv_app.get(None) + reqctx = _cv_request.get(None) + rv: dict[str, t.Any] = {} + if appctx is not None: + rv["g"] = appctx.g + if reqctx is not None: + rv["request"] = reqctx.request + rv["session"] = reqctx.session + return rv + + +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: 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(app: Flask, template: Template, context: dict[str, t.Any]) -> str: + app.update_template_context(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. + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.get_or_select_template(template_name_or_list) + return _render(app, 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. + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.from_string(source) + return _render(app, template, context) + + +def _stream( + app: Flask, template: Template, context: dict[str, t.Any] +) -> t.Iterator[str]: + app.update_template_context(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 + ) + + rv = generate() + + # If a request context is active, keep it while generating. + if request: + rv = stream_with_context(rv) + + return rv + + +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 + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.get_or_select_template(template_name_or_list) + return _stream(app, 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 + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.from_string(source) + return _stream(app, template, context) diff --git a/src/flask/testing.py b/src/flask/testing.py new file mode 100644 index 0000000..a27b7c8 --- /dev/null +++ b/src/flask/testing.py @@ -0,0 +1,298 @@ +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 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: + sep = b"?" if isinstance(url.query, bytes) else "?" + path += sep + url.query + + self.app = app + super().__init__(path, base_url, *args, **kwargs) + + def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore + """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 but has knowledge about + Flask's contexts to 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. + while self._new_contexts: + cm = self._new_contexts.pop() + self._context_stack.enter_context(cm) + + 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 + ) -> t.Any: + """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 new file mode 100644 index 0000000..cf6d4ae --- /dev/null +++ b/src/flask/typing.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +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, + t.List[t.Any], + # Only dict is actually accepted, but Mapping allows for TypedDict. + t.Mapping[str, t.Any], + t.Iterator[str], + t.Iterator[bytes], +] + +# the possible types for an individual HTTP header +# This should be a Union, but mypy doesn't pass unless it's a TypeVar. +HeaderValue = t.Union[str, t.List[str], t.Tuple[str, ...]] + +# the possible types for HTTP headers +HeadersValue = t.Union[ + "Headers", + t.Mapping[str, HeaderValue], + t.Sequence[t.Tuple[str, HeaderValue]], +] + +# The possible types returned by a route function. +ResponseReturnValue = t.Union[ + ResponseValue, + t.Tuple[ResponseValue, HeadersValue], + t.Tuple[ResponseValue, int], + t.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 = t.Optional[str] # The App key is None, whereas blueprints are named +AfterRequestCallable = t.Union[ + t.Callable[[ResponseClass], ResponseClass], + t.Callable[[ResponseClass], t.Awaitable[ResponseClass]], +] +BeforeFirstRequestCallable = t.Union[ + t.Callable[[], None], t.Callable[[], t.Awaitable[None]] +] +BeforeRequestCallable = t.Union[ + t.Callable[[], t.Optional[ResponseReturnValue]], + t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]], +] +ShellContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]] +TeardownCallable = t.Union[ + t.Callable[[t.Optional[BaseException]], None], + t.Callable[[t.Optional[BaseException]], t.Awaitable[None]], +] +TemplateContextProcessorCallable = t.Union[ + t.Callable[[], t.Dict[str, t.Any]], + t.Callable[[], t.Awaitable[t.Dict[str, t.Any]]], +] +TemplateFilterCallable = t.Callable[..., t.Any] +TemplateGlobalCallable = t.Callable[..., t.Any] +TemplateTestCallable = t.Callable[..., bool] +URLDefaultCallable = t.Callable[[str, t.Dict[str, t.Any]], None] +URLValuePreprocessorCallable = t.Callable[ + [t.Optional[str], t.Optional[t.Dict[str, t.Any]]], 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.Union[ + t.Callable[[t.Any], ResponseReturnValue], + t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]], +] + +RouteCallable = t.Union[ + t.Callable[..., ResponseReturnValue], + t.Callable[..., t.Awaitable[ResponseReturnValue]], +] diff --git a/src/flask/views.py b/src/flask/views.py new file mode 100644 index 0000000..794fdc0 --- /dev/null +++ b/src/flask/views.py @@ -0,0 +1,191 @@ +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[[F], F]]] = [] + + #: 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) + + 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 new file mode 100644 index 0000000..db3118e --- /dev/null +++ b/src/flask/wrappers.py @@ -0,0 +1,174 @@ +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 + + @property + def max_content_length(self) -> int | None: # type: ignore[override] + """Read-only view of the ``MAX_CONTENT_LENGTH`` config key.""" + if current_app: + return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return] + else: + return None + + @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 e: + if current_app and current_app.debug: + raise + + raise BadRequest() from e + + +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 new file mode 100644 index 0000000..58cf85d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,160 @@ +import os +import pkgutil +import sys + +import pytest +from _pytest import monkeypatch + +from flask import Flask +from flask.globals import request_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(): + yield + + # make sure we're not leaking a request context since we are + # testing flask internally in debug mode in a few cases + leaks = [] + while request_ctx: + leaks.append(request_ctx._get_current_object()) + request_ctx.pop() + + assert leaks == [] + + +@pytest.fixture(params=(True, False)) +def limit_loader(request, monkeypatch): + """Patch pkgutil.get_loader to give loader without get_filename or archive. + + This provides for tests where a system has custom loaders, e.g. Google App + Engine's HardenedModulesHook, which have neither the `get_filename` method + nor the `archive` attribute. + + This fixture will run the testcase twice, once with and once without the + limitation/mock. + """ + if not request.param: + return + + class LimitedLoader: + def __init__(self, loader): + self.loader = loader + + def __getattr__(self, name): + if name in {"archive", "get_filename"}: + raise AttributeError(f"Mocking a loader which does not have {name!r}.") + return getattr(self.loader, name) + + old_get_loader = pkgutil.get_loader + + def get_loader(*args, **kwargs): + return LimitedLoader(old_get_loader(*args, **kwargs)) + + monkeypatch.setattr(pkgutil, "get_loader", get_loader) + + +@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 new file mode 100644 index 0000000..4eedab1 --- /dev/null +++ b/tests/static/config.json @@ -0,0 +1,4 @@ +{ + "TEST_KEY": "foo", + "SECRET_KEY": "config" +} diff --git a/tests/static/config.toml b/tests/static/config.toml new file mode 100644 index 0000000..64acdbd --- /dev/null +++ b/tests/static/config.toml @@ -0,0 +1,2 @@ +TEST_KEY="foo" +SECRET_KEY="config" diff --git a/tests/static/index.html b/tests/static/index.html new file mode 100644 index 0000000..de8b69b --- /dev/null +++ b/tests/static/index.html @@ -0,0 +1 @@ +

Hello World!

diff --git a/tests/templates/_macro.html b/tests/templates/_macro.html new file mode 100644 index 0000000..3460ae2 --- /dev/null +++ b/tests/templates/_macro.html @@ -0,0 +1 @@ +{% macro hello(name) %}Hello {{ name }}!{% endmacro %} diff --git a/tests/templates/context_template.html b/tests/templates/context_template.html new file mode 100644 index 0000000..fadf3e5 --- /dev/null +++ b/tests/templates/context_template.html @@ -0,0 +1 @@ +

{{ value }}|{{ injected_value }} diff --git a/tests/templates/escaping_template.html b/tests/templates/escaping_template.html new file mode 100644 index 0000000..dc47644 --- /dev/null +++ b/tests/templates/escaping_template.html @@ -0,0 +1,6 @@ +{{ text }} +{{ html }} +{% autoescape false %}{{ text }} +{{ html }}{% endautoescape %} +{% autoescape true %}{{ text }} +{{ html }}{% endautoescape %} diff --git a/tests/templates/mail.txt b/tests/templates/mail.txt new file mode 100644 index 0000000..d6cb92e --- /dev/null +++ b/tests/templates/mail.txt @@ -0,0 +1 @@ +{{ foo}} Mail diff --git a/tests/templates/nested/nested.txt b/tests/templates/nested/nested.txt new file mode 100644 index 0000000..2c8634f --- /dev/null +++ b/tests/templates/nested/nested.txt @@ -0,0 +1 @@ +I'm nested diff --git a/tests/templates/non_escaping_template.txt b/tests/templates/non_escaping_template.txt new file mode 100644 index 0000000..542864e --- /dev/null +++ b/tests/templates/non_escaping_template.txt @@ -0,0 +1,8 @@ +{{ text }} +{{ html }} +{% autoescape false %}{{ text }} +{{ html }}{% endautoescape %} +{% autoescape true %}{{ text }} +{{ html }}{% endautoescape %} +{{ text }} +{{ html }} diff --git a/tests/templates/simple_template.html b/tests/templates/simple_template.html new file mode 100644 index 0000000..c24612c --- /dev/null +++ b/tests/templates/simple_template.html @@ -0,0 +1 @@ +

{{ whiskey }}

diff --git a/tests/templates/template_filter.html b/tests/templates/template_filter.html new file mode 100644 index 0000000..d51506a --- /dev/null +++ b/tests/templates/template_filter.html @@ -0,0 +1 @@ +{{ value|super_reverse }} diff --git a/tests/templates/template_test.html b/tests/templates/template_test.html new file mode 100644 index 0000000..92d5561 --- /dev/null +++ b/tests/templates/template_test.html @@ -0,0 +1,3 @@ +{% if value is boolean %} + Success! +{% endif %} diff --git a/tests/test_appctx.py b/tests/test_appctx.py new file mode 100644 index 0000000..ca9e079 --- /dev/null +++ b/tests/test_appctx.py @@ -0,0 +1,209 @@ +import pytest + +import flask +from flask.globals import app_ctx +from flask.globals import request_ctx + + +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("/") + + assert cleanup_stuff == [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) == 1 + assert isinstance(cleanup_stuff[0], ValueError) + assert str(cleanup_stuff[0]) == "dummy" + + +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: + with request_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 diff --git a/tests/test_apps/.env b/tests/test_apps/.env new file mode 100644 index 0000000..0890b61 --- /dev/null +++ b/tests/test_apps/.env @@ -0,0 +1,4 @@ +FOO=env +SPAM=1 +EGGS=2 +HAM=火腿 diff --git a/tests/test_apps/.flaskenv b/tests/test_apps/.flaskenv new file mode 100644 index 0000000..59f96af --- /dev/null +++ b/tests/test_apps/.flaskenv @@ -0,0 +1,3 @@ +FOO=flaskenv +BAR=bar +EGGS=0 diff --git a/tests/test_apps/blueprintapp/__init__.py b/tests/test_apps/blueprintapp/__init__.py new file mode 100644 index 0000000..ad594cf --- /dev/null +++ b/tests/test_apps/blueprintapp/__init__.py @@ -0,0 +1,9 @@ +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/__init__.py b/tests/test_apps/blueprintapp/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_apps/blueprintapp/apps/admin/__init__.py b/tests/test_apps/blueprintapp/apps/admin/__init__.py new file mode 100644 index 0000000..b197fad --- /dev/null +++ b/tests/test_apps/blueprintapp/apps/admin/__init__.py @@ -0,0 +1,20 @@ +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/admin/static/css/test.css b/tests/test_apps/blueprintapp/apps/admin/static/css/test.css new file mode 100644 index 0000000..b9f564d --- /dev/null +++ b/tests/test_apps/blueprintapp/apps/admin/static/css/test.css @@ -0,0 +1 @@ +/* nested file */ diff --git a/tests/test_apps/blueprintapp/apps/admin/static/test.txt b/tests/test_apps/blueprintapp/apps/admin/static/test.txt new file mode 100644 index 0000000..f220d22 --- /dev/null +++ b/tests/test_apps/blueprintapp/apps/admin/static/test.txt @@ -0,0 +1 @@ +Admin File diff --git a/tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html b/tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html new file mode 100644 index 0000000..eeec199 --- /dev/null +++ b/tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html @@ -0,0 +1 @@ +Hello from the Admin diff --git a/tests/test_apps/blueprintapp/apps/frontend/__init__.py b/tests/test_apps/blueprintapp/apps/frontend/__init__.py new file mode 100644 index 0000000..7cc5cd8 --- /dev/null +++ b/tests/test_apps/blueprintapp/apps/frontend/__init__.py @@ -0,0 +1,14 @@ +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/blueprintapp/apps/frontend/templates/frontend/index.html b/tests/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html new file mode 100644 index 0000000..a062d71 --- /dev/null +++ b/tests/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html @@ -0,0 +1 @@ +Hello from the Frontend diff --git a/tests/test_apps/cliapp/__init__.py b/tests/test_apps/cliapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_apps/cliapp/app.py b/tests/test_apps/cliapp/app.py new file mode 100644 index 0000000..017ce28 --- /dev/null +++ b/tests/test_apps/cliapp/app.py @@ -0,0 +1,3 @@ +from flask import Flask + +testapp = Flask("testapp") diff --git a/tests/test_apps/cliapp/factory.py b/tests/test_apps/cliapp/factory.py new file mode 100644 index 0000000..1d27396 --- /dev/null +++ b/tests/test_apps/cliapp/factory.py @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000..2c96c9b --- /dev/null +++ b/tests/test_apps/cliapp/importerrorapp.py @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..8330f6e --- /dev/null +++ b/tests/test_apps/cliapp/inner1/__init__.py @@ -0,0 +1,3 @@ +from flask import Flask + +application = Flask(__name__) diff --git a/tests/test_apps/cliapp/inner1/inner2/__init__.py b/tests/test_apps/cliapp/inner1/inner2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_apps/cliapp/inner1/inner2/flask.py b/tests/test_apps/cliapp/inner1/inner2/flask.py new file mode 100644 index 0000000..d7562aa --- /dev/null +++ b/tests/test_apps/cliapp/inner1/inner2/flask.py @@ -0,0 +1,3 @@ +from flask import Flask + +app = Flask(__name__) diff --git a/tests/test_apps/cliapp/message.txt b/tests/test_apps/cliapp/message.txt new file mode 100644 index 0000000..fc2b2cf --- /dev/null +++ b/tests/test_apps/cliapp/message.txt @@ -0,0 +1 @@ +So long, and thanks for all the fish. diff --git a/tests/test_apps/cliapp/multiapp.py b/tests/test_apps/cliapp/multiapp.py new file mode 100644 index 0000000..4ed0f32 --- /dev/null +++ b/tests/test_apps/cliapp/multiapp.py @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000..71a2f90 --- /dev/null +++ b/tests/test_apps/helloworld/hello.py @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..ab2d6e9 --- /dev/null +++ b/tests/test_apps/helloworld/wsgi.py @@ -0,0 +1 @@ +from hello import app # noqa: F401 diff --git a/tests/test_apps/subdomaintestmodule/__init__.py b/tests/test_apps/subdomaintestmodule/__init__.py new file mode 100644 index 0000000..b4ce4b1 --- /dev/null +++ b/tests/test_apps/subdomaintestmodule/__init__.py @@ -0,0 +1,3 @@ +from flask import Module + +mod = Module(__name__, "foo", subdomain="foo") diff --git a/tests/test_apps/subdomaintestmodule/static/hello.txt b/tests/test_apps/subdomaintestmodule/static/hello.txt new file mode 100644 index 0000000..12e23c1 --- /dev/null +++ b/tests/test_apps/subdomaintestmodule/static/hello.txt @@ -0,0 +1 @@ +Hello Subdomain diff --git a/tests/test_async.py b/tests/test_async.py new file mode 100644 index 0000000..f52b049 --- /dev/null +++ b/tests/test_async.py @@ -0,0 +1,145 @@ +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 new file mode 100644 index 0000000..214cfee --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,1890 @@ +import gc +import re +import uuid +import warnings +import weakref +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 + +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(): + app = flask.Flask(__name__) + + def index(): + return "Hello World!" + + index.provide_automatic_options = False + app.route("/")(index) + rv = app.test_client().open("/", method="OPTIONS") + assert 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") + assert sorted(rv.allow) == ["OPTIONS"] + + +def test_provide_automatic_options_kwarg(app, client): + def index(): + return flask.request.method + + def more(): + return flask.request.method + + app.add_url_rule("/", view_func=index, provide_automatic_options=False) + app.add_url_rule( + "/more", + view_func=more, + methods=["GET", "POST"], + provide_automatic_options=False, + ) + assert client.get("/").data == b"GET" + + rv = client.post("/") + assert rv.status_code == 405 + assert sorted(rv.allow) == ["GET", "HEAD"] + + rv = client.open("/", method="OPTIONS") + assert rv.status_code == 405 + + 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", "POST"] + + rv = client.open("/more", method="OPTIONS") + assert rv.status_code == 405 + + +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(app, client): + @app.route("/set", methods=["POST"]) + def set(): + assert not flask.session.accessed + assert not flask.session.modified + flask.session["value"] = flask.request.form["value"] + assert flask.session.accessed + assert flask.session.modified + return "value set" + + @app.route("/get") + def get(): + assert not flask.session.accessed + assert not flask.session.modified + v = flask.session.get("value", "None") + assert flask.session.accessed + assert not flask.session.modified + return v + + assert client.post("/set", data={"value": "42"}).data == b"value set" + assert client.get("/get").data == b"42" + + +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_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 + + 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 + + +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_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): + rv = client.get("/static/index.html") + 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" + rv.close() + + +def test_static_url_path(): + app = flask.Flask(__name__, static_url_path="/foo") + app.testing = True + rv = app.test_client().get("/foo/index.html") + assert rv.status_code == 200 + rv.close() + + 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 + rv = app.test_client().get("/foo/index.html") + assert rv.status_code == 200 + rv.close() + + 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="") + rv = app.test_client().open("/static/index.html", method="GET") + assert rv.status_code == 200 + rv.close() + + +def test_static_url_empty_path_default(app): + app = flask.Flask(__name__, static_folder="") + rv = app.test_client().open("/static/index.html", method="GET") + assert rv.status_code == 200 + rv.close() + + +def test_static_folder_with_pathlib_path(app): + from pathlib import Path + + app = flask.Flask(__name__, static_folder=Path("static")) + rv = app.test_client().open("/static/index.html", method="GET") + assert rv.status_code == 200 + rv.close() + + +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() + rv = c.get("http://example.com/static/index.html") + assert rv.status_code == 200 + rv.close() + 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 + + +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" + + # suppress Werkzeug 0.15 warning about name mismatch + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", "Current server name", UserWarning, "flask.app" + ) + rv = client.get("/", "http://foo.localhost") + 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_max_content_length(app, client): + app.config["MAX_CONTENT_LENGTH"] = 64 + + @app.before_request + def always_first(): + flask.request.form["myfile"] + AssertionError() + + @app.route("/accept", methods=["POST"]) + def accept_file(): + flask.request.form["myfile"] + AssertionError() + + @app.errorhandler(413) + def catcher(error): + return "42" + + rv = client.post("/accept", data={"myfile": "foo" * 100}) + assert rv.data == b"42" + + +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 + + # suppress Werkzeug 0.15 warning about name mismatch + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", "Current server name", UserWarning, "flask.app" + ) + # ip address can't match name + rv = client.get("/", "http://127.0.0.1:3000/") + 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 new file mode 100644 index 0000000..69bc71a --- /dev/null +++ b/tests/test_blueprints.py @@ -0,0 +1,1054 @@ +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" + rv = client.get("/admin/static/test.txt") + assert rv.data.strip() == b"Admin File" + rv.close() + rv = client.get("/admin/static/css/test.css") + assert rv.data.strip() == b"/* nested file */" + rv.close() + + # 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 + rv = client.get("/admin/static/css/test.css") + cc = parse_cache_control_header(rv.headers["Cache-Control"]) + assert cc.max_age == expected_max_age + rv.close() + 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): + class MyBlueprint(flask.Blueprint): + def get_send_file_max_age(self, filename): + return 100 + + blueprint = MyBlueprint("blueprint", __name__, static_folder="static") + app.register_blueprint(blueprint) + + # try/finally, in case other tests use this app for Blueprint tests. + max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"] + try: + with app.test_request_context(): + unexpected_max_age = 3600 + if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == unexpected_max_age: + unexpected_max_age = 7200 + app.config["SEND_FILE_MAX_AGE_DEFAULT"] = unexpected_max_age + rv = blueprint.send_static_file("index.html") + cc = parse_cache_control_header(rv.headers["Cache-Control"]) + assert cc.max_age == 100 + rv.close() + finally: + app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default + + +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] + + 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_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) + + 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_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 + + # 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 + + with app.app_context(): + rv = flask.render_template_string("{{ get_answer() }}") + assert rv == "42" + + +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: + subdomain = "api" + 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=subdomain) + + client.allow_subdomain_redirects = True + + domain_name = "domain.tld" + app.config["SERVER_NAME"] = domain_name + response = client.get("/child/", base_url="http://api." + domain_name) + + assert response.status_code == 200 + + +def test_child_and_parent_subdomain(app, client) -> None: + child_subdomain = "api" + parent_subdomain = "parent" + parent = flask.Blueprint("parent", __name__) + child = flask.Blueprint("child", __name__, subdomain=child_subdomain) + + @child.route("/") + def index(): + return "child" + + parent.register_blueprint(child) + app.register_blueprint(parent, subdomain=parent_subdomain) + + client.allow_subdomain_redirects = True + + domain_name = "domain.tld" + app.config["SERVER_NAME"] = domain_name + response = client.get( + "/", base_url=f"http://{child_subdomain}.{parent_subdomain}.{domain_name}" + ) + + assert response.status_code == 200 + + response = client.get("/", base_url=f"http://{parent_subdomain}.{domain_name}") + + 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 new file mode 100644 index 0000000..0999548 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,686 @@ +# 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 + + runner = CliRunner(mix_stderr=False) + 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 + + runner = CliRunner(mix_stderr=False) + 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) + runner = CliRunner(mix_stderr=False) + 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:]): + # 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") + + +@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 new file mode 100644 index 0000000..e5b1906 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,250 @@ +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 new file mode 100644 index 0000000..d94a765 --- /dev/null +++ b/tests/test_converters.py @@ -0,0 +1,42 @@ +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 new file mode 100644 index 0000000..3566385 --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,349 @@ +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): + rv = flask.send_file("static/index.html") + assert rv.direct_passthrough + assert rv.mimetype == "text/html" + + with app.open_resource("static/index.html") as f: + rv.direct_passthrough = False + assert rv.data == f.read() + + rv.close() + + def test_static_file(self, app, req_ctx): + # Default max_age is None. + + # Test with static file handler. + rv = app.send_static_file("index.html") + assert rv.cache_control.max_age is None + rv.close() + + # Test with direct use of send_file. + rv = flask.send_file("static/index.html") + assert rv.cache_control.max_age is None + rv.close() + + app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 3600 + + # Test with static file handler. + rv = app.send_static_file("index.html") + assert rv.cache_control.max_age == 3600 + rv.close() + + # Test with direct use of send_file. + rv = flask.send_file("static/index.html") + assert rv.cache_control.max_age == 3600 + rv.close() + + # Test with pathlib.Path. + rv = app.send_static_file(FakePath("index.html")) + assert rv.cache_control.max_age == 3600 + rv.close() + + 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. + rv = app.send_static_file("index.html") + assert rv.cache_control.max_age == 10 + rv.close() + + # Test with direct use of send_file. + rv = flask.send_file("static/index.html") + assert rv.cache_control.max_age == 10 + rv.close() + + def test_send_from_directory(self, app, req_ctx): + app.root_path = os.path.join( + os.path.dirname(__file__), "test_apps", "subdomaintestmodule" + ) + rv = flask.send_from_directory("static", "hello.txt") + rv.direct_passthrough = False + assert rv.data.strip() == b"Hello Subdomain" + rv.close() + + +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" + + +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(self, 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(self, mode): + app = flask.Flask(__name__) + + with pytest.raises(ValueError): + app.open_resource("static/index.html", mode) diff --git a/tests/test_instance_config.py b/tests/test_instance_config.py new file mode 100644 index 0000000..1918bd9 --- /dev/null +++ b/tests/test_instance_config.py @@ -0,0 +1,111 @@ +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, limit_loader +): + (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( + limit_loader, 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( + limit_loader, 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 new file mode 100644 index 0000000..1e2b27d --- /dev/null +++ b/tests/test_json.py @@ -0,0 +1,346 @@ +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 new file mode 100644 index 0000000..677160a --- /dev/null +++ b/tests/test_json_tag.py @@ -0,0 +1,86 @@ +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 new file mode 100644 index 0000000..a5f0463 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,98 @@ +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 new file mode 100644 index 0000000..0ddcf97 --- /dev/null +++ b/tests/test_regression.py @@ -0,0 +1,30 @@ +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 new file mode 100644 index 0000000..6c38b66 --- /dev/null +++ b/tests/test_reqctx.py @@ -0,0 +1,325 @@ +import warnings + +import pytest + +import flask +from flask.globals import request_ctx +from flask.sessions import SecureCookieSessionInterface +from flask.sessions import SessionInterface + +try: + from greenlet import greenlet +except ImportError: + greenlet = None + + +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() + + +@pytest.mark.skipif(greenlet is None, reason="greenlet not installed") +class TestGreenletContextCopying: + def test_greenlet_context_copying(self, app, client): + greenlets = [] + + @app.route("/") + def index(): + flask.session["fizz"] = "buzz" + reqctx = request_ctx.copy() + + def g(): + assert not flask.request + assert not flask.current_app + with reqctx: + assert flask.request + assert flask.current_app == app + assert flask.request.path == "/" + assert flask.request.args["foo"] == "bar" + assert flask.session.get("fizz") == "buzz" + assert not flask.request + return 42 + + greenlets.append(greenlet(g)) + return "Hello World!" + + rv = client.get("/?foo=bar") + assert rv.data == b"Hello World!" + + result = greenlets[0].run() + assert result == 42 + + def test_greenlet_context_copying_api(self, app, client): + greenlets = [] + + @app.route("/") + def index(): + flask.session["fizz"] = "buzz" + + @flask.copy_current_request_context + def g(): + assert flask.request + assert flask.current_app == app + assert flask.request.path == "/" + assert flask.request.args["foo"] == "bar" + assert flask.session.get("fizz") == "buzz" + return 42 + + greenlets.append(greenlet(g)) + return "Hello World!" + + rv = client.get("/?foo=bar") + assert rv.data == b"Hello World!" + + result = greenlets[0].run() + assert result == 42 + + +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" + + +def test_bad_environ_raises_bad_request(): + app = flask.Flask(__name__) + + from flask.testing import EnvironBuilder + + builder = EnvironBuilder(app) + environ = builder.get_environ() + + # use a non-printable character in the Host - this is key to this test + environ["HTTP_HOST"] = "\x8a" + + with app.request_context(environ): + response = app.full_dispatch_request() + assert response.status_code == 400 + + +def test_environ_for_valid_idna_completes(): + app = flask.Flask(__name__) + + @app.route("/") + def index(): + return "Hello World!" + + from flask.testing import EnvironBuilder + + builder = EnvironBuilder(app) + environ = builder.get_environ() + + # these characters are all IDNA-compatible + environ["HTTP_HOST"] = "ąśźäüжŠßя.com" + + with app.request_context(environ): + response = app.full_dispatch_request() + + assert response.status_code == 200 + + +def test_normal_environ_completes(): + app = flask.Flask(__name__) + + @app.route("/") + def index(): + return "Hello World!" + + response = app.test_client().get("/", headers={"host": "xn--on-0ia.com"}) + assert response.status_code == 200 diff --git a/tests/test_session_interface.py b/tests/test_session_interface.py new file mode 100644 index 0000000..613da37 --- /dev/null +++ b/tests/test_session_interface.py @@ -0,0 +1,28 @@ +import flask +from flask.globals import request_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): + request_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 new file mode 100644 index 0000000..32ab333 --- /dev/null +++ b/tests/test_signals.py @@ -0,0 +1,181 @@ +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 new file mode 100644 index 0000000..087c50d --- /dev/null +++ b/tests/test_subclassing.py @@ -0,0 +1,21 @@ +from io import StringIO + +import flask + + +def test_suppressed_exception_logging(): + class SuppressedFlask(flask.Flask): + def log_exception(self, 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 new file mode 100644 index 0000000..c9fb375 --- /dev/null +++ b/tests/test_templating.py @@ -0,0 +1,451 @@ +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" + + +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) + + +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" + + +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 new file mode 100644 index 0000000..de05215 --- /dev/null +++ b/tests/test_testing.py @@ -0,0 +1,396 @@ +import importlib.metadata + +import click +import pytest + +import flask +from flask import appcontext_popped +from flask.cli import ScriptInfo +from flask.globals import _cv_request +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") + request.addfinalizer(builder.close) + + 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_keep_session(app, client, app_ctx): + @app.route("/", methods=["GET", "POST"]) + def index(): + if flask.request.method == "POST": + return flask.redirect("/getsession") + flask.session["data"] = "foo" + return "index" + + @app.route("/getsession") + def get_session(): + return flask.session.get("data", "") + + with client: + rv = client.get("/getsession") + assert rv.data == b"" + + rv = client.get("/") + assert rv.data == b"index" + assert flask.session.get("data") == "foo" + + rv = client.post("/", data={}, follow_redirects=True) + assert rv.data == b"foo" + assert flask.session.get("data") == "foo" + + rv = client.get("/getsession") + assert rv.data == b"foo" + + +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_request.get(None) is req_ctx diff --git a/tests/test_user_error_handler.py b/tests/test_user_error_handler.py new file mode 100644 index 0000000..79c5a73 --- /dev/null +++ b/tests/test_user_error_handler.py @@ -0,0 +1,295 @@ +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 new file mode 100644 index 0000000..eab5eda --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,260 @@ +import pytest +from werkzeug.http import parse_set_header + +import flask.views + + +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(): + app = flask.Flask(__name__) + + class Index1(flask.views.View): + provide_automatic_options = False + + def dispatch_request(self): + return "Hello World!" + + app.add_url_rule("/", view_func=Index1.as_view("index")) + c = app.test_client() + rv = c.open("/", method="OPTIONS") + assert rv.status_code == 405 + + app = flask.Flask(__name__) + + class Index2(flask.views.View): + methods = ["OPTIONS"] + provide_automatic_options = True + + def dispatch_request(self): + return "Hello World!" + + app.add_url_rule("/", view_func=Index2.as_view("index")) + c = app.test_client() + rv = c.open("/", method="OPTIONS") + assert sorted(rv.allow) == ["OPTIONS"] + + app = flask.Flask(__name__) + + class Index3(flask.views.View): + def dispatch_request(self): + return "Hello World!" + + app.add_url_rule("/", view_func=Index3.as_view("index")) + c = app.test_client() + rv = c.open("/", method="OPTIONS") + assert "OPTIONS" in rv.allow + + +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("/", 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/typing/typing_app_decorators.py b/tests/typing/typing_app_decorators.py new file mode 100644 index 0000000..0e25a30 --- /dev/null +++ b/tests/typing/typing_app_decorators.py @@ -0,0 +1,32 @@ +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/typing/typing_error_handler.py b/tests/typing/typing_error_handler.py new file mode 100644 index 0000000..ec9c886 --- /dev/null +++ b/tests/typing/typing_error_handler.py @@ -0,0 +1,33 @@ +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/typing/typing_route.py b/tests/typing/typing_route.py new file mode 100644 index 0000000..8bc271b --- /dev/null +++ b/tests/typing/typing_route.py @@ -0,0 +1,112 @@ +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 0000000..60ebdb0 --- /dev/null +++ b/tox.ini @@ -0,0 +1,49 @@ +[tox] +envlist = + py3{13,12,11,10,9,8} + pypy310 + py312-min + py38-dev + style + typing + docs +skip_missing_interpreters = true + +[testenv] +package = wheel +wheel_build_env = .pkg +envtmpdir = {toxworkdir}/tmp/{envname} +constrain_package_deps = true +use_frozen_constraints = true +deps = + -r requirements/tests.txt + min: -r requirements-skip/tests-min.txt + dev: -r requirements-skip/tests-dev.txt +commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs} + +[testenv:style] +deps = pre-commit +skip_install = true +commands = pre-commit run --all-files + +[testenv:typing] +deps = -r requirements/typing.txt +commands = mypy + +[testenv:docs] +deps = -r requirements/docs.txt +commands = sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml + +[testenv:update-requirements] +deps = + pip-tools + pre-commit +skip_install = true +change_dir = requirements +commands = + pre-commit autoupdate -j4 + pip-compile -U build.in + pip-compile -U docs.in + pip-compile -U tests.in + pip-compile -U typing.in + pip-compile -U dev.in