Developer's Flask Project Guide
Developer's Flask Project Guide
flask-without_docs.d ......................................................................................
.gitignore.............................................................................................5
.readthedocs.yaml .................................................................................... 5
.editorconfig.........................................................................................5
.pre-commit-config.yaml.............................................................................5
.devcontainer............................................................................................
on-create-command.sh..............................................................................6
devcontainer.json.................................................................................6
README.md..............................................................................................6
tox.ini ................................................................................................ 7
LICENSE.txt ........................................................................................... 8
pyproject.toml........................................................................................8
CODE_OF_CONDUCT.md..................................................................................10
.github...................................................................................................
pull_request_template.md........................................................................11
ISSUE_TEMPLATE ...............................................................................
feature-request.md............................................................................11
config.yml.....................................................................................12
bug-report.md..................................................................................12
workflows.............................................................................................
pre-commit.yaml ............................................................................... 12
lock.yaml ...................................................................................... 13
tests.yaml.....................................................................................13
publish.yaml...................................................................................14
CONTRIBUTING.rst .................................................................................... 15
requirements............................................................................................
build.in...........................................................................................18
dev.in.............................................................................................18
tests.in...........................................................................................19
docs.in............................................................................................19
typing.in ......................................................................................... 19
tests-min.in......................................................................................19
build.txt ......................................................................................... 19
tests.txt ......................................................................................... 19
tests-dev.txt.....................................................................................20
tests-min.txt.....................................................................................20
typing.txt ........................................................................................ 20
docs.txt...........................................................................................21
dev.txt............................................................................................22
examples ................................................................................................
celery.................................................................................................
make_celery.py ................................................................................ 25
pyproject.toml ................................................................................ 25
README.md ...................................................................................... 25
requirements.txt..............................................................................26
src .................................................................................................
task_app ......................................................................................
1
2
tasks.py.................................................................................26
views.py.................................................................................27
__init__.py..............................................................................27
templates...................................................................................
index.html............................................................................28
javascript.............................................................................................
.gitignore.....................................................................................30
pyproject.toml ................................................................................ 30
tests ...............................................................................................
conftest.py.................................................................................30
test_js_example.py ........................................................................ 31
README.rst.....................................................................................31
LICENSE.txt....................................................................................32
js_example .......................................................................................
__init__.py.................................................................................32
views.py .................................................................................... 32
templates ......................................................................................
jquery.html..............................................................................33
fetch.html...............................................................................33
xhr.html.................................................................................34
base.html................................................................................34
tutorial...............................................................................................
.gitignore.....................................................................................35
pyproject.toml ................................................................................ 35
README.rst.....................................................................................36
LICENSE.txt....................................................................................37
tests ...............................................................................................
test_factory.py............................................................................37
data.sql .................................................................................... 37
test_db.py..................................................................................38
conftest.py.................................................................................38
test_auth.py................................................................................39
test_blog.py................................................................................40
flaskr ..............................................................................................
schema.sql..................................................................................41
db.py........................................................................................42
__init__.py.................................................................................42
static...........................................................................................
style.css................................................................................43
auth.py......................................................................................45
blog.py......................................................................................47
templates ......................................................................................
base.html................................................................................49
auth.........................................................................................
login.html............................................................................49
register.html........................................................................49
blog .........................................................................................
create.html .......................................................................... 50
update.html .......................................................................... 50
index.html............................................................................50
CHANGES.rst .......................................................................................... 51
tests......................................................................................................
static .................................................................................................
index.html.....................................................................................73
config.toml....................................................................................73
config.json....................................................................................73
3
test_subclassing.py..............................................................................74
templates.............................................................................................
nested .............................................................................................
nested.txt..................................................................................74
mail.txt........................................................................................74
simple_template.html ......................................................................... 74
template_filter.html ......................................................................... 74
context_template.html........................................................................74
template_test.html............................................................................74
_macro.html....................................................................................74
escaping_template.html.......................................................................74
non_escaping_template.txt ................................................................... 75
test_regression.py...............................................................................75
test_session_interface.py ...................................................................... 75
test_converters.py...............................................................................76
test_apps ............................................................................................
.flaskenv ...................................................................................... 76
.env ............................................................................................ 77
subdomaintestmodule............................................................................
static...........................................................................................
hello.txt................................................................................77
__init__.py.................................................................................77
helloworld.........................................................................................
wsgi.py......................................................................................77
hello.py .................................................................................... 77
cliapp..............................................................................................
__init__.py.................................................................................77
message.txt.................................................................................77
app.py.......................................................................................77
multiapp.py.................................................................................77
importerrorapp.py..........................................................................78
inner1..........................................................................................
inner2.......................................................................................
__init__.py .......................................................................... 78
flask.py..............................................................................78
__init__.py..............................................................................78
factory.py..................................................................................78
blueprintapp......................................................................................
__init__.py.................................................................................78
apps............................................................................................
__init__.py..............................................................................78
frontend ....................................................................................
templates................................................................................
frontend..............................................................................
index.html ..................................................................... 78
__init__.py .......................................................................... 79
admin.......................................................................................
templates................................................................................
admin.................................................................................
index.html ..................................................................... 79
static.....................................................................................
test.txt...........................................................................79
css.....................................................................................
test.css........................................................................79
__init__.py .......................................................................... 79
test_request.py .................................................................................. 79
4
test_json_tag.py.................................................................................80
test_logging.py .................................................................................. 82
test_async.py.....................................................................................83
test_instance_config.py.........................................................................85
type_check ..........................................................................................
typing_app_decorators.py .................................................................... 87
typing_error_handler.py......................................................................87
typing_route.py ............................................................................... 88
conftest.py.......................................................................................90
test_signals.py .................................................................................. 92
test_appctx.py....................................................................................94
test_views.py.....................................................................................97
test_config.py .................................................................................. 101
test_user_error_handler.py....................................................................105
test_reqctx.py .................................................................................. 109
test_json.py.....................................................................................114
test_helpers.py ................................................................................. 119
test_testing.py ................................................................................. 124
test_templating.py..............................................................................129
test_cli.py......................................................................................136
test_blueprints.py..............................................................................146
test_basic.py....................................................................................160
src........................................................................................................
flask...................................................................................................
py.typed ...................................................................................... 188
__main__.py...................................................................................188
signals.py....................................................................................188
globals.py....................................................................................188
logging.py....................................................................................189
__init__.py...................................................................................190
typing.py ..................................................................................... 191
blueprints.py ................................................................................ 192
debughelpers.py..............................................................................194
views.py ...................................................................................... 197
templating.py ................................................................................ 200
wrappers.py...................................................................................203
testing.py....................................................................................206
config.py ..................................................................................... 211
ctx.py.........................................................................................216
sessions.py...................................................................................222
json................................................................................................
__init__.py................................................................................228
provider.py................................................................................230
tag.py......................................................................................233
helpers.py....................................................................................238
cli.py.........................................................................................247
app.py.........................................................................................263
sansio..............................................................................................
README.md..................................................................................285
blueprints.py ............................................................................. 285
scaffold.py................................................................................294
app.py......................................................................................305
5
.gitignore
1 .idea/
2 .vscode/
3 .venv*/
4 venv*/
5 __pycache__/
6 dist/
7 .coverage*
8 htmlcov/
9 .tox/
10 docs/_build/
.readthedocs.yaml
1 version: 2
2 build:
3 os: ubuntu-22.04
4 tools:
5 python: '3.12'
6 python:
7 install:
8 - requirements: requirements/docs.txt
9 - method: pip
10 path: .
11 sphinx:
12 builder: dirhtml
13 fail_on_warning: true
.editorconfig
1 root = true
2
3 [*]
4 indent_style = space
5 indent_size = 4
6 insert_final_newline = true
7 trim_trailing_whitespace = true
8 end_of_line = lf
9 charset = utf-8
10 max_line_length = 88
11
12 [*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}]
13 indent_size = 2
.pre-commit-config.yaml
1 repos:
2 - repo: https://github.com/astral-sh/ruff-pre-commit
3 rev: v0.7.3
4 hooks:
5 - id: ruff
6 - id: ruff-format
7 - repo: https://github.com/pre-commit/pre-commit-hooks
8 rev: v5.0.0
9 hooks:
10 - id: check-merge-conflict
11 - id: debug-statements
12 - id: fix-byte-order-marker
13 - id: trailing-whitespace
14 - id: end-of-file-fixer
6
.devcontainer/on-create-command.sh
1 #!/bin/bash
2 set -e
3 python3 -m venv --upgrade-deps .venv
4 . .venv/bin/activate
5 pip install -r requirements/dev.txt
6 pip install -e .
7 pre-commit install --install-hooks
.devcontainer/devcontainer.json
1 {
2 "name": "pallets/flask",
3 "image": "mcr.microsoft.com/devcontainers/python:3",
4 "customizations": {
5 "vscode": {
6 "settings": {
7 "python.defaultInterpreterPath": "${workspaceFolder}/.venv",
8 "python.terminal.activateEnvInCurrentTerminal": true,
9 "python.terminal.launchArgs": [
10 "-X",
11 "dev"
12 ]
13 }
14 }
15 },
16 "onCreateCommand": ".devcontainer/on-create-command.sh"
17 }
README.md
1 # Flask
2
3 Flask is a lightweight [WSGI][] web application framework. It is designed
4 to make getting started quick and easy, with the ability to scale up to
5 complex applications. It began as a simple wrapper around [Werkzeug][]
6 and [Jinja][], and has become one of the most popular Python web
7 application frameworks.
8
9 Flask offers suggestions, but doesn't enforce any dependencies or
10 project layout. It is up to the developer to choose the tools and
11 libraries they want to use. There are many extensions provided by the
12 community that make adding new functionality easy.
13
14 [WSGI]: https://wsgi.readthedocs.io/
15 [Werkzeug]: https://werkzeug.palletsprojects.com/
16 [Jinja]: https://jinja.palletsprojects.com/
17
18
19 ## A Simple Example
20
21 ```python
22 # save this as app.py
23 from flask import Flask
24
25 app = Flask(__name__)
26
27 @app.route("/")
28 def hello():
29 return "Hello, World!"
30 ```
31
32 ```
33 $ flask run
34 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
35 ```
36
7
37
38 ## Donate
39
40 The Pallets organization develops and supports Flask and the libraries
41 it uses. In order to grow the community of contributors and users, and
42 allow the maintainers to devote more time to the projects, [please
43 donate today][].
44
45 [please donate today]: https://palletsprojects.com/donate
tox.ini
1 [tox]
2 envlist =
3 py3{13,12,11,10,9}
4 pypy310
5 py313-min
6 py39-dev
7 style
8 typing
9 docs
10 skip_missing_interpreters = true
11
12 [testenv]
13 package = wheel
14 wheel_build_env = .pkg
15 envtmpdir = {toxworkdir}/tmp/{envname}
16 constrain_package_deps = true
17 use_frozen_constraints = true
18 deps =
19 -r requirements/tests.txt
20 min: -r requirements/tests-min.txt
21 dev: -r requirements/tests-dev.txt
22 commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs}
23
24 [testenv:style]
25 deps = pre-commit
26 skip_install = true
27 commands = pre-commit run --all-files
28
29 [testenv:typing]
30 deps = -r requirements/typing.txt
31 commands =
32 mypy
33 pyright
34
35 [testenv:docs]
36 deps = -r requirements/docs.txt
37 commands = sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml
38
39 [testenv:update-actions]
40 labels = update
41 deps = gha-update
42 skip_install = true
43 commands = gha-update
44
45 [testenv:update-pre_commit]
46 labels = update
47 deps = pre-commit
48 skip_install = true
49 commands = pre-commit autoupdate -j4
50
51 [testenv:update-requirements]
52 labels = update
53 deps = pip-tools
54 skip_install = true
55 change_dir = requirements
56 commands =
57 pip-compile build.in -q {posargs:-U}
58 pip-compile docs.in -q {posargs:-U}
8
LICENSE.txt
1 Copyright 2010 Pallets
2
3 Redistribution and use in source and binary forms, with or without
4 modification, are permitted provided that the following conditions are
5 met:
6
7 1. Redistributions of source code must retain the above copyright
8 notice, this list of conditions and the following disclaimer.
9
10 2. Redistributions in binary form must reproduce the above copyright
11 notice, this list of conditions and the following disclaimer in the
12 documentation and/or other materials provided with the distribution.
13
14 3. Neither the name of the copyright holder nor the names of its
15 contributors may be used to endorse or promote products derived from
16 this software without specific prior written permission.
17
18 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
21 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
24 TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
pyproject.toml
1 [project]
2 name = "Flask"
3 version = "3.1.0"
4 description = "A simple framework for building complex web applications."
5 readme = "README.md"
6 license = {file = "LICENSE.txt"}
7 maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}]
8 classifiers = [
9 "Development Status :: 5 - Production/Stable",
10 "Environment :: Web Environment",
11 "Framework :: Flask",
12 "Intended Audience :: Developers",
13 "License :: OSI Approved :: BSD License",
14 "Operating System :: OS Independent",
15 "Programming Language :: Python",
16 "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
17 "Topic :: Internet :: WWW/HTTP :: WSGI",
18 "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
19 "Topic :: Software Development :: Libraries :: Application Frameworks",
20 "Typing :: Typed",
21 ]
22 requires-python = ">=3.9"
23 dependencies = [
24 "Werkzeug>=3.1",
25 "Jinja2>=3.1.2",
26 "itsdangerous>=2.2",
27 "click>=8.1.3",
28 "blinker>=1.9",
29 "importlib-metadata>=3.6; python_version < '3.10'",
30 ]
pyproject.toml 9
31
32 [project.urls]
33 Donate = "https://palletsprojects.com/donate"
34 Documentation = "https://flask.palletsprojects.com/"
35 Changes = "https://flask.palletsprojects.com/changes/"
36 Source = "https://github.com/pallets/flask/"
37 Chat = "https://discord.gg/pallets"
38
39 [project.optional-dependencies]
40 async = ["asgiref>=3.2"]
41 dotenv = ["python-dotenv"]
42
43 [project.scripts]
44 flask = "flask.cli:main"
45
46 [build-system]
47 requires = ["flit_core<4"]
48 build-backend = "flit_core.buildapi"
49
50 [tool.flit.module]
51 name = "flask"
52
53 [tool.flit.sdist]
54 include = [
55 "docs/",
56 "examples/",
57 "requirements/",
58 "tests/",
59 "CHANGES.rst",
60 "CONTRIBUTING.rst",
61 "tox.ini",
62 ]
63 exclude = [
64 "docs/_build/",
65 ]
66
67 [tool.pytest.ini_options]
68 testpaths = ["tests"]
69 filterwarnings = [
70 "error",
71 ]
72
73 [tool.coverage.run]
74 branch = true
75 source = ["flask", "tests"]
76
77 [tool.coverage.paths]
78 source = ["src", "*/site-packages"]
79
80 [tool.mypy]
81 python_version = "3.9"
82 files = ["src/flask", "tests/type_check"]
83 show_error_codes = true
84 pretty = true
85 strict = true
86
87 [[tool.mypy.overrides]]
88 module = [
89 "asgiref.*",
90 "dotenv.*",
91 "cryptography.*",
92 "importlib_metadata",
93 ]
94 ignore_missing_imports = true
95
96 [tool.pyright]
97 pythonVersion = "3.9"
98 include = ["src/flask", "tests/type_check"]
99 typeCheckingMode = "basic"
100
101 [tool.ruff]
10
CODE_OF_CONDUCT.md
1 # Contributor Covenant Code of Conduct
2
3 ## Our Pledge
4
5 In the interest of fostering an open and welcoming environment, we as
6 contributors and maintainers pledge to making participation in our project and
7 our community a harassment-free experience for everyone, regardless of age, body
8 size, disability, ethnicity, sex characteristics, gender identity and expression,
9 level of experience, education, socio-economic status, nationality, personal
10 appearance, race, religion, or sexual identity and orientation.
11
12 ## Our Standards
13
14 Examples of behavior that contributes to creating a positive environment
15 include:
16
17 * Using welcoming and inclusive language
18 * Being respectful of differing viewpoints and experiences
19 * Gracefully accepting constructive criticism
20 * Focusing on what is best for the community
21 * Showing empathy towards other community members
22
23 Examples of unacceptable behavior by participants include:
24
25 * The use of sexualized language or imagery and unwelcome sexual attention or
26 advances
27 * Trolling, insulting/derogatory comments, and personal or political attacks
28 * Public or private harassment
29 * Publishing others' private information, such as a physical or electronic
30 address, without explicit permission
31 * Other conduct which could reasonably be considered inappropriate in a
32 professional setting
33
34 ## Our Responsibilities
35
36 Project maintainers are responsible for clarifying the standards of acceptable
37 behavior and are expected to take appropriate and fair corrective action in
38 response to any instances of unacceptable behavior.
39
40 Project maintainers have the right and responsibility to remove, edit, or
41 reject comments, commits, code, wiki edits, issues, and other contributions
42 that are not aligned to this Code of Conduct, or to ban temporarily or
43 permanently any contributor for other behaviors that they deem inappropriate,
44 threatening, offensive, or harmful.
11
45
46 ## Scope
47
48 This Code of Conduct applies both within project spaces and in public spaces
49 when an individual is representing the project or its community. Examples of
50 representing a project or community include using an official project e-mail
51 address, posting via an official social media account, or acting as an appointed
52 representative at an online or offline event. Representation of a project may be
53 further defined and clarified by project maintainers.
54
55 ## Enforcement
56
57 Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 reported by contacting the project team at report@palletsprojects.com. All
59 complaints will be reviewed and investigated and will result in a response that
60 is deemed necessary and appropriate to the circumstances. The project team is
61 obligated to maintain confidentiality with regard to the reporter of an incident.
62 Further details of specific enforcement policies may be posted separately.
63
64 Project maintainers who do not follow or enforce the Code of Conduct in good
65 faith may face temporary or permanent repercussions as determined by other
66 members of the project's leadership.
67
68 ## Attribution
69
70 This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72
73 [homepage]: https://www.contributor-covenant.org
74
75 For answers to common questions about this code of conduct, see
76 https://www.contributor-covenant.org/faq
.github/pull_request_template.md
1 <!--
2 Before opening a PR, open a ticket describing the issue or feature the
3 PR will address. An issue is not required for fixing typos in
4 documentation, or other simple non-code changes.
5
6 Replace this comment with a description of the change. Describe how it
7 addresses the linked ticket.
8 -->
9
10 <!--
11 Link to relevant issues or previous PRs, one per line. Use "fixes" to
12 automatically close an issue.
13
14 fixes #<issue number>
15 -->
16
17 <!--
18 Ensure each step in CONTRIBUTING.rst is complete, especially the following:
19
20 - Add tests that demonstrate the correct behavior of the change. Tests
21 should fail without the change.
22 - Add or update relevant docs, in the docs folder and in code.
23 - Add an entry in CHANGES.rst summarizing the change and linking to the issue.
24 - Add `.. versionchanged::` entries in any relevant code docs.
25 -->
.github/ISSUE_TEMPLATE/feature-request.md
1 ---
2 name: Feature request
3 about: Suggest a new feature for Flask
4 ---
5
12
6 <!--
7 Replace this comment with a description of what the feature should do.
8 Include details such as links to relevant specs or previous discussions.
9 -->
10
11 <!--
12 Replace this comment with an example of the problem which this feature
13 would resolve. Is this problem solvable without changes to Flask, such
14 as by subclassing or using an extension?
15 -->
.github/ISSUE_TEMPLATE/config.yml
1 blank_issues_enabled: false
2 contact_links:
3 - name: Security issue
4 url: https://github.com/pallets/flask/security/advisories/new
5 about: Do not report security issues publicly. Create a private advisory.
6 - name: Questions
7 url: https://github.com/pallets/flask/discussions/
8 about: Ask questions about your own code on the Discussions tab.
9 - name: Questions on
10 url: https://discord.gg/pallets
11 about: Ask questions about your own code on our Discord chat.
.github/ISSUE_TEMPLATE/bug-report.md
1 ---
2 name: Bug report
3 about: Report a bug in Flask (not other projects which depend on Flask)
4 ---
5
6 <!--
7 This issue tracker is a tool to address bugs in Flask itself. Please use
8 GitHub Discussions or the Pallets Discord for questions about your own code.
9
10 Replace this comment with a clear outline of what the bug is.
11 -->
12
13 <!--
14 Describe how to replicate the bug.
15
16 Include a minimal reproducible example that demonstrates the bug.
17 Include the full traceback if there was an exception.
18 -->
19
20 <!--
21 Describe the expected behavior that should have happened but didn't.
22 -->
23
24 Environment:
25
26 - Python version:
27 - Flask version:
.github/workflows/pre-commit.yaml
1 name: pre-commit
2 on:
3 pull_request:
4 push:
5 branches: [main, stable]
6 jobs:
7 main:
8 runs-on: ubuntu-latest
9 steps:
13
.github/workflows/lock.yaml
1 name: Lock inactive closed issues
2 # Lock closed issues that have not received any further activity for two weeks.
3 # This does not close open issues, only humans may do that. It is easier to
4 # respond to new issues with fresh examples rather than continuing discussions
5 # on old issues.
6
7 on:
8 schedule:
9 - cron: '0 0 * * *'
10 permissions:
11 issues: write
12 pull-requests: write
13 concurrency:
14 group: lock
15 jobs:
16 lock:
17 runs-on: ubuntu-latest
18 steps:
19 - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
20 with:
21 issue-inactive-days: 14
22 pr-inactive-days: 14
23 discussion-inactive-days: 14
.github/workflows/tests.yaml
1 name: Tests
2 on:
3 push:
4 branches: [main, stable]
5 paths-ignore: ['docs/**', '*.md', '*.rst']
6 pull_request:
7 paths-ignore: [ 'docs/**', '*.md', '*.rst' ]
8 jobs:
9 tests:
10 name: ${{ matrix.name || matrix.python }}
11 runs-on: ${{ matrix.os || 'ubuntu-latest' }}
12 strategy:
13 fail-fast: false
14 matrix:
15 include:
16 - {python: '3.13'}
17 - {python: '3.12'}
18 - {name: Windows, python: '3.12', os: windows-latest}
19 - {name: Mac, python: '3.12', os: macos-latest}
20 - {python: '3.11'}
21 - {python: '3.10'}
22 - {python: '3.9'}
23 - {name: PyPy, python: 'pypy-3.10', tox: pypy310}
24 - {name: Minimum Versions, python: '3.12', tox: py-min}
25 - {name: Development Versions, python: '3.9', tox: py-dev}
26 steps:
27 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
28 - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
29 with:
30 python-version: ${{ matrix.python }}
31 allow-prereleases: true
32 cache: pip
14
33 cache-dependency-path: requirements*/*.txt
34 - run: pip install tox
35 - run: tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }}
36 typing:
37 runs-on: ubuntu-latest
38 steps:
39 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
40 - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
41 with:
42 python-version: '3.x'
43 cache: pip
44 cache-dependency-path: requirements*/*.txt
45 - name: cache mypy
46 uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
47 with:
48 path: ./.mypy_cache
49 key: mypy|${{ hashFiles('pyproject.toml') }}
50 - run: pip install tox
51 - run: tox run -e typing
.github/workflows/publish.yaml
1 name: Publish
2 on:
3 push:
4 tags:
5 - '*'
6 jobs:
7 build:
8 runs-on: ubuntu-latest
9 outputs:
10 hash: ${{ steps.hash.outputs.hash }}
11 steps:
12 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
13 - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
14 with:
15 python-version: '3.x'
16 cache: pip
17 cache-dependency-path: requirements*/*.txt
18 - run: pip install -r requirements/build.txt
19 # Use the commit date instead of the current date during the build.
20 - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
21 - run: python -m build
22 # Generate hashes used for provenance.
23 - name: generate hash
24 id: hash
25 run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT
26 - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
27 with:
28 path: ./dist
29 provenance:
30 needs: [build]
31 permissions:
32 actions: read
33 id-token: write
34 contents: write
35 # Can't pin with hash due to how this workflow works.
36 uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
37 with:
38 base64-subjects: ${{ needs.build.outputs.hash }}
39 create-release:
40 # Upload the sdist, wheels, and provenance to a GitHub release. They remain
41 # available as build artifacts for a while as well.
42 needs: [provenance]
43 runs-on: ubuntu-latest
44 permissions:
45 contents: write
46 steps:
47 - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
48 - name: create release
15
49 run: >
50 gh release create --draft --repo ${{ github.repository }}
51 ${{ github.ref_name }}
52 *.intoto.jsonl/* artifact/*
53 env:
54 GH_TOKEN: ${{ github.token }}
55 publish-pypi:
56 needs: [provenance]
57 # Wait for approval before attempting to upload to PyPI. This allows reviewing the
58 # files in the draft release.
59 environment:
60 name: publish
61 url: https://pypi.org/project/Flask/${{ github.ref_name }}
62 runs-on: ubuntu-latest
63 permissions:
64 id-token: write
65 steps:
66 - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
67 - uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2
68 with:
69 packages-dir: artifact/
CONTRIBUTING.rst
1 How to contribute to Flask
2 ==========================
3
4 Thank you for considering contributing to Flask!
5
6
7 Support questions
8 -----------------
9
10 Please don't use the issue tracker for this. The issue tracker is a tool
11 to address bugs and feature requests in Flask itself. Use one of the
12 following resources for questions about using Flask or issues with your
13 own code:
14
15 - The ``#questions`` channel on our Discord chat:
16 https://discord.gg/pallets
17 - Ask on `Stack Overflow`_. Search with Google first using:
18 ``site:stackoverflow.com flask {search term, exception message, etc.}``
19 - Ask on our `GitHub Discussions`_ for long term discussion or larger
20 questions.
21
22 .. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent
23 .. _GitHub Discussions: https://github.com/pallets/flask/discussions
24
25
26 Reporting issues
27 ----------------
28
29 Include the following information in your post:
30
31 - Describe what you expected to happen.
32 - If possible, include a `minimal reproducible example`_ to help us
33 identify the issue. This also helps check that the issue is not with
34 your own code.
35 - Describe what actually happened. Include the full traceback if there
36 was an exception.
37 - List your Python and Flask versions. If possible, check if this
38 issue is already fixed in the latest releases or the latest code in
39 the repository.
40
41 .. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example
42
43
44 Submitting patches
45 ------------------
46
CONTRIBUTING.rst 16
47 If there is not an open issue for what you want to submit, prefer
48 opening one for discussion before working on a PR. You can work on any
49 issue that doesn't have an open PR linked to it or a maintainer assigned
50 to it. These show up in the sidebar. No need to ask if you can work on
51 an issue that interests you.
52
53 Include the following in your patch:
54
55 - Use `Black`_ to format your code. This and other tools will run
56 automatically if you install `pre-commit`_ using the instructions
57 below.
58 - Include tests if your patch adds or changes code. Make sure the test
59 fails without your patch.
60 - Update any relevant docs pages and docstrings. Docs pages and
61 docstrings should be wrapped at 72 characters.
62 - Add an entry in ``CHANGES.rst``. Use the same style as other
63 entries. Also include ``.. versionchanged::`` inline changelogs in
64 relevant docstrings.
65
66 .. _Black: https://black.readthedocs.io
67 .. _pre-commit: https://pre-commit.com
68
69
70 First time setup using GitHub Codespaces
71 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
72
73 `GitHub Codespaces`_ creates a development environment that is already set up for the
74 project. By default it opens in Visual Studio Code for the Web, but this can
75 be changed in your GitHub profile settings to use Visual Studio Code or JetBrains
76 PyCharm on your local computer.
77
78 - Make sure you have a `GitHub account`_.
79 - From the project's repository page, click the green "Code" button and then "Create
80 codespace on main".
81 - The codespace will be set up, then Visual Studio Code will open. However, you'll
82 need to wait a bit longer for the Python extension to be installed. You'll know it's
83 ready when the terminal at the bottom shows that the virtualenv was activated.
84 - Check out a branch and `start coding`_.
85
86 .. _GitHub Codespaces: https://docs.github.com/en/codespaces
87 .. _devcontainer:
,→ https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-t
88
89 First time setup in your local environment
90 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
91
92 - Make sure you have a `GitHub account`_.
93 - Download and install the `latest version of git`_.
94 - Configure git with your `username`_ and `email`_.
95
96 .. code-block:: text
97
98 $ git config --global user.name 'your name'
99 $ git config --global user.email 'your email'
100
101 - Fork Flask to your GitHub account by clicking the `Fork`_ button.
102 - `Clone`_ your fork locally, replacing ``your-username`` in the command below with
103 your actual username.
104
105 .. code-block:: text
106
107 $ git clone https://github.com/your-username/flask
108 $ cd flask
109
110 - Create a virtualenv. Use the latest version of Python.
111
112 - Linux/macOS
113
114 .. code-block:: text
115
116 $ python3 -m venv .venv
CONTRIBUTING.rst 17
117 $ . .venv/bin/activate
118
119 - Windows
120
121 .. code-block:: text
122
123 > py -3 -m venv .venv
124 > .venv\Scripts\activate
125
126 - Install the development dependencies, then install Flask in editable mode.
127
128 .. code-block:: text
129
130 $ python -m pip install -U pip
131 $ pip install -r requirements/dev.txt && pip install -e .
132
133 - Install the pre-commit hooks.
134
135 .. code-block:: text
136
137 $ pre-commit install --install-hooks
138
139 .. _GitHub account: https://github.com/join
140 .. _latest version of git: https://git-scm.com/downloads
141 .. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git
142 .. _email:
,→ https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address
143 .. _Fork: https://github.com/pallets/flask/fork
144 .. _Clone:
,→ https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork
145
146 .. _start coding:
147
148 Start coding
149 ~~~~~~~~~~~~
150
151 - Create a branch to identify the issue you would like to work on. If you're
152 submitting a bug or documentation fix, branch off of the latest ".x" branch.
153
154 .. code-block:: text
155
156 $ git fetch origin
157 $ git checkout -b your-branch-name origin/2.0.x
158
159 If you're submitting a feature addition or change, branch off of the "main" branch.
160
161 .. code-block:: text
162
163 $ git fetch origin
164 $ git checkout -b your-branch-name origin/main
165
166 - Using your favorite editor, make your changes, `committing as you go`_.
167
168 - If you are in a codespace, you will be prompted to `create a fork`_ the first
169 time you make a commit. Enter ``Y`` to continue.
170
171 - Include tests that cover any code changes you make. Make sure the test fails without
172 your patch. Run the tests as described below.
173 - Push your commits to your fork on GitHub and `create a pull request`_. Link to the
174 issue being addressed with ``fixes #123`` in the pull request description.
175
176 .. code-block:: text
177
178 $ git push --set-upstream origin your-branch-name
179
180 .. _committing as you go:
,→ https://afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes
181 .. _create a fork:
,→ https://docs.github.com/en/codespaces/developing-in-codespaces/using-source-control-in-your-codespace#about-automatic-forking
182 .. _create a pull request:
,→ https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
18
183
184 .. _Running the tests:
185
186 Running the tests
187 ~~~~~~~~~~~~~~~~~
188
189 Run the basic test suite with pytest.
190
191 .. code-block:: text
192
193 $ pytest
194
195 This runs the tests for the current environment, which is usually
196 sufficient. CI will run the full suite when you submit your pull
197 request. You can run the full test suite with tox if you don't want to
198 wait.
199
200 .. code-block:: text
201
202 $ tox
203
204
205 Running test coverage
206 ~~~~~~~~~~~~~~~~~~~~~
207
208 Generating a report of lines that do not have test coverage can indicate
209 where to start contributing. Run ``pytest`` using ``coverage`` and
210 generate a report.
211
212 If you are using GitHub Codespaces, ``coverage`` is already installed
213 so you can skip the installation command.
214
215 .. code-block:: text
216
217 $ pip install coverage
218 $ coverage run -m pytest
219 $ coverage html
220
221 Open ``htmlcov/index.html`` in your browser to explore the report.
222
223 Read more about `coverage <https://coverage.readthedocs.io>`__.
224
225
226 Building the docs
227 ~~~~~~~~~~~~~~~~~
228
229 Build the docs in the ``docs`` directory using Sphinx.
230
231 .. code-block:: text
232
233 $ cd docs
234 $ make html
235
236 Open ``_build/html/index.html`` in your browser to view the docs.
237
238 Read more about `Sphinx <https://www.sphinx-doc.org/en/stable/>`__.
requirements/build.in
1 build
requirements/dev.in
1 -r docs.txt
2 -r tests.txt
3 -r typing.txt
4 pre-commit
5 tox
19
requirements/tests.in
1 pytest
2 asgiref
3 greenlet ; python_version < "3.11"
4 python-dotenv
requirements/docs.in
1 pallets-sphinx-themes
2 sphinx
3 sphinxcontrib-log-cabinet
4 sphinx-tabs
requirements/typing.in
1 mypy
2 pyright
3 pytest
4 types-contextvars
5 types-dataclasses
6 asgiref
7 cryptography
8 python-dotenv
requirements/tests-min.in
1 werkzeug==3.1.0
2 jinja2==3.1.2
3 markupsafe==2.1.1
4 itsdangerous==2.2.0
5 click==8.1.3
6 blinker==1.9.0
requirements/build.txt
1 #
2 # This file is autogenerated by pip-compile with Python 3.13
3 # by the following command:
4 #
5 # pip-compile build.in
6 #
7 build==1.2.2.post1
8 # via -r build.in
9 packaging==24.2
10 # via build
11 pyproject-hooks==1.2.0
12 # via build
requirements/tests.txt
1 #
2 # This file is autogenerated by pip-compile with Python 3.13
3 # by the following command:
4 #
5 # pip-compile tests.in
6 #
7 asgiref==3.8.1
8 # via -r tests.in
9 iniconfig==2.0.0
10 # via pytest
11 packaging==24.2
20
12 # via pytest
13 pluggy==1.5.0
14 # via pytest
15 pytest==8.3.3
16 # via -r tests.in
17 python-dotenv==1.0.1
18 # via -r tests.in
requirements/tests-dev.txt
1 https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz
2 https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz
3 https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz
4 https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz
5 https://github.com/pallets/click/archive/refs/heads/main.tar.gz
6 https://github.com/pallets-eco/blinker/archive/refs/heads/main.tar.gz
requirements/tests-min.txt
1 #
2 # This file is autogenerated by pip-compile with Python 3.13
3 # by the following command:
4 #
5 # pip-compile tests-min.in
6 #
7 blinker==1.9.0
8 # via -r tests-min.in
9 click==8.1.3
10 # via -r tests-min.in
11 itsdangerous==2.2.0
12 # via -r tests-min.in
13 jinja2==3.1.2
14 # via -r tests-min.in
15 markupsafe==2.1.1
16 # via
17 # -r tests-min.in
18 # jinja2
19 # werkzeug
20 werkzeug==3.1.0
21 # via -r tests-min.in
requirements/typing.txt
1 #
2 # This file is autogenerated by pip-compile with Python 3.13
3 # by the following command:
4 #
5 # pip-compile typing.in
6 #
7 asgiref==3.8.1
8 # via -r typing.in
9 cffi==1.17.1
10 # via cryptography
11 cryptography==43.0.3
12 # via -r typing.in
13 iniconfig==2.0.0
14 # via pytest
15 mypy==1.13.0
16 # via -r typing.in
17 mypy-extensions==1.0.0
18 # via mypy
19 nodeenv==1.9.1
20 # via pyright
21 packaging==24.2
22 # via pytest
23 pluggy==1.5.0
21
24 # via pytest
25 pycparser==2.22
26 # via cffi
27 pyright==1.1.389
28 # via -r typing.in
29 pytest==8.3.3
30 # via -r typing.in
31 python-dotenv==1.0.1
32 # via -r typing.in
33 types-contextvars==2.4.7.3
34 # via -r typing.in
35 types-dataclasses==0.6.6
36 # via -r typing.in
37 typing-extensions==4.12.2
38 # via
39 # mypy
40 # pyright
requirements/docs.txt
1 #
2 # This file is autogenerated by pip-compile with Python 3.13
3 # by the following command:
4 #
5 # pip-compile docs.in
6 #
7 alabaster==1.0.0
8 # via sphinx
9 babel==2.16.0
10 # via sphinx
11 certifi==2024.8.30
12 # via requests
13 charset-normalizer==3.4.0
14 # via requests
15 docutils==0.21.2
16 # via
17 # sphinx
18 # sphinx-tabs
19 idna==3.10
20 # via requests
21 imagesize==1.4.1
22 # via sphinx
23 jinja2==3.1.4
24 # via sphinx
25 markupsafe==3.0.2
26 # via jinja2
27 packaging==24.2
28 # via
29 # pallets-sphinx-themes
30 # sphinx
31 pallets-sphinx-themes==2.3.0
32 # via -r docs.in
33 pygments==2.18.0
34 # via
35 # sphinx
36 # sphinx-tabs
37 requests==2.32.3
38 # via sphinx
39 snowballstemmer==2.2.0
40 # via sphinx
41 sphinx==8.1.3
42 # via
43 # -r docs.in
44 # pallets-sphinx-themes
45 # sphinx-notfound-page
46 # sphinx-tabs
47 # sphinxcontrib-log-cabinet
48 sphinx-notfound-page==1.0.4
49 # via pallets-sphinx-themes
50 sphinx-tabs==3.4.7
22
51 # via -r docs.in
52 sphinxcontrib-applehelp==2.0.0
53 # via sphinx
54 sphinxcontrib-devhelp==2.0.0
55 # via sphinx
56 sphinxcontrib-htmlhelp==2.1.0
57 # via sphinx
58 sphinxcontrib-jsmath==1.0.1
59 # via sphinx
60 sphinxcontrib-log-cabinet==1.0.1
61 # via -r docs.in
62 sphinxcontrib-qthelp==2.0.0
63 # via sphinx
64 sphinxcontrib-serializinghtml==2.0.0
65 # via sphinx
66 urllib3==2.2.3
67 # via requests
requirements/dev.txt
1 #
2 # This file is autogenerated by pip-compile with Python 3.13
3 # by the following command:
4 #
5 # pip-compile dev.in
6 #
7 alabaster==1.0.0
8 # via
9 # -r /Users/david/Projects/flask/requirements/docs.txt
10 # sphinx
11 asgiref==3.8.1
12 # via
13 # -r /Users/david/Projects/flask/requirements/tests.txt
14 # -r /Users/david/Projects/flask/requirements/typing.txt
15 babel==2.16.0
16 # via
17 # -r /Users/david/Projects/flask/requirements/docs.txt
18 # sphinx
19 cachetools==5.5.0
20 # via tox
21 certifi==2024.8.30
22 # via
23 # -r /Users/david/Projects/flask/requirements/docs.txt
24 # requests
25 cffi==1.17.1
26 # via
27 # -r /Users/david/Projects/flask/requirements/typing.txt
28 # cryptography
29 cfgv==3.4.0
30 # via pre-commit
31 chardet==5.2.0
32 # via tox
33 charset-normalizer==3.4.0
34 # via
35 # -r /Users/david/Projects/flask/requirements/docs.txt
36 # requests
37 colorama==0.4.6
38 # via tox
39 cryptography==43.0.3
40 # via -r /Users/david/Projects/flask/requirements/typing.txt
41 distlib==0.3.9
42 # via virtualenv
43 docutils==0.21.2
44 # via
45 # -r /Users/david/Projects/flask/requirements/docs.txt
46 # sphinx
47 # sphinx-tabs
48 filelock==3.16.1
49 # via
50 # tox
requirements/dev.txt 23
51 # virtualenv
52 identify==2.6.2
53 # via pre-commit
54 idna==3.10
55 # via
56 # -r /Users/david/Projects/flask/requirements/docs.txt
57 # requests
58 imagesize==1.4.1
59 # via
60 # -r /Users/david/Projects/flask/requirements/docs.txt
61 # sphinx
62 iniconfig==2.0.0
63 # via
64 # -r /Users/david/Projects/flask/requirements/tests.txt
65 # -r /Users/david/Projects/flask/requirements/typing.txt
66 # pytest
67 jinja2==3.1.4
68 # via
69 # -r /Users/david/Projects/flask/requirements/docs.txt
70 # sphinx
71 markupsafe==3.0.2
72 # via
73 # -r /Users/david/Projects/flask/requirements/docs.txt
74 # jinja2
75 mypy==1.13.0
76 # via -r /Users/david/Projects/flask/requirements/typing.txt
77 mypy-extensions==1.0.0
78 # via
79 # -r /Users/david/Projects/flask/requirements/typing.txt
80 # mypy
81 nodeenv==1.9.1
82 # via
83 # -r /Users/david/Projects/flask/requirements/typing.txt
84 # pre-commit
85 # pyright
86 packaging==24.2
87 # via
88 # -r /Users/david/Projects/flask/requirements/docs.txt
89 # -r /Users/david/Projects/flask/requirements/tests.txt
90 # -r /Users/david/Projects/flask/requirements/typing.txt
91 # pallets-sphinx-themes
92 # pyproject-api
93 # pytest
94 # sphinx
95 # tox
96 pallets-sphinx-themes==2.3.0
97 # via -r /Users/david/Projects/flask/requirements/docs.txt
98 platformdirs==4.3.6
99 # via
100 # tox
101 # virtualenv
102 pluggy==1.5.0
103 # via
104 # -r /Users/david/Projects/flask/requirements/tests.txt
105 # -r /Users/david/Projects/flask/requirements/typing.txt
106 # pytest
107 # tox
108 pre-commit==4.0.1
109 # via -r dev.in
110 pycparser==2.22
111 # via
112 # -r /Users/david/Projects/flask/requirements/typing.txt
113 # cffi
114 pygments==2.18.0
115 # via
116 # -r /Users/david/Projects/flask/requirements/docs.txt
117 # sphinx
118 # sphinx-tabs
119 pyproject-api==1.8.0
120 # via tox
121 pyright==1.1.389
requirements/dev.txt 24
193 # -r /Users/david/Projects/flask/requirements/docs.txt
194 # requests
195 virtualenv==20.27.1
196 # via
197 # pre-commit
198 # tox
examples/celery/make_celery.py
1 from task_app import create_app
2
3 flask_app = create_app()
4 celery_app = flask_app.extensions["celery"]
examples/celery/pyproject.toml
1 [project]
2 name = "flask-example-celery"
3 version = "1.0.0"
4 description = "Example Flask application with Celery background tasks."
5 readme = "README.md"
6 classifiers = ["Private :: Do Not Upload"]
7 dependencies = ["flask", "celery[redis]"]
8
9 [build-system]
10 requires = ["flit_core<4"]
11 build-backend = "flit_core.buildapi"
12
13 [tool.flit.module]
14 name = "task_app"
15
16 [tool.ruff]
17 src = ["src"]
examples/celery/README.md
1 Background Tasks with Celery
2 ============================
3
4 This example shows how to configure Celery with Flask, how to set up an API for
5 submitting tasks and polling results, and how to use that API with JavaScript. See
6 [Flask's documentation about Celery](https://flask.palletsprojects.com/patterns/celery/).
7
8 From this directory, create a virtualenv and install the application into it. Then run a
9 Celery worker.
10
11 ```shell
12 $ python3 -m venv .venv
13 $ . ./.venv/bin/activate
14 $ pip install -r requirements.txt && pip install -e .
15 $ celery -A make_celery worker --loglevel INFO
16 ```
17
18 In a separate terminal, activate the virtualenv and run the Flask development server.
19
20 ```shell
21 $ . ./.venv/bin/activate
22 $ flask -A task_app run --debug
23 ```
24
25 Go to http://localhost:5000/ and use the forms to submit tasks. You can see the polling
26 requests in the browser dev tools and the Flask logs. You can see the tasks submitting
27 and completing in the Celery logs.
26
examples/celery/requirements.txt
1 #
2 # This file is autogenerated by pip-compile with Python 3.11
3 # by the following command:
4 #
5 # pip-compile --resolver=backtracking pyproject.toml
6 #
7 amqp==5.1.1
8 # via kombu
9 async-timeout==4.0.2
10 # via redis
11 billiard==3.6.4.0
12 # via celery
13 blinker==1.6.2
14 # via flask
15 celery[redis]==5.2.7
16 # via flask-example-celery (pyproject.toml)
17 click==8.1.3
18 # via
19 # celery
20 # click-didyoumean
21 # click-plugins
22 # click-repl
23 # flask
24 click-didyoumean==0.3.0
25 # via celery
26 click-plugins==1.1.1
27 # via celery
28 click-repl==0.2.0
29 # via celery
30 flask==2.3.2
31 # via flask-example-celery (pyproject.toml)
32 itsdangerous==2.1.2
33 # via flask
34 jinja2==3.1.2
35 # via flask
36 kombu==5.2.4
37 # via celery
38 markupsafe==2.1.2
39 # via
40 # jinja2
41 # werkzeug
42 prompt-toolkit==3.0.38
43 # via click-repl
44 pytz==2023.3
45 # via celery
46 redis==4.5.4
47 # via celery
48 six==1.16.0
49 # via click-repl
50 vine==5.0.0
51 # via
52 # amqp
53 # celery
54 # kombu
55 wcwidth==0.2.6
56 # via prompt-toolkit
57 werkzeug==2.3.3
58 # via flask
examples/celery/src/task_app/tasks.py
1 import time
2
3 from celery import shared_task
4 from celery import Task
5
6
27
7 @shared_task(ignore_result=False)
8 def add(a: int, b: int) -> int:
9 return a + b
10
11
12 @shared_task()
13 def block() -> None:
14 time.sleep(5)
15
16
17 @shared_task(bind=True, ignore_result=False)
18 def process(self: Task, total: int) -> object:
19 for i in range(total):
20 self.update_state(state="PROGRESS", meta={"current": i + 1, "total": total})
21 time.sleep(1)
22
23 return {"current": total, "total": total}
examples/celery/src/task_app/views.py
1 from celery.result import AsyncResult
2 from flask import Blueprint
3 from flask import request
4
5 from . import tasks
6
7 bp = Blueprint("tasks", __name__, url_prefix="/tasks")
8
9
10 @bp.get("/result/<id>")
11 def result(id: str) -> dict[str, object]:
12 result = AsyncResult(id)
13 ready = result.ready()
14 return {
15 "ready": ready,
16 "successful": result.successful() if ready else None,
17 "value": result.get() if ready else result.result,
18 }
19
20
21 @bp.post("/add")
22 def add() -> dict[str, object]:
23 a = request.form.get("a", type=int)
24 b = request.form.get("b", type=int)
25 result = tasks.add.delay(a, b)
26 return {"result_id": result.id}
27
28
29 @bp.post("/block")
30 def block() -> dict[str, object]:
31 result = tasks.block.delay()
32 return {"result_id": result.id}
33
34
35 @bp.post("/process")
36 def process() -> dict[str, object]:
37 result = tasks.process.delay(total=request.form.get("total", type=int))
38 return {"result_id": result.id}
examples/celery/src/task_app/__init__.py
1 from celery import Celery
2 from celery import Task
3 from flask import Flask
4 from flask import render_template
5
6
7 def create_app() -> Flask:
28
8 app = Flask(__name__)
9 app.config.from_mapping(
10 CELERY=dict(
11 broker_url="redis://localhost",
12 result_backend="redis://localhost",
13 task_ignore_result=True,
14 ),
15 )
16 app.config.from_prefixed_env()
17 celery_init_app(app)
18
19 @app.route("/")
20 def index() -> str:
21 return render_template("index.html")
22
23 from . import views
24
25 app.register_blueprint(views.bp)
26 return app
27
28
29 def celery_init_app(app: Flask) -> Celery:
30 class FlaskTask(Task):
31 def __call__(self, *args: object, **kwargs: object) -> object:
32 with app.app_context():
33 return self.run(*args, **kwargs)
34
35 celery_app = Celery(app.name, task_cls=FlaskTask)
36 celery_app.config_from_object(app.config["CELERY"])
37 celery_app.set_default()
38 app.extensions["celery"] = celery_app
39 return celery_app
examples/celery/src/task_app/templates/index.html
1 <!doctype html>
2 <html>
3 <head>
4 <meta charset=UTF-8>
5 <title>Celery Example</title>
6 </head>
7 <body>
8 <h2>Celery Example</h2>
9 Execute background tasks with Celery. Submits tasks and shows results using JavaScript.
10
11 <hr>
12 <h4>Add</h4>
13 <p>Start a task to add two numbers, then poll for the result.
14 <form id=add method=post action="{{ url_for("tasks.add") }}">
15 <label>A <input type=number name=a value=4></label><br>
16 <label>B <input type=number name=b value=2></label><br>
17 <input type=submit>
18 </form>
19 <p>Result: <span id=add-result></span></p>
20
21 <hr>
22 <h4>Block</h4>
23 <p>Start a task that takes 5 seconds. However, the response will return immediately.
24 <form id=block method=post action="{{ url_for("tasks.block") }}">
25 <input type=submit>
26 </form>
27 <p id=block-result></p>
28
29 <hr>
30 <h4>Process</h4>
31 <p>Start a task that counts, waiting one second each time, showing progress.
32 <form id=process method=post action="{{ url_for("tasks.process") }}">
33 <label>Total <input type=number name=total value="10"></label><br>
34 <input type=submit>
35 </form>
examples/celery/src/task_app/templates/index.html 29
36 <p id=process-result></p>
37
38 <script>
39 const taskForm = (formName, doPoll, report) => {
40 document.forms[formName].addEventListener("submit", (event) => {
41 event.preventDefault()
42 fetch(event.target.action, {
43 method: "POST",
44 body: new FormData(event.target)
45 })
46 .then(response => response.json())
47 .then(data => {
48 report(null)
49
50 const poll = () => {
51 fetch(`/tasks/result/${data["result_id"]}`)
52 .then(response => response.json())
53 .then(data => {
54 report(data)
55
56 if (!data["ready"]) {
57 setTimeout(poll, 500)
58 } else if (!data["successful"]) {
59 console.error(formName, data)
60 }
61 })
62 }
63
64 if (doPoll) {
65 poll()
66 }
67 })
68 })
69 }
70
71 taskForm("add", true, data => {
72 const el = document.getElementById("add-result")
73
74 if (data === null) {
75 el.innerText = "submitted"
76 } else if (!data["ready"]) {
77 el.innerText = "waiting"
78 } else if (!data["successful"]) {
79 el.innerText = "error, check console"
80 } else {
81 el.innerText = data["value"]
82 }
83 })
84
85 taskForm("block", false, data => {
86 document.getElementById("block-result").innerText = (
87 "request finished, check celery log to see task finish in 5 seconds"
88 )
89 })
90
91 taskForm("process", true, data => {
92 const el = document.getElementById("process-result")
93
94 if (data === null) {
95 el.innerText = "submitted"
96 } else if (!data["ready"]) {
97 el.innerText = `${data["value"]["current"]} / ${data["value"]["total"]}`
98 } else if (!data["successful"]) {
99 el.innerText = "error, check console"
100 } else {
101 el.innerText = "� done"
102 }
103 console.log(data)
104 })
105
106 </script>
30
107 </body>
108 </html>
examples/javascript/.gitignore
1 .venv/
2 *.pyc
3 __pycache__/
4 instance/
5 .cache/
6 .pytest_cache/
7 .coverage
8 htmlcov/
9 dist/
10 build/
11 *.egg-info/
12 .idea/
13 *.swp
14 *~
examples/javascript/pyproject.toml
1 [project]
2 name = "js_example"
3 version = "1.1.0"
4 description = "Demonstrates making AJAX requests to Flask."
5 readme = "README.rst"
6 license = {file = "LICENSE.txt"}
7 maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}]
8 classifiers = ["Private :: Do Not Upload"]
9 dependencies = ["flask"]
10
11 [project.urls]
12 Documentation = "https://flask.palletsprojects.com/patterns/javascript/"
13
14 [project.optional-dependencies]
15 test = ["pytest"]
16
17 [build-system]
18 requires = ["flit_core<4"]
19 build-backend = "flit_core.buildapi"
20
21 [tool.flit.module]
22 name = "js_example"
23
24 [tool.pytest.ini_options]
25 testpaths = ["tests"]
26 filterwarnings = ["error"]
27
28 [tool.coverage.run]
29 branch = true
30 source = ["js_example", "tests"]
31
32 [tool.ruff]
33 src = ["src"]
examples/javascript/tests/conftest.py
1 import pytest
2
3 from js_example import app
4
5
6 @pytest.fixture(name="app")
7 def fixture_app():
8 app.testing = True
31
9 yield app
10 app.testing = False
11
12
13 @pytest.fixture
14 def client(app):
15 return app.test_client()
examples/javascript/tests/test_js_example.py
1 import pytest
2 from flask import template_rendered
3
4
5 @pytest.mark.parametrize(
6 ("path", "template_name"),
7 (
8 ("/", "fetch.html"),
9 ("/plain", "xhr.html"),
10 ("/fetch", "fetch.html"),
11 ("/jquery", "jquery.html"),
12 ),
13 )
14 def test_index(app, client, path, template_name):
15 def check(sender, template, context):
16 assert template.name == template_name
17
18 with template_rendered.connected_to(check, app):
19 client.get(path)
20
21
22 @pytest.mark.parametrize(
23 ("a", "b", "result"), ((2, 3, 5), (2.5, 3, 5.5), (2, None, 2), (2, "b", 2))
24 )
25 def test_add(client, a, b, result):
26 response = client.post("/add", data={"a": a, "b": b})
27 assert response.get_json()["result"] == result
examples/javascript/README.rst
1 JavaScript Ajax Example
2 =======================
3
4 Demonstrates how to post form data and process a JSON response using
5 JavaScript. This allows making requests without navigating away from the
6 page. Demonstrates using |fetch|_, |XMLHttpRequest|_, and
7 |jQuery.ajax|_. See the `Flask docs`_ about JavaScript and Ajax.
8
9 .. |fetch| replace:: ``fetch``
10 .. _fetch: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
11
12 .. |XMLHttpRequest| replace:: ``XMLHttpRequest``
13 .. _XMLHttpRequest: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
14
15 .. |jQuery.ajax| replace:: ``jQuery.ajax``
16 .. _jQuery.ajax: https://api.jquery.com/jQuery.ajax/
17
18 .. _Flask docs: https://flask.palletsprojects.com/patterns/javascript/
19
20
21 Install
22 -------
23
24 .. code-block:: text
25
26 $ python3 -m venv .venv
27 $ . .venv/bin/activate
28 $ pip install -e .
32
29
30
31 Run
32 ---
33
34 .. code-block:: text
35
36 $ flask --app js_example run
37
38 Open http://127.0.0.1:5000 in a browser.
39
40
41 Test
42 ----
43
44 .. code-block:: text
45
46 $ pip install -e '.[test]'
47 $ coverage run -m pytest
48 $ coverage report
examples/javascript/LICENSE.txt
1 Copyright 2010 Pallets
2
3 Redistribution and use in source and binary forms, with or without
4 modification, are permitted provided that the following conditions are
5 met:
6
7 1. Redistributions of source code must retain the above copyright
8 notice, this list of conditions and the following disclaimer.
9
10 2. Redistributions in binary form must reproduce the above copyright
11 notice, this list of conditions and the following disclaimer in the
12 documentation and/or other materials provided with the distribution.
13
14 3. Neither the name of the copyright holder nor the names of its
15 contributors may be used to endorse or promote products derived from
16 this software without specific prior written permission.
17
18 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
21 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
24 TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
examples/javascript/js_example/__init__.py
1 from flask import Flask
2
3 app = Flask(__name__)
4
5 from js_example import views # noqa: E402, F401
examples/javascript/js_example/views.py
1 from flask import jsonify
2 from flask import render_template
3 from flask import request
4
33
examples/javascript/js_example/templates/jquery.html
1 {% extends 'base.html' %}
2
3 {% block intro %}
4 <a href="https://jquery.com/">jQuery</a> is a popular library that
5 adds cross browser APIs for common tasks. However, it requires loading
6 an extra library.
7 {% endblock %}
8
9 {% block script %}
10 <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
11 <script>
12 function addSubmit(ev) {
13 ev.preventDefault();
14 $.ajax({
15 method: 'POST',
16 url: {{ url_for('add')|tojson }},
17 data: $(this).serialize()
18 }).done(addShow);
19 }
20
21 function addShow(data) {
22 $('#result').text(data.result);
23 }
24
25 $('#calc').on('submit', addSubmit);
26 </script>
27 {% endblock %}
examples/javascript/js_example/templates/fetch.html
1 {% extends 'base.html' %}
2
3 {% block intro %}
4 <a href="https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch"><code>fetch</code></a>
5 is the <em>modern</em> plain JavaScript way to make requests. It's
6 supported in all modern browsers.
7 {% endblock %}
8
9 {% block script %}
10 <script>
11 function addSubmit(ev) {
12 ev.preventDefault();
13 fetch({{ url_for('add')|tojson }}, {
14 method: 'POST',
15 body: new FormData(this)
16 })
17 .then(parseJSON)
18 .then(addShow);
19 }
20
21 function parseJSON(response) {
34
22 return response.json();
23 }
24
25 function addShow(data) {
26 var span = document.getElementById('result');
27 span.innerText = data.result;
28 }
29
30 var form = document.getElementById('calc');
31 form.addEventListener('submit', addSubmit);
32 </script>
33 {% endblock %}
examples/javascript/js_example/templates/xhr.html
1 {% extends 'base.html' %}
2
3 {% block intro %}
4 <a href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest"><code>XMLHttpRequest</code></a>
5 is the original JavaScript way to make requests. It's natively supported
6 by all browsers, but has been superseded by
7 <a href="{{ url_for("index", js="fetch") }}"><code>fetch</code></a>.
8 {% endblock %}
9
10 {% block script %}
11 <script>
12 function addSubmit(ev) {
13 ev.preventDefault();
14 var request = new XMLHttpRequest();
15 request.addEventListener('load', addShow);
16 request.open('POST', {{ url_for('add')|tojson }});
17 request.send(new FormData(this));
18 }
19
20 function addShow() {
21 var data = JSON.parse(this.responseText);
22 var span = document.getElementById('result');
23 span.innerText = data.result;
24 }
25
26 var form = document.getElementById('calc');
27 form.addEventListener('submit', addSubmit);
28 </script>
29 {% endblock %}
examples/javascript/js_example/templates/base.html
1 <!doctype html>
2 <title>JavaScript Example</title>
3 <link rel="stylesheet" href="https://unpkg.com/normalize.css@8.0.1/normalize.css">
4 <link rel="stylesheet" href="https://unpkg.com/sakura.css@1.3.1/css/sakura.css">
5 <style>
6 ul { margin: 0; padding: 0; display: flex; list-style-type: none; }
7 li > * { padding: 1em; }
8 li.active > a { color: #5e5e5e; border-bottom: 2px solid #4a4a4a; }
9 form { display: flex; }
10 label > input { width: 3em; }
11 form > * { padding-right: 1em; }
12 #result { font-weight: bold; }
13 </style>
14 <ul>
15 <li><span>Type:</span>
16 <li class="{% if js == 'fetch' %}active{% endif %}">
17 <a href="{{ url_for('index', js='fetch') }}">Fetch</a>
18 <li class="{% if js == 'xhr' %}active{% endif %}">
19 <a href="{{ url_for('index', js='xhr') }}">XHR</a>
20 <li class="{% if js == 'jquery' %}active{% endif %}">
21 <a href="{{ url_for('index', js='jquery') }}">jQuery</a>
35
22 </ul>
23 <hr>
24 <p>{% block intro %}{% endblock %}</p>
25 <hr>
26 <form id="calc">
27 <label>a <input name="a"></label>
28 <span>+</span>
29 <label>b <input name="b"></label>
30 <input type="submit" value="Calculate">
31 </form>
32 <span>= <span id="result"></span></span>
33 {% block script %}{% endblock %}
examples/tutorial/.gitignore
1 .venv/
2 *.pyc
3 __pycache__/
4 instance/
5 .cache/
6 .pytest_cache/
7 .coverage
8 htmlcov/
9 dist/
10 build/
11 *.egg-info/
12 .idea/
13 *.swp
14 *~
examples/tutorial/pyproject.toml
1 [project]
2 name = "flaskr"
3 version = "1.0.0"
4 description = "The basic blog app built in the Flask tutorial."
5 readme = "README.rst"
6 license = {file = "LICENSE.txt"}
7 maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}]
8 classifiers = ["Private :: Do Not Upload"]
9 dependencies = [
10 "flask",
11 ]
12
13 [project.urls]
14 Documentation = "https://flask.palletsprojects.com/tutorial/"
15
16 [project.optional-dependencies]
17 test = ["pytest"]
18
19 [build-system]
20 requires = ["flit_core<4"]
21 build-backend = "flit_core.buildapi"
22
23 [tool.flit.module]
24 name = "flaskr"
25
26 [tool.flit.sdist]
27 include = [
28 "tests/",
29 ]
30
31 [tool.pytest.ini_options]
32 testpaths = ["tests"]
33 filterwarnings = ["error"]
34
35 [tool.coverage.run]
36 branch = true
36
examples/tutorial/README.rst
1 Flaskr
2 ======
3
4 The basic blog app built in the Flask `tutorial`_.
5
6 .. _tutorial: https://flask.palletsprojects.com/tutorial/
7
8
9 Install
10 -------
11
12 **Be sure to use the same version of the code as the version of the docs
13 you're reading.** You probably want the latest tagged version, but the
14 default Git version is the main branch. ::
15
16 # clone the repository
17 $ git clone https://github.com/pallets/flask
18 $ cd flask
19 # checkout the correct version
20 $ git tag # shows the tagged versions
21 $ git checkout latest-tag-found-above
22 $ cd examples/tutorial
23
24 Create a virtualenv and activate it::
25
26 $ python3 -m venv .venv
27 $ . .venv/bin/activate
28
29 Or on Windows cmd::
30
31 $ py -3 -m venv .venv
32 $ .venv\Scripts\activate.bat
33
34 Install Flaskr::
35
36 $ pip install -e .
37
38 Or if you are using the main branch, install Flask from source before
39 installing Flaskr::
40
41 $ pip install -e ../..
42 $ pip install -e .
43
44
45 Run
46 ---
47
48 .. code-block:: text
49
50 $ flask --app flaskr init-db
51 $ flask --app flaskr run --debug
52
53 Open http://127.0.0.1:5000 in a browser.
54
55
56 Test
57 ----
58
59 ::
60
61 $ pip install '.[test]'
62 $ pytest
63
37
examples/tutorial/LICENSE.txt
1 Copyright 2010 Pallets
2
3 Redistribution and use in source and binary forms, with or without
4 modification, are permitted provided that the following conditions are
5 met:
6
7 1. Redistributions of source code must retain the above copyright
8 notice, this list of conditions and the following disclaimer.
9
10 2. Redistributions in binary form must reproduce the above copyright
11 notice, this list of conditions and the following disclaimer in the
12 documentation and/or other materials provided with the distribution.
13
14 3. Neither the name of the copyright holder nor the names of its
15 contributors may be used to endorse or promote products derived from
16 this software without specific prior written permission.
17
18 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
21 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
24 TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
examples/tutorial/tests/test_factory.py
1 from flaskr import create_app
2
3
4 def test_config():
5 """Test create_app without passing test config."""
6 assert not create_app().testing
7 assert create_app({"TESTING": True}).testing
8
9
10 def test_hello(client):
11 response = client.get("/hello")
12 assert response.data == b"Hello, World!"
examples/tutorial/tests/data.sql
1 INSERT INTO user (username, password)
2 VALUES
3 ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'),
4 ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79');
5
6 INSERT INTO post (title, body, author_id, created)
7 VALUES
8 ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00');
38
examples/tutorial/tests/test_db.py
1 import sqlite3
2
3 import pytest
4
5 from flaskr.db import get_db
6
7
8 def test_get_close_db(app):
9 with app.app_context():
10 db = get_db()
11 assert db is get_db()
12
13 with pytest.raises(sqlite3.ProgrammingError) as e:
14 db.execute("SELECT 1")
15
16 assert "closed" in str(e.value)
17
18
19 def test_init_db_command(runner, monkeypatch):
20 class Recorder:
21 called = False
22
23 def fake_init_db():
24 Recorder.called = True
25
26 monkeypatch.setattr("flaskr.db.init_db", fake_init_db)
27 result = runner.invoke(args=["init-db"])
28 assert "Initialized" in result.output
29 assert Recorder.called
examples/tutorial/tests/conftest.py
1 import os
2 import tempfile
3
4 import pytest
5
6 from flaskr import create_app
7 from flaskr.db import get_db
8 from flaskr.db import init_db
9
10 # read in SQL for populating test data
11 with open(os.path.join(os.path.dirname(__file__), "data.sql"), "rb") as f:
12 _data_sql = f.read().decode("utf8")
13
14
15 @pytest.fixture
16 def app():
17 """Create and configure a new app instance for each test."""
18 # create a temporary file to isolate the database for each test
19 db_fd, db_path = tempfile.mkstemp()
20 # create the app with common test config
21 app = create_app({"TESTING": True, "DATABASE": db_path})
22
23 # create the database and load test data
24 with app.app_context():
25 init_db()
26 get_db().executescript(_data_sql)
27
28 yield app
29
30 # close and remove the temporary database
31 os.close(db_fd)
32 os.unlink(db_path)
33
34
35 @pytest.fixture
39
36 def client(app):
37 """A test client for the app."""
38 return app.test_client()
39
40
41 @pytest.fixture
42 def runner(app):
43 """A test runner for the app's Click commands."""
44 return app.test_cli_runner()
45
46
47 class AuthActions:
48 def __init__(self, client):
49 self._client = client
50
51 def login(self, username="test", password="test"):
52 return self._client.post(
53 "/auth/login", data={"username": username, "password": password}
54 )
55
56 def logout(self):
57 return self._client.get("/auth/logout")
58
59
60 @pytest.fixture
61 def auth(client):
62 return AuthActions(client)
examples/tutorial/tests/test_auth.py
1 import pytest
2 from flask import g
3 from flask import session
4
5 from flaskr.db import get_db
6
7
8 def test_register(client, app):
9 # test that viewing the page renders without template errors
10 assert client.get("/auth/register").status_code == 200
11
12 # test that successful registration redirects to the login page
13 response = client.post("/auth/register", data={"username": "a", "password": "a"})
14 assert response.headers["Location"] == "/auth/login"
15
16 # test that the user was inserted into the database
17 with app.app_context():
18 assert (
19 get_db().execute("SELECT * FROM user WHERE username = 'a'").fetchone()
20 is not None
21 )
22
23
24 @pytest.mark.parametrize(
25 ("username", "password", "message"),
26 (
27 ("", "", b"Username is required."),
28 ("a", "", b"Password is required."),
29 ("test", "test", b"already registered"),
30 ),
31 )
32 def test_register_validate_input(client, username, password, message):
33 response = client.post(
34 "/auth/register", data={"username": username, "password": password}
35 )
36 assert message in response.data
37
38
39 def test_login(client, auth):
40 # test that viewing the page renders without template errors
40
examples/tutorial/tests/test_blog.py
1 import pytest
2
3 from flaskr.db import get_db
4
5
6 def test_index(client, auth):
7 response = client.get("/")
8 assert b"Log In" in response.data
9 assert b"Register" in response.data
10
11 auth.login()
12 response = client.get("/")
13 assert b"test title" in response.data
14 assert b"by test on 2018-01-01" in response.data
15 assert b"test\nbody" in response.data
16 assert b'href="/1/update"' in response.data
17
18
19 @pytest.mark.parametrize("path", ("/create", "/1/update", "/1/delete"))
20 def test_login_required(client, path):
21 response = client.post(path)
22 assert response.headers["Location"] == "/auth/login"
23
24
25 def test_author_required(app, client, auth):
26 # change the post author to another user
27 with app.app_context():
28 db = get_db()
29 db.execute("UPDATE post SET author_id = 2 WHERE id = 1")
30 db.commit()
31
32 auth.login()
33 # current user can't modify other user's post
34 assert client.post("/1/update").status_code == 403
35 assert client.post("/1/delete").status_code == 403
36 # current user doesn't see edit link
37 assert b'href="/1/update"' not in client.get("/").data
38
41
39
40 @pytest.mark.parametrize("path", ("/2/update", "/2/delete"))
41 def test_exists_required(client, auth, path):
42 auth.login()
43 assert client.post(path).status_code == 404
44
45
46 def test_create(client, auth, app):
47 auth.login()
48 assert client.get("/create").status_code == 200
49 client.post("/create", data={"title": "created", "body": ""})
50
51 with app.app_context():
52 db = get_db()
53 count = db.execute("SELECT COUNT(id) FROM post").fetchone()[0]
54 assert count == 2
55
56
57 def test_update(client, auth, app):
58 auth.login()
59 assert client.get("/1/update").status_code == 200
60 client.post("/1/update", data={"title": "updated", "body": ""})
61
62 with app.app_context():
63 db = get_db()
64 post = db.execute("SELECT * FROM post WHERE id = 1").fetchone()
65 assert post["title"] == "updated"
66
67
68 @pytest.mark.parametrize("path", ("/create", "/1/update"))
69 def test_create_update_validate(client, auth, path):
70 auth.login()
71 response = client.post(path, data={"title": "", "body": ""})
72 assert b"Title is required." in response.data
73
74
75 def test_delete(client, auth, app):
76 auth.login()
77 response = client.post("/1/delete")
78 assert response.headers["Location"] == "/"
79
80 with app.app_context():
81 db = get_db()
82 post = db.execute("SELECT * FROM post WHERE id = 1").fetchone()
83 assert post is None
examples/tutorial/flaskr/schema.sql
1 -- Initialize the database.
2 -- Drop any existing data and create empty tables.
3
4 DROP TABLE IF EXISTS user;
5 DROP TABLE IF EXISTS post;
6
7 CREATE TABLE user (
8 id INTEGER PRIMARY KEY AUTOINCREMENT,
9 username TEXT UNIQUE NOT NULL,
10 password TEXT NOT NULL
11 );
12
13 CREATE TABLE post (
14 id INTEGER PRIMARY KEY AUTOINCREMENT,
15 author_id INTEGER NOT NULL,
16 created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
17 title TEXT NOT NULL,
18 body TEXT NOT NULL,
19 FOREIGN KEY (author_id) REFERENCES user (id)
20 );
42
examples/tutorial/flaskr/db.py
1 import sqlite3
2 from datetime import datetime
3
4 import click
5 from flask import current_app
6 from flask import g
7
8
9 def get_db():
10 """Connect to the application's configured database. The connection
11 is unique for each request and will be reused if this is called
12 again.
13 """
14 if "db" not in g:
15 g.db = sqlite3.connect(
16 current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES
17 )
18 g.db.row_factory = sqlite3.Row
19
20 return g.db
21
22
23 def close_db(e=None):
24 """If this request connected to the database, close the
25 connection.
26 """
27 db = g.pop("db", None)
28
29 if db is not None:
30 db.close()
31
32
33 def init_db():
34 """Clear existing data and create new tables."""
35 db = get_db()
36
37 with current_app.open_resource("schema.sql") as f:
38 db.executescript(f.read().decode("utf8"))
39
40
41 @click.command("init-db")
42 def init_db_command():
43 """Clear existing data and create new tables."""
44 init_db()
45 click.echo("Initialized the database.")
46
47
48 sqlite3.register_converter("timestamp", lambda v: datetime.fromisoformat(v.decode()))
49
50
51 def init_app(app):
52 """Register database functions with the Flask app. This is called by
53 the application factory.
54 """
55 app.teardown_appcontext(close_db)
56 app.cli.add_command(init_db_command)
examples/tutorial/flaskr/__init__.py
1 import os
2
3 from flask import Flask
4
5
6 def create_app(test_config=None):
7 """Create and configure an instance of the Flask application."""
8 app = Flask(__name__, instance_relative_config=True)
43
9 app.config.from_mapping(
10 # a default secret that should be overridden by instance config
11 SECRET_KEY="dev",
12 # store the database in the instance folder
13 DATABASE=os.path.join(app.instance_path, "flaskr.sqlite"),
14 )
15
16 if test_config is None:
17 # load the instance config, if it exists, when not testing
18 app.config.from_pyfile("config.py", silent=True)
19 else:
20 # load the test config if passed in
21 app.config.update(test_config)
22
23 # ensure the instance folder exists
24 try:
25 os.makedirs(app.instance_path)
26 except OSError:
27 pass
28
29 @app.route("/hello")
30 def hello():
31 return "Hello, World!"
32
33 # register the database commands
34 from . import db
35
36 db.init_app(app)
37
38 # apply the blueprints to the app
39 from . import auth
40 from . import blog
41
42 app.register_blueprint(auth.bp)
43 app.register_blueprint(blog.bp)
44
45 # make url_for('index') == url_for('blog.index')
46 # in another app, you might define a separate main index here with
47 # app.route, while giving the blog blueprint a url_prefix, but for
48 # the tutorial the blog will be the main index
49 app.add_url_rule("/", endpoint="index")
50
51 return app
examples/tutorial/flaskr/static/style.css
1 html {
2 font-family: sans-serif;
3 background: #eee;
4 padding: 1rem;
5 }
6
7 body {
8 max-width: 960px;
9 margin: 0 auto;
10 background: white;
11 }
12
13 h1, h2, h3, h4, h5, h6 {
14 font-family: serif;
15 color: #377ba8;
16 margin: 1rem 0;
17 }
18
19 a {
20 color: #377ba8;
21 }
22
23 hr {
24 border: none;
examples/tutorial/flaskr/static/style.css 44
96 font-style: italic;
97 }
98
99 .post .body {
100 white-space: pre-line;
101 }
102
103 .content:last-child {
104 margin-bottom: 0;
105 }
106
107 .content form {
108 margin: 1em 0;
109 display: flex;
110 flex-direction: column;
111 }
112
113 .content label {
114 font-weight: bold;
115 margin-bottom: 0.5em;
116 }
117
118 .content input, .content textarea {
119 margin-bottom: 1em;
120 }
121
122 .content textarea {
123 min-height: 12em;
124 resize: vertical;
125 }
126
127 input.danger {
128 color: #cc2f2e;
129 }
130
131 input[type=submit] {
132 align-self: start;
133 min-width: 10em;
134 }
examples/tutorial/flaskr/auth.py
1 import functools
2
3 from flask import Blueprint
4 from flask import flash
5 from flask import g
6 from flask import redirect
7 from flask import render_template
8 from flask import request
9 from flask import session
10 from flask import url_for
11 from werkzeug.security import check_password_hash
12 from werkzeug.security import generate_password_hash
13
14 from .db import get_db
15
16 bp = Blueprint("auth", __name__, url_prefix="/auth")
17
18
19 def login_required(view):
20 """View decorator that redirects anonymous users to the login page."""
21
22 @functools.wraps(view)
23 def wrapped_view(**kwargs):
24 if g.user is None:
25 return redirect(url_for("auth.login"))
26
27 return view(**kwargs)
28
examples/tutorial/flaskr/auth.py 46
29 return wrapped_view
30
31
32 @bp.before_app_request
33 def load_logged_in_user():
34 """If a user id is stored in the session, load the user object from
35 the database into ``g.user``."""
36 user_id = session.get("user_id")
37
38 if user_id is None:
39 g.user = None
40 else:
41 g.user = (
42 get_db().execute("SELECT * FROM user WHERE id = ?", (user_id,)).fetchone()
43 )
44
45
46 @bp.route("/register", methods=("GET", "POST"))
47 def register():
48 """Register a new user.
49
50 Validates that the username is not already taken. Hashes the
51 password for security.
52 """
53 if request.method == "POST":
54 username = request.form["username"]
55 password = request.form["password"]
56 db = get_db()
57 error = None
58
59 if not username:
60 error = "Username is required."
61 elif not password:
62 error = "Password is required."
63
64 if error is None:
65 try:
66 db.execute(
67 "INSERT INTO user (username, password) VALUES (?, ?)",
68 (username, generate_password_hash(password)),
69 )
70 db.commit()
71 except db.IntegrityError:
72 # The username was already taken, which caused the
73 # commit to fail. Show a validation error.
74 error = f"User {username} is already registered."
75 else:
76 # Success, go to the login page.
77 return redirect(url_for("auth.login"))
78
79 flash(error)
80
81 return render_template("auth/register.html")
82
83
84 @bp.route("/login", methods=("GET", "POST"))
85 def login():
86 """Log in a registered user by adding the user id to the session."""
87 if request.method == "POST":
88 username = request.form["username"]
89 password = request.form["password"]
90 db = get_db()
91 error = None
92 user = db.execute(
93 "SELECT * FROM user WHERE username = ?", (username,)
94 ).fetchone()
95
96 if user is None:
97 error = "Incorrect username."
98 elif not check_password_hash(user["password"], password):
99 error = "Incorrect password."
47
100
101 if error is None:
102 # store the user id in a new session and return to the index
103 session.clear()
104 session["user_id"] = user["id"]
105 return redirect(url_for("index"))
106
107 flash(error)
108
109 return render_template("auth/login.html")
110
111
112 @bp.route("/logout")
113 def logout():
114 """Clear the current session, including the stored user id."""
115 session.clear()
116 return redirect(url_for("index"))
examples/tutorial/flaskr/blog.py
1 from flask import Blueprint
2 from flask import flash
3 from flask import g
4 from flask import redirect
5 from flask import render_template
6 from flask import request
7 from flask import url_for
8 from werkzeug.exceptions import abort
9
10 from .auth import login_required
11 from .db import get_db
12
13 bp = Blueprint("blog", __name__)
14
15
16 @bp.route("/")
17 def index():
18 """Show all the posts, most recent first."""
19 db = get_db()
20 posts = db.execute(
21 "SELECT p.id, title, body, created, author_id, username"
22 " FROM post p JOIN user u ON p.author_id = u.id"
23 " ORDER BY created DESC"
24 ).fetchall()
25 return render_template("blog/index.html", posts=posts)
26
27
28 def get_post(id, check_author=True):
29 """Get a post and its author by id.
30
31 Checks that the id exists and optionally that the current user is
32 the author.
33
34 :param id: id of post to get
35 :param check_author: require the current user to be the author
36 :return: the post with author information
37 :raise 404: if a post with the given id doesn't exist
38 :raise 403: if the current user isn't the author
39 """
40 post = (
41 get_db()
42 .execute(
43 "SELECT p.id, title, body, created, author_id, username"
44 " FROM post p JOIN user u ON p.author_id = u.id"
45 " WHERE p.id = ?",
46 (id,),
47 )
48 .fetchone()
49 )
50
examples/tutorial/flaskr/blog.py 48
51 if post is None:
52 abort(404, f"Post id {id} doesn't exist.")
53
54 if check_author and post["author_id"] != g.user["id"]:
55 abort(403)
56
57 return post
58
59
60 @bp.route("/create", methods=("GET", "POST"))
61 @login_required
62 def create():
63 """Create a new post for the current user."""
64 if request.method == "POST":
65 title = request.form["title"]
66 body = request.form["body"]
67 error = None
68
69 if not title:
70 error = "Title is required."
71
72 if error is not None:
73 flash(error)
74 else:
75 db = get_db()
76 db.execute(
77 "INSERT INTO post (title, body, author_id) VALUES (?, ?, ?)",
78 (title, body, g.user["id"]),
79 )
80 db.commit()
81 return redirect(url_for("blog.index"))
82
83 return render_template("blog/create.html")
84
85
86 @bp.route("/<int:id>/update", methods=("GET", "POST"))
87 @login_required
88 def update(id):
89 """Update a post if the current user is the author."""
90 post = get_post(id)
91
92 if request.method == "POST":
93 title = request.form["title"]
94 body = request.form["body"]
95 error = None
96
97 if not title:
98 error = "Title is required."
99
100 if error is not None:
101 flash(error)
102 else:
103 db = get_db()
104 db.execute(
105 "UPDATE post SET title = ?, body = ? WHERE id = ?", (title, body, id)
106 )
107 db.commit()
108 return redirect(url_for("blog.index"))
109
110 return render_template("blog/update.html", post=post)
111
112
113 @bp.route("/<int:id>/delete", methods=("POST",))
114 @login_required
115 def delete(id):
116 """Delete a post.
117
118 Ensures that the post exists and that the logged in user is the
119 author of the post.
120 """
121 get_post(id)
49
122 db = get_db()
123 db.execute("DELETE FROM post WHERE id = ?", (id,))
124 db.commit()
125 return redirect(url_for("blog.index"))
examples/tutorial/flaskr/templates/base.html
1 <!doctype html>
2 <title>{% block title %}{% endblock %} - Flaskr</title>
3 <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
4 <nav>
5 <h1><a href="{{ url_for('index') }}">Flaskr</a></h1>
6 <ul>
7 {% if g.user %}
8 <li><span>{{ g.user['username'] }}</span>
9 <li><a href="{{ url_for('auth.logout') }}">Log Out</a>
10 {% else %}
11 <li><a href="{{ url_for('auth.register') }}">Register</a>
12 <li><a href="{{ url_for('auth.login') }}">Log In</a>
13 {% endif %}
14 </ul>
15 </nav>
16 <section class="content">
17 <header>
18 {% block header %}{% endblock %}
19 </header>
20 {% for message in get_flashed_messages() %}
21 <div class="flash">{{ message }}</div>
22 {% endfor %}
23 {% block content %}{% endblock %}
24 </section>
examples/tutorial/flaskr/templates/auth/login.html
1 {% extends 'base.html' %}
2
3 {% block header %}
4 <h1>{% block title %}Log In{% endblock %}</h1>
5 {% endblock %}
6
7 {% block content %}
8 <form method="post">
9 <label for="username">Username</label>
10 <input name="username" id="username" required>
11 <label for="password">Password</label>
12 <input type="password" name="password" id="password" required>
13 <input type="submit" value="Log In">
14 </form>
15 {% endblock %}
examples/tutorial/flaskr/templates/auth/register.html
1 {% extends 'base.html' %}
2
3 {% block header %}
4 <h1>{% block title %}Register{% endblock %}</h1>
5 {% endblock %}
6
7 {% block content %}
8 <form method="post">
9 <label for="username">Username</label>
10 <input name="username" id="username" required>
11 <label for="password">Password</label>
12 <input type="password" name="password" id="password" required>
13 <input type="submit" value="Register">
50
14 </form>
15 {% endblock %}
examples/tutorial/flaskr/templates/blog/create.html
1 {% extends 'base.html' %}
2
3 {% block header %}
4 <h1>{% block title %}New Post{% endblock %}</h1>
5 {% endblock %}
6
7 {% block content %}
8 <form method="post">
9 <label for="title">Title</label>
10 <input name="title" id="title" value="{{ request.form['title'] }}" required>
11 <label for="body">Body</label>
12 <textarea name="body" id="body">{{ request.form['body'] }}</textarea>
13 <input type="submit" value="Save">
14 </form>
15 {% endblock %}
examples/tutorial/flaskr/templates/blog/update.html
1 {% extends 'base.html' %}
2
3 {% block header %}
4 <h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
5 {% endblock %}
6
7 {% block content %}
8 <form method="post">
9 <label for="title">Title</label>
10 <input name="title" id="title" value="{{ request.form['title'] or post['title'] }}" required>
11 <label for="body">Body</label>
12 <textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
13 <input type="submit" value="Save">
14 </form>
15 <hr>
16 <form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
17 <input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');">
18 </form>
19 {% endblock %}
examples/tutorial/flaskr/templates/blog/index.html
1 {% extends 'base.html' %}
2
3 {% block header %}
4 <h1>{% block title %}Posts{% endblock %}</h1>
5 {% if g.user %}
6 <a class="action" href="{{ url_for('blog.create') }}">New</a>
7 {% endif %}
8 {% endblock %}
9
10 {% block content %}
11 {% for post in posts %}
12 <article class="post">
13 <header>
14 <div>
15 <h1>{{ post['title'] }}</h1>
16 <div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div>
17 </div>
18 {% if g.user['id'] == post['author_id'] %}
19 <a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a>
20 {% endif %}
21 </header>
51
CHANGES.rst
1 Version 3.1.0
2 -------------
3
4 Released 2024-11-13
5
6 - Drop support for Python 3.8. :pr:`5623`
7 - Update minimum dependency versions to latest feature releases.
8 Werkzeug >= 3.1, ItsDangerous >= 2.2, Blinker >= 1.9. :pr:`5624,5633`
9 - Provide a configuration option to control automatic option
10 responses. :pr:`5496`
11 - ``Flask.open_resource``/``open_instance_resource`` and
12 ``Blueprint.open_resource`` take an ``encoding`` parameter to use when
13 opening in text mode. It defaults to ``utf-8``. :issue:`5504`
14 - ``Request.max_content_length`` can be customized per-request instead of only
15 through the ``MAX_CONTENT_LENGTH`` config. Added
16 ``MAX_FORM_MEMORY_SIZE`` and ``MAX_FORM_PARTS`` config. Added documentation
17 about resource limits to the security page. :issue:`5625`
18 - Add support for the ``Partitioned`` cookie attribute (CHIPS), with the
19 ``SESSION_COOKIE_PARTITIONED`` config. :issue:`5472`
20 - ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files.
21 ``load_dotenv`` loads default files in addition to a path unless
22 ``load_defaults=False`` is passed. :issue:`5628`
23 - Support key rotation with the ``SECRET_KEY_FALLBACKS`` config, a list of old
24 secret keys that can still be used for unsigning. Extensions will need to
25 add support. :issue:`5621`
26 - Fix how setting ``host_matching=True`` or ``subdomain_matching=False``
27 interacts with ``SERVER_NAME``. Setting ``SERVER_NAME`` no longer restricts
28 requests to only that domain. :issue:`5553`
29 - ``Request.trusted_hosts`` is checked during routing, and can be set through
30 the ``TRUSTED_HOSTS`` config. :issue:`5636`
31
32
33 Version 3.0.3
34 -------------
35
36 Released 2024-04-07
37
38 - The default ``hashlib.sha1`` may not be available in FIPS builds. Don't
39 access it at import time so the developer has time to change the default.
40 :issue:`5448`
41 - Don't initialize the ``cli`` attribute in the sansio scaffold, but rather in
42 the ``Flask`` concrete class. :pr:`5270`
43
44
45 Version 3.0.2
46 -------------
47
48 Released 2024-02-03
49
50 - Correct type for ``jinja_loader`` property. :issue:`5388`
51 - Fix error with ``--extra-files`` and ``--exclude-patterns`` CLI options.
52 :issue:`5391`
53
54
55 Version 3.0.1
56 -------------
57
58 Released 2024-01-18
59
60 - Correct type for ``path`` argument to ``send_file``. :issue:`5336`
CHANGES.rst 52
61 - Fix a typo in an error message for the ``flask run --key`` option. :pr:`5344`
62 - Session data is untagged without relying on the built-in ``json.loads``
63 ``object_hook``. This allows other JSON providers that don't implement that.
64 :issue:`5381`
65 - Address more type findings when using mypy strict mode. :pr:`5383`
66
67
68 Version 3.0.0
69 -------------
70
71 Released 2023-09-30
72
73 - Remove previously deprecated code. :pr:`5223`
74 - Deprecate the ``__version__`` attribute. Use feature detection, or
75 ``importlib.metadata.version("flask")``, instead. :issue:`5230`
76 - Restructure the code such that the Flask (app) and Blueprint
77 classes have Sans-IO bases. :pr:`5127`
78 - Allow self as an argument to url_for. :pr:`5264`
79 - Require Werkzeug >= 3.0.0.
80
81
82 Version 2.3.3
83 -------------
84
85 Released 2023-08-21
86
87 - Python 3.12 compatibility.
88 - Require Werkzeug >= 2.3.7.
89 - Use ``flit_core`` instead of ``setuptools`` as build backend.
90 - Refactor how an app's root and instance paths are determined. :issue:`5160`
91
92
93 Version 2.3.2
94 -------------
95
96 Released 2023-05-01
97
98 - Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed.
99 - Update Werkzeug requirement to >=2.3.3 to apply recent bug fixes.
100
101
102 Version 2.3.1
103 -------------
104
105 Released 2023-04-25
106
107 - Restore deprecated ``from flask import Markup``. :issue:`5084`
108
109
110 Version 2.3.0
111 -------------
112
113 Released 2023-04-25
114
115 - Drop support for Python 3.7. :pr:`5072`
116 - Update minimum requirements to the latest versions: Werkzeug>=2.3.0, Jinja2>3.1.2,
117 itsdangerous>=2.1.2, click>=8.1.3.
118 - Remove previously deprecated code. :pr:`4995`
119
120 - The ``push`` and ``pop`` methods of the deprecated ``_app_ctx_stack`` and
121 ``_request_ctx_stack`` objects are removed. ``top`` still exists to give
122 extensions more time to update, but it will be removed.
123 - The ``FLASK_ENV`` environment variable, ``ENV`` config key, and ``app.env``
124 property are removed.
125 - The ``session_cookie_name``, ``send_file_max_age_default``, ``use_x_sendfile``,
126 ``propagate_exceptions``, and ``templates_auto_reload`` properties on ``app``
127 are removed.
128 - The ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_MIMETYPE``, and
129 ``JSONIFY_PRETTYPRINT_REGULAR`` config keys are removed.
130 - The ``app.before_first_request`` and ``bp.before_app_first_request`` decorators
131 are removed.
CHANGES.rst 53
132 - ``json_encoder`` and ``json_decoder`` attributes on app and blueprint, and the
133 corresponding ``json.JSONEncoder`` and ``JSONDecoder`` classes, are removed.
134 - The ``json.htmlsafe_dumps`` and ``htmlsafe_dump`` functions are removed.
135 - Calling setup methods on blueprints after registration is an error instead of a
136 warning. :pr:`4997`
137
138 - Importing ``escape`` and ``Markup`` from ``flask`` is deprecated. Import them
139 directly from ``markupsafe`` instead. :pr:`4996`
140 - The ``app.got_first_request`` property is deprecated. :pr:`4997`
141 - The ``locked_cached_property`` decorator is deprecated. Use a lock inside the
142 decorated function if locking is needed. :issue:`4993`
143 - Signals are always available. ``blinker>=1.6.2`` is a required dependency. The
144 ``signals_available`` attribute is deprecated. :issue:`5056`
145 - Signals support ``async`` subscriber functions. :pr:`5049`
146 - Remove uses of locks that could cause requests to block each other very briefly.
147 :issue:`4993`
148 - Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``.
149 :pr:`4947`
150 - Ensure subdomains are applied with nested blueprints. :issue:`4834`
151 - ``config.from_file`` can use ``text=False`` to indicate that the parser wants a
152 binary file instead. :issue:`4989`
153 - If a blueprint is created with an empty name it raises a ``ValueError``.
154 :issue:`5010`
155 - ``SESSION_COOKIE_DOMAIN`` does not fall back to ``SERVER_NAME``. The default is not
156 to set the domain, which modern browsers interpret as an exact match rather than
157 a subdomain match. Warnings about ``localhost`` and IP addresses are also removed.
158 :issue:`5051`
159 - The ``routes`` command shows each rule's ``subdomain`` or ``host`` when domain
160 matching is in use. :issue:`5004`
161 - Use postponed evaluation of annotations. :pr:`5071`
162
163
164 Version 2.2.5
165 -------------
166
167 Released 2023-05-02
168
169 - Update for compatibility with Werkzeug 2.3.3.
170 - Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed.
171
172
173 Version 2.2.4
174 -------------
175
176 Released 2023-04-25
177
178 - Update for compatibility with Werkzeug 2.3.
179
180
181 Version 2.2.3
182 -------------
183
184 Released 2023-02-15
185
186 - Autoescape is enabled by default for ``.svg`` template files. :issue:`4831`
187 - Fix the type of ``template_folder`` to accept ``pathlib.Path``. :issue:`4892`
188 - Add ``--debug`` option to the ``flask run`` command. :issue:`4777`
189
190
191 Version 2.2.2
192 -------------
193
194 Released 2022-08-08
195
196 - Update Werkzeug dependency to >= 2.2.2. This includes fixes related
197 to the new faster router, header parsing, and the development
198 server. :pr:`4754`
199 - Fix the default value for ``app.env`` to be ``"production"``. This
200 attribute remains deprecated. :issue:`4740`
201
202
CHANGES.rst 54
274 this will become an error just like the application setup methods.
275 :issue:`4571`
276 - ``before_first_request`` is deprecated. Run setup code when creating
277 the application instead. :issue:`4605`
278 - Added the ``View.init_every_request`` class attribute. If a view
279 subclass sets this to ``False``, the view will not create a new
280 instance on every request. :issue:`2520`.
281 - A ``flask.cli.FlaskGroup`` Click group can be nested as a
282 sub-command in a custom CLI. :issue:`3263`
283 - Add ``--app`` and ``--debug`` options to the ``flask`` CLI, instead
284 of requiring that they are set through environment variables.
285 :issue:`2836`
286 - Add ``--env-file`` option to the ``flask`` CLI. This allows
287 specifying a dotenv file to load in addition to ``.env`` and
288 ``.flaskenv``. :issue:`3108`
289 - It is no longer required to decorate custom CLI commands on
290 ``app.cli`` or ``blueprint.cli`` with ``@with_appcontext``, an app
291 context will already be active at that point. :issue:`2410`
292 - ``SessionInterface.get_expiration_time`` uses a timezone-aware
293 value. :pr:`4645`
294 - View functions can return generators directly instead of wrapping
295 them in a ``Response``. :pr:`4629`
296 - Add ``stream_template`` and ``stream_template_string`` functions to
297 render a template as a stream of pieces. :pr:`4629`
298 - A new implementation of context preservation during debugging and
299 testing. :pr:`4666`
300
301 - ``request``, ``g``, and other context-locals point to the
302 correct data when running code in the interactive debugger
303 console. :issue:`2836`
304 - Teardown functions are always run at the end of the request,
305 even if the context is preserved. They are also run after the
306 preserved context is popped.
307 - ``stream_with_context`` preserves context separately from a
308 ``with client`` block. It will be cleaned up when
309 ``response.get_data()`` or ``response.close()`` is called.
310
311 - Allow returning a list from a view function, to convert it to a
312 JSON response like a dict is. :issue:`4672`
313 - When type checking, allow ``TypedDict`` to be returned from view
314 functions. :pr:`4695`
315 - Remove the ``--eager-loading/--lazy-loading`` options from the
316 ``flask run`` command. The app is always eager loaded the first
317 time, then lazily loaded in the reloader. The reloader always prints
318 errors immediately but continues serving. Remove the internal
319 ``DispatchingApp`` middleware used by the previous implementation.
320 :issue:`4715`
321
322
323 Version 2.1.3
324 -------------
325
326 Released 2022-07-13
327
328 - Inline some optional imports that are only used for certain CLI
329 commands. :pr:`4606`
330 - Relax type annotation for ``after_request`` functions. :issue:`4600`
331 - ``instance_path`` for namespace packages uses the path closest to
332 the imported submodule. :issue:`4610`
333 - Clearer error message when ``render_template`` and
334 ``render_template_string`` are used outside an application context.
335 :pr:`4693`
336
337
338 Version 2.1.2
339 -------------
340
341 Released 2022-04-28
342
343 - Fix type annotation for ``json.loads``, it accepts str or bytes.
344 :issue:`4519`
CHANGES.rst 56
345 - The ``--cert`` and ``--key`` options on ``flask run`` can be given
346 in either order. :issue:`4459`
347
348
349 Version 2.1.1
350 -------------
351
352 Released on 2022-03-30
353
354 - Set the minimum required version of importlib_metadata to 3.6.0,
355 which is required on Python < 3.10. :issue:`4502`
356
357
358 Version 2.1.0
359 -------------
360
361 Released 2022-03-28
362
363 - Drop support for Python 3.6. :pr:`4335`
364 - Update Click dependency to >= 8.0. :pr:`4008`
365 - Remove previously deprecated code. :pr:`4337`
366
367 - The CLI does not pass ``script_info`` to app factory functions.
368 - ``config.from_json`` is replaced by
369 ``config.from_file(name, load=json.load)``.
370 - ``json`` functions no longer take an ``encoding`` parameter.
371 - ``safe_join`` is removed, use ``werkzeug.utils.safe_join``
372 instead.
373 - ``total_seconds`` is removed, use ``timedelta.total_seconds``
374 instead.
375 - The same blueprint cannot be registered with the same name. Use
376 ``name=`` when registering to specify a unique name.
377 - The test client's ``as_tuple`` parameter is removed. Use
378 ``response.request.environ`` instead. :pr:`4417`
379
380 - Some parameters in ``send_file`` and ``send_from_directory`` were
381 renamed in 2.0. The deprecation period for the old names is extended
382 to 2.2. Be sure to test with deprecation warnings visible.
383
384 - ``attachment_filename`` is renamed to ``download_name``.
385 - ``cache_timeout`` is renamed to ``max_age``.
386 - ``add_etags`` is renamed to ``etag``.
387 - ``filename`` is renamed to ``path``.
388
389 - The ``RequestContext.g`` property is deprecated. Use ``g`` directly
390 or ``AppContext.g`` instead. :issue:`3898`
391 - ``copy_current_request_context`` can decorate async functions.
392 :pr:`4303`
393 - The CLI uses ``importlib.metadata`` instead of ``pkg_resources`` to
394 load command entry points. :issue:`4419`
395 - Overriding ``FlaskClient.open`` will not cause an error on redirect.
396 :issue:`3396`
397 - Add an ``--exclude-patterns`` option to the ``flask run`` CLI
398 command to specify patterns that will be ignored by the reloader.
399 :issue:`4188`
400 - When using lazy loading (the default with the debugger), the Click
401 context from the ``flask run`` command remains available in the
402 loader thread. :issue:`4460`
403 - Deleting the session cookie uses the ``httponly`` flag.
404 :issue:`4485`
405 - Relax typing for ``errorhandler`` to allow the user to use more
406 precise types and decorate the same function multiple times.
407 :issue:`4095, 4295, 4297`
408 - Fix typing for ``__exit__`` methods for better compatibility with
409 ``ExitStack``. :issue:`4474`
410 - From Werkzeug, for redirect responses the ``Location`` header URL
411 will remain relative, and exclude the scheme and domain, by default.
412 :pr:`4496`
413 - Add ``Config.from_prefixed_env()`` to load config values from
414 environment variables that start with ``FLASK_`` or another prefix.
415 This parses values as JSON by default, and allows setting keys in
CHANGES.rst 57
629 -------------
630
631 Released 2019-07-04
632
633 - Bump minimum Werkzeug version to >= 0.15.
634 - Drop support for Python 3.4.
635 - Error handlers for ``InternalServerError`` or ``500`` will always be
636 passed an instance of ``InternalServerError``. If they are invoked
637 due to an unhandled exception, that original exception is now
638 available as ``e.original_exception`` rather than being passed
639 directly to the handler. The same is true if the handler is for the
640 base ``HTTPException``. This makes error handler behavior more
641 consistent. :pr:`3266`
642
643 - ``Flask.finalize_request`` is called for all unhandled
644 exceptions even if there is no ``500`` error handler.
645
646 - ``Flask.logger`` takes the same name as ``Flask.name`` (the value
647 passed as ``Flask(import_name)``. This reverts 1.0's behavior of
648 always logging to ``"flask.app"``, in order to support multiple apps
649 in the same process. A warning will be shown if old configuration is
650 detected that needs to be moved. :issue:`2866`
651 - ``RequestContext.copy`` includes the current session object in the
652 request context copy. This prevents ``session`` pointing to an
653 out-of-date object. :issue:`2935`
654 - Using built-in RequestContext, unprintable Unicode characters in
655 Host header will result in a HTTP 400 response and not HTTP 500 as
656 previously. :pr:`2994`
657 - ``send_file`` supports ``PathLike`` objects as described in
658 :pep:`519`, to support ``pathlib`` in Python 3. :pr:`3059`
659 - ``send_file`` supports ``BytesIO`` partial content.
660 :issue:`2957`
661 - ``open_resource`` accepts the "rt" file mode. This still does the
662 same thing as "r". :issue:`3163`
663 - The ``MethodView.methods`` attribute set in a base class is used by
664 subclasses. :issue:`3138`
665 - ``Flask.jinja_options`` is a ``dict`` instead of an
666 ``ImmutableDict`` to allow easier configuration. Changes must still
667 be made before creating the environment. :pr:`3190`
668 - Flask's ``JSONMixin`` for the request and response wrappers was
669 moved into Werkzeug. Use Werkzeug's version with Flask-specific
670 support. This bumps the Werkzeug dependency to >= 0.15.
671 :issue:`3125`
672 - The ``flask`` command entry point is simplified to take advantage
673 of Werkzeug 0.15's better reloader support. This bumps the Werkzeug
674 dependency to >= 0.15. :issue:`3022`
675 - Support ``static_url_path`` that ends with a forward slash.
676 :issue:`3134`
677 - Support empty ``static_folder`` without requiring setting an empty
678 ``static_url_path`` as well. :pr:`3124`
679 - ``jsonify`` supports ``dataclass`` objects. :pr:`3195`
680 - Allow customizing the ``Flask.url_map_class`` used for routing.
681 :pr:`3069`
682 - The development server port can be set to 0, which tells the OS to
683 pick an available port. :issue:`2926`
684 - The return value from ``cli.load_dotenv`` is more consistent with
685 the documentation. It will return ``False`` if python-dotenv is not
686 installed, or if the given path isn't a file. :issue:`2937`
687 - Signaling support has a stub for the ``connect_via`` method when
688 the Blinker library is not installed. :pr:`3208`
689 - Add an ``--extra-files`` option to the ``flask run`` CLI command to
690 specify extra files that will trigger the reloader on change.
691 :issue:`2897`
692 - Allow returning a dictionary from a view function. Similar to how
693 returning a string will produce a ``text/html`` response, returning
694 a dict will call ``jsonify`` to produce a ``application/json``
695 response. :pr:`3111`
696 - Blueprints have a ``cli`` Click group like ``app.cli``. CLI commands
697 registered with a blueprint will be available as a group under the
698 ``flask`` command. :issue:`1357`.
699 - When using the test client as a context manager (``with client:``),
CHANGES.rst 61
700 all preserved request contexts are popped when the block exits,
701 ensuring nested contexts are cleaned up correctly. :pr:`3157`
702 - Show a better error message when the view return type is not
703 supported. :issue:`3214`
704 - ``flask.testing.make_test_environ_builder()`` has been deprecated in
705 favour of a new class ``flask.testing.EnvironBuilder``. :pr:`3232`
706 - The ``flask run`` command no longer fails if Python is not built
707 with SSL support. Using the ``--cert`` option will show an
708 appropriate error message. :issue:`3211`
709 - URL matching now occurs after the request context is pushed, rather
710 than when it's created. This allows custom URL converters to access
711 the app and request contexts, such as to query a database for an id.
712 :issue:`3088`
713
714
715 Version 1.0.4
716 -------------
717
718 Released 2019-07-04
719
720 - The key information for ``BadRequestKeyError`` is no longer cleared
721 outside debug mode, so error handlers can still access it. This
722 requires upgrading to Werkzeug 0.15.5. :issue:`3249`
723 - ``send_file`` url quotes the ":" and "/" characters for more
724 compatible UTF-8 filename support in some browsers. :issue:`3074`
725 - Fixes for :pep:`451` import loaders and pytest 5.x. :issue:`3275`
726 - Show message about dotenv on stderr instead of stdout. :issue:`3285`
727
728
729 Version 1.0.3
730 -------------
731
732 Released 2019-05-17
733
734 - ``send_file`` encodes filenames as ASCII instead of Latin-1
735 (ISO-8859-1). This fixes compatibility with Gunicorn, which is
736 stricter about header encodings than :pep:`3333`. :issue:`2766`
737 - Allow custom CLIs using ``FlaskGroup`` to set the debug flag without
738 it always being overwritten based on environment variables.
739 :pr:`2765`
740 - ``flask --version`` outputs Werkzeug's version and simplifies the
741 Python version. :pr:`2825`
742 - ``send_file`` handles an ``attachment_filename`` that is a native
743 Python 2 string (bytes) with UTF-8 coded bytes. :issue:`2933`
744 - A catch-all error handler registered for ``HTTPException`` will not
745 handle ``RoutingException``, which is used internally during
746 routing. This fixes the unexpected behavior that had been introduced
747 in 1.0. :pr:`2986`
748 - Passing the ``json`` argument to ``app.test_client`` does not
749 push/pop an extra app context. :issue:`2900`
750
751
752 Version 1.0.2
753 -------------
754
755 Released 2018-05-02
756
757 - Fix more backwards compatibility issues with merging slashes between
758 a blueprint prefix and route. :pr:`2748`
759 - Fix error with ``flask routes`` command when there are no routes.
760 :issue:`2751`
761
762
763 Version 1.0.1
764 -------------
765
766 Released 2018-04-29
767
768 - Fix registering partials (with no ``__name__``) as view functions.
769 :pr:`2730`
770 - Don't treat lists returned from view functions the same as tuples.
CHANGES.rst 62
842 :pr:`2314`
843 - ``Cookie`` is added to the response's ``Vary`` header if the session
844 is accessed at all during the request (and not deleted). :pr:`2288`
845 - ``Flask.test_request_context`` accepts ``subdomain`` and
846 ``url_scheme`` arguments for use when building the base URL.
847 :pr:`1621`
848 - Set ``APPLICATION_ROOT`` to ``'/'`` by default. This was already the
849 implicit default when it was set to ``None``.
850 - ``TRAP_BAD_REQUEST_ERRORS`` is enabled by default in debug mode.
851 ``BadRequestKeyError`` has a message with the bad key in debug mode
852 instead of the generic bad request message. :pr:`2348`
853 - Allow registering new tags with ``TaggedJSONSerializer`` to support
854 storing other types in the session cookie. :pr:`2352`
855 - Only open the session if the request has not been pushed onto the
856 context stack yet. This allows ``stream_with_context`` generators to
857 access the same session that the containing view uses. :pr:`2354`
858 - Add ``json`` keyword argument for the test client request methods.
859 This will dump the given object as JSON and set the appropriate
860 content type. :pr:`2358`
861 - Extract JSON handling to a mixin applied to both the ``Request`` and
862 ``Response`` classes. This adds the ``Response.is_json`` and
863 ``Response.get_json`` methods to the response to make testing JSON
864 response much easier. :pr:`2358`
865 - Removed error handler caching because it caused unexpected results
866 for some exception inheritance hierarchies. Register handlers
867 explicitly for each exception if you want to avoid traversing the
868 MRO. :pr:`2362`
869 - Fix incorrect JSON encoding of aware, non-UTC datetimes. :pr:`2374`
870 - Template auto reloading will honor debug mode even if
871 ``Flask.jinja_env`` was already accessed. :pr:`2373`
872 - The following old deprecated code was removed. :issue:`2385`
873
874 - ``flask.ext`` - import extensions directly by their name instead
875 of through the ``flask.ext`` namespace. For example,
876 ``import flask.ext.sqlalchemy`` becomes
877 ``import flask_sqlalchemy``.
878 - ``Flask.init_jinja_globals`` - extend
879 ``Flask.create_jinja_environment`` instead.
880 - ``Flask.error_handlers`` - tracked by
881 ``Flask.error_handler_spec``, use ``Flask.errorhandler``
882 to register handlers.
883 - ``Flask.request_globals_class`` - use
884 ``Flask.app_ctx_globals_class`` instead.
885 - ``Flask.static_path`` - use ``Flask.static_url_path`` instead.
886 - ``Request.module`` - use ``Request.blueprint`` instead.
887
888 - The ``Request.json`` property is no longer deprecated. :issue:`1421`
889 - Support passing a ``EnvironBuilder`` or ``dict`` to
890 ``test_client.open``. :pr:`2412`
891 - The ``flask`` command and ``Flask.run`` will load environment
892 variables from ``.env`` and ``.flaskenv`` files if python-dotenv is
893 installed. :pr:`2416`
894 - When passing a full URL to the test client, the scheme in the URL is
895 used instead of ``PREFERRED_URL_SCHEME``. :pr:`2430`
896 - ``Flask.logger`` has been simplified. ``LOGGER_NAME`` and
897 ``LOGGER_HANDLER_POLICY`` config was removed. The logger is always
898 named ``flask.app``. The level is only set on first access, it
899 doesn't check ``Flask.debug`` each time. Only one format is used,
900 not different ones depending on ``Flask.debug``. No handlers are
901 removed, and a handler is only added if no handlers are already
902 configured. :pr:`2436`
903 - Blueprint view function names may not contain dots. :pr:`2450`
904 - Fix a ``ValueError`` caused by invalid ``Range`` requests in some
905 cases. :issue:`2526`
906 - The development server uses threads by default. :pr:`2529`
907 - Loading config files with ``silent=True`` will ignore ``ENOTDIR``
908 errors. :pr:`2581`
909 - Pass ``--cert`` and ``--key`` options to ``flask run`` to run the
910 development server over HTTPS. :pr:`2606`
911 - Added ``SESSION_COOKIE_SAMESITE`` to control the ``SameSite``
912 attribute on the session cookie. :pr:`2607`
CHANGES.rst 64
1126 possible to use that filter in single quoted attributes. This should
1127 make using that filter with angular.js easier.
1128 - Added support for byte strings back to the session system. This
1129 broke compatibility with the common case of people putting binary
1130 data for token verification into the session.
1131 - Fixed an issue where registering the same method twice for the same
1132 endpoint would trigger an exception incorrectly.
1133
1134
1135 Version 0.10
1136 ------------
1137
1138 Released 2013-06-13, codename Limoncello
1139
1140 - Changed default cookie serialization format from pickle to JSON to
1141 limit the impact an attacker can do if the secret key leaks.
1142 - Added ``template_test`` methods in addition to the already existing
1143 ``template_filter`` method family.
1144 - Added ``template_global`` methods in addition to the already
1145 existing ``template_filter`` method family.
1146 - Set the content-length header for x-sendfile.
1147 - ``tojson`` filter now does not escape script blocks in HTML5
1148 parsers.
1149 - ``tojson`` used in templates is now safe by default. This was
1150 allowed due to the different escaping behavior.
1151 - Flask will now raise an error if you attempt to register a new
1152 function on an already used endpoint.
1153 - Added wrapper module around simplejson and added default
1154 serialization of datetime objects. This allows much easier
1155 customization of how JSON is handled by Flask or any Flask
1156 extension.
1157 - Removed deprecated internal ``flask.session`` module alias. Use
1158 ``flask.sessions`` instead to get the session module. This is not to
1159 be confused with ``flask.session`` the session proxy.
1160 - Templates can now be rendered without request context. The behavior
1161 is slightly different as the ``request``, ``session`` and ``g``
1162 objects will not be available and blueprint's context processors are
1163 not called.
1164 - The config object is now available to the template as a real global
1165 and not through a context processor which makes it available even in
1166 imported templates by default.
1167 - Added an option to generate non-ascii encoded JSON which should
1168 result in less bytes being transmitted over the network. It's
1169 disabled by default to not cause confusion with existing libraries
1170 that might expect ``flask.json.dumps`` to return bytes by default.
1171 - ``flask.g`` is now stored on the app context instead of the request
1172 context.
1173 - ``flask.g`` now gained a ``get()`` method for not erroring out on
1174 non existing items.
1175 - ``flask.g`` now can be used with the ``in`` operator to see what's
1176 defined and it now is iterable and will yield all attributes stored.
1177 - ``flask.Flask.request_globals_class`` got renamed to
1178 ``flask.Flask.app_ctx_globals_class`` which is a better name to what
1179 it does since 0.10.
1180 - ``request``, ``session`` and ``g`` are now also added as proxies to
1181 the template context which makes them available in imported
1182 templates. One has to be very careful with those though because
1183 usage outside of macros might cause caching.
1184 - Flask will no longer invoke the wrong error handlers if a proxy
1185 exception is passed through.
1186 - Added a workaround for chrome's cookies in localhost not working as
1187 intended with domain names.
1188 - Changed logic for picking defaults for cookie values from sessions
1189 to work better with Google Chrome.
1190 - Added ``message_flashed`` signal that simplifies flashing testing.
1191 - Added support for copying of request contexts for better working
1192 with greenlets.
1193 - Removed custom JSON HTTP exception subclasses. If you were relying
1194 on them you can reintroduce them again yourself trivially. Using
1195 them however is strongly discouraged as the interface was flawed.
1196 - Python requirements changed: requiring Python 2.6 or 2.7 now to
CHANGES.rst 68
1410 Also the behavior for ``after_request`` was changed. It's now no
1411 longer executed when an exception is raised.
1412 - Implemented ``has_request_context``.
1413 - Deprecated ``init_jinja_globals``. Override the
1414 ``Flask.create_jinja_environment`` method instead to achieve the
1415 same functionality.
1416 - Added ``safe_join``.
1417 - The automatic JSON request data unpacking now looks at the charset
1418 mimetype parameter.
1419 - Don't modify the session on ``get_flashed_messages`` if there are no
1420 messages in the session.
1421 - ``before_request`` handlers are now able to abort requests with
1422 errors.
1423 - It is not possible to define user exception handlers. That way you
1424 can provide custom error messages from a central hub for certain
1425 errors that might occur during request processing (for instance
1426 database connection errors, timeouts from remote resources etc.).
1427 - Blueprints can provide blueprint specific error handlers.
1428 - Implemented generic class-based views.
1429
1430
1431 Version 0.6.1
1432 -------------
1433
1434 Released 2010-12-31
1435
1436 - Fixed an issue where the default ``OPTIONS`` response was not
1437 exposing all valid methods in the ``Allow`` header.
1438 - Jinja2 template loading syntax now allows "./" in front of a
1439 template load path. Previously this caused issues with module
1440 setups.
1441 - Fixed an issue where the subdomain setting for modules was ignored
1442 for the static folder.
1443 - Fixed a security problem that allowed clients to download arbitrary
1444 files if the host server was a windows based operating system and
1445 the client uses backslashes to escape the directory the files where
1446 exposed from.
1447
1448
1449 Version 0.6
1450 -----------
1451
1452 Released 2010-07-27, codename Whisky
1453
1454 - After request functions are now called in reverse order of
1455 registration.
1456 - OPTIONS is now automatically implemented by Flask unless the
1457 application explicitly adds 'OPTIONS' as method to the URL rule. In
1458 this case no automatic OPTIONS handling kicks in.
1459 - Static rules are now even in place if there is no static folder for
1460 the module. This was implemented to aid GAE which will remove the
1461 static folder if it's part of a mapping in the .yml file.
1462 - ``Flask.config`` is now available in the templates as ``config``.
1463 - Context processors will no longer override values passed directly to
1464 the render function.
1465 - Added the ability to limit the incoming request data with the new
1466 ``MAX_CONTENT_LENGTH`` configuration value.
1467 - The endpoint for the ``Module.add_url_rule`` method is now optional
1468 to be consistent with the function of the same name on the
1469 application object.
1470 - Added a ``make_response`` function that simplifies creating response
1471 object instances in views.
1472 - Added signalling support based on blinker. This feature is currently
1473 optional and supposed to be used by extensions and applications. If
1474 you want to use it, make sure to have ``blinker`` installed.
1475 - Refactored the way URL adapters are created. This process is now
1476 fully customizable with the ``Flask.create_url_adapter`` method.
1477 - Modules can now register for a subdomain instead of just an URL
1478 prefix. This makes it possible to bind a whole module to a
1479 configurable subdomain.
1480
CHANGES.rst 72
1481
1482 Version 0.5.2
1483 -------------
1484
1485 Released 2010-07-15
1486
1487 - Fixed another issue with loading templates from directories when
1488 modules were used.
1489
1490
1491 Version 0.5.1
1492 -------------
1493
1494 Released 2010-07-06
1495
1496 - Fixes an issue with template loading from directories when modules
1497 where used.
1498
1499
1500 Version 0.5
1501 -----------
1502
1503 Released 2010-07-06, codename Calvados
1504
1505 - Fixed a bug with subdomains that was caused by the inability to
1506 specify the server name. The server name can now be set with the
1507 ``SERVER_NAME`` config key. This key is now also used to set the
1508 session cookie cross-subdomain wide.
1509 - Autoescaping is no longer active for all templates. Instead it is
1510 only active for ``.html``, ``.htm``, ``.xml`` and ``.xhtml``. Inside
1511 templates this behavior can be changed with the ``autoescape`` tag.
1512 - Refactored Flask internally. It now consists of more than a single
1513 file.
1514 - ``send_file`` now emits etags and has the ability to do conditional
1515 responses builtin.
1516 - (temporarily) dropped support for zipped applications. This was a
1517 rarely used feature and led to some confusing behavior.
1518 - Added support for per-package template and static-file directories.
1519 - Removed support for ``create_jinja_loader`` which is no longer used
1520 in 0.5 due to the improved module support.
1521 - Added a helper function to expose files from any directory.
1522
1523
1524 Version 0.4
1525 -----------
1526
1527 Released 2010-06-18, codename Rakia
1528
1529 - Added the ability to register application wide error handlers from
1530 modules.
1531 - ``Flask.after_request`` handlers are now also invoked if the request
1532 dies with an exception and an error handling page kicks in.
1533 - Test client has not the ability to preserve the request context for
1534 a little longer. This can also be used to trigger custom requests
1535 that do not pop the request stack for testing.
1536 - Because the Python standard library caches loggers, the name of the
1537 logger is configurable now to better support unittests.
1538 - Added ``TESTING`` switch that can activate unittesting helpers.
1539 - The logger switches to ``DEBUG`` mode now if debug is enabled.
1540
1541
1542 Version 0.3.1
1543 -------------
1544
1545 Released 2010-05-28
1546
1547 - Fixed a error reporting bug with ``Config.from_envvar``.
1548 - Removed some unused code.
1549 - Release does no longer include development leftover files (.git
1550 folder for themes, built documentation in zip and pdf file and some
1551 .pyc files)
73
1552
1553
1554 Version 0.3
1555 -----------
1556
1557 Released 2010-05-28, codename Schnaps
1558
1559 - Added support for categories for flashed messages.
1560 - The application now configures a ``logging.Handler`` and will log
1561 request handling exceptions to that logger when not in debug mode.
1562 This makes it possible to receive mails on server errors for
1563 example.
1564 - Added support for context binding that does not require the use of
1565 the with statement for playing in the console.
1566 - The request context is now available within the with statement
1567 making it possible to further push the request context or pop it.
1568 - Added support for configurations.
1569
1570
1571 Version 0.2
1572 -----------
1573
1574 Released 2010-05-12, codename J?germeister
1575
1576 - Various bugfixes
1577 - Integrated JSON support
1578 - Added ``get_template_attribute`` helper function.
1579 - ``Flask.add_url_rule`` can now also register a view function.
1580 - Refactored internal request dispatching.
1581 - Server listens on 127.0.0.1 by default now to fix issues with
1582 chrome.
1583 - Added external URL support.
1584 - Added support for ``send_file``.
1585 - Module support and internal request handling refactoring to better
1586 support pluggable applications.
1587 - Sessions can be set to be permanent now on a per-session basis.
1588 - Better error reporting on missing secret keys.
1589 - Added support for Google Appengine.
1590
1591
1592 Version 0.1
1593 -----------
1594
1595 Released 2010-04-16
1596
1597 - First public preview release.
tests/static/index.html
1 <h1>Hello World!</h1>
tests/static/config.toml
1 TEST_KEY="foo"
2 SECRET_KEY="config"
tests/static/config.json
1 {
2 "TEST_KEY": "foo",
3 "SECRET_KEY": "config"
4 }
74
tests/test_subclassing.py
1 from io import StringIO
2
3 import flask
4
5
6 def test_suppressed_exception_logging():
7 class SuppressedFlask(flask.Flask):
8 def log_exception(self, exc_info):
9 pass
10
11 out = StringIO()
12 app = SuppressedFlask(__name__)
13
14 @app.route("/")
15 def index():
16 raise Exception("test")
17
18 rv = app.test_client().get("/", errors_stream=out)
19 assert rv.status_code == 500
20 assert b"Internal Server Error" in rv.data
21 assert not out.getvalue()
tests/templates/nested/nested.txt
1 I'm nested
tests/templates/mail.txt
1 {{ foo}} Mail
tests/templates/simple_template.html
1 <h1>{{ whiskey }}</h1>
tests/templates/template_filter.html
1 {{ value|super_reverse }}
tests/templates/context_template.html
1 <p>{{ value }}|{{ injected_value }}
tests/templates/template_test.html
1 {% if value is boolean %}
2 Success!
3 {% endif %}
tests/templates/_macro.html
1 {% macro hello(name) %}Hello {{ name }}!{% endmacro %}
tests/templates/escaping_template.html
1 {{ text }}
2 {{ html }}
75
tests/templates/non_escaping_template.txt
1 {{ text }}
2 {{ html }}
3 {% autoescape false %}{{ text }}
4 {{ html }}{% endautoescape %}
5 {% autoescape true %}{{ text }}
6 {{ html }}{% endautoescape %}
7 {{ text }}
8 {{ html }}
tests/test_regression.py
1 import flask
2
3
4 def test_aborting(app):
5 class Foo(Exception):
6 whatever = 42
7
8 @app.errorhandler(Foo)
9 def handle_foo(e):
10 return str(e.whatever)
11
12 @app.route("/")
13 def index():
14 raise flask.abort(flask.redirect(flask.url_for("test")))
15
16 @app.route("/test")
17 def test():
18 raise Foo()
19
20 with app.test_client() as c:
21 rv = c.get("/")
22 location_parts = rv.headers["Location"].rpartition("/")
23
24 if location_parts[0]:
25 # For older Werkzeug that used absolute redirects.
26 assert location_parts[0] == "http://localhost"
27
28 assert location_parts[2] == "test"
29 rv = c.get("/test")
30 assert rv.data == b"42"
tests/test_session_interface.py
1 import flask
2 from flask.globals import request_ctx
3 from flask.sessions import SessionInterface
4
5
6 def test_open_session_with_endpoint():
7 """If request.endpoint (or other URL matching behavior) is needed
8 while loading the session, RequestContext.match_request() can be
9 called manually.
10 """
11
12 class MySessionInterface(SessionInterface):
13 def save_session(self, app, session, response):
14 pass
15
76
tests/test_converters.py
1 from werkzeug.routing import BaseConverter
2
3 from flask import request
4 from flask import session
5 from flask import url_for
6
7
8 def test_custom_converters(app, client):
9 class ListConverter(BaseConverter):
10 def to_python(self, value):
11 return value.split(",")
12
13 def to_url(self, value):
14 base_to_url = super().to_url
15 return ",".join(base_to_url(x) for x in value)
16
17 app.url_map.converters["list"] = ListConverter
18
19 @app.route("/<list:args>")
20 def index(args):
21 return "|".join(args)
22
23 assert client.get("/1,2,3").data == b"1|2|3"
24
25 with app.test_request_context():
26 assert url_for("index", args=[4, 5, 6]) == "/4,5,6"
27
28
29 def test_context_available(app, client):
30 class ContextConverter(BaseConverter):
31 def to_python(self, value):
32 assert request is not None
33 assert session is not None
34 return value
35
36 app.url_map.converters["ctx"] = ContextConverter
37
38 @app.get("/<ctx:name>")
39 def index(name):
40 return name
41
42 assert client.get("/admin").data == b"admin"
tests/test_apps/.flaskenv
1 FOO=flaskenv
2 BAR=bar
3 EGGS=0
77
tests/test_apps/.env
1 FOO=env
2 SPAM=1
3 EGGS=2
4 HAM=��
tests/test_apps/subdomaintestmodule/static/hello.txt
1 Hello Subdomain
tests/test_apps/subdomaintestmodule/__init__.py
1 from flask import Module
2
3 mod = Module(__name__, "foo", subdomain="foo")
tests/test_apps/helloworld/wsgi.py
1 from hello import app # noqa: F401
tests/test_apps/helloworld/hello.py
1 from flask import Flask
2
3 app = Flask(__name__)
4
5
6 @app.route("/")
7 def hello():
8 return "Hello World!"
tests/test_apps/cliapp/__init__.py
1
tests/test_apps/cliapp/message.txt
1 So long, and thanks for all the fish.
tests/test_apps/cliapp/app.py
1 from flask import Flask
2
3 testapp = Flask("testapp")
tests/test_apps/cliapp/multiapp.py
1 from flask import Flask
2
3 app1 = Flask("app1")
4 app2 = Flask("app2")
78
tests/test_apps/cliapp/importerrorapp.py
1 from flask import Flask
2
3 raise ImportError()
4
5 testapp = Flask("testapp")
tests/test_apps/cliapp/inner1/inner2/__init__.py
1
tests/test_apps/cliapp/inner1/inner2/flask.py
1 from flask import Flask
2
3 app = Flask(__name__)
tests/test_apps/cliapp/inner1/__init__.py
1 from flask import Flask
2
3 application = Flask(__name__)
tests/test_apps/cliapp/factory.py
1 from flask import Flask
2
3
4 def create_app():
5 return Flask("app")
6
7
8 def create_app2(foo, bar):
9 return Flask("_".join(["app2", foo, bar]))
10
11
12 def no_app():
13 pass
tests/test_apps/blueprintapp/__init__.py
1 from flask import Flask
2
3 app = Flask(__name__)
4 app.config["DEBUG"] = True
5 from blueprintapp.apps.admin import admin # noqa: E402
6 from blueprintapp.apps.frontend import frontend # noqa: E402
7
8 app.register_blueprint(admin)
9 app.register_blueprint(frontend)
tests/test_apps/blueprintapp/apps/__init__.py
1
tests/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html
1 Hello from the Frontend
79
tests/test_apps/blueprintapp/apps/frontend/__init__.py
1 from flask import Blueprint
2 from flask import render_template
3
4 frontend = Blueprint("frontend", __name__, template_folder="templates")
5
6
7 @frontend.route("/")
8 def index():
9 return render_template("frontend/index.html")
10
11
12 @frontend.route("/missing")
13 def missing_template():
14 return render_template("missing_template.html")
tests/test_apps/blueprintapp/apps/admin/templates/admin/index.html
1 Hello from the Admin
tests/test_apps/blueprintapp/apps/admin/static/test.txt
1 Admin File
tests/test_apps/blueprintapp/apps/admin/static/css/test.css
1 /* nested file */
tests/test_apps/blueprintapp/apps/admin/__init__.py
1 from flask import Blueprint
2 from flask import render_template
3
4 admin = Blueprint(
5 "admin",
6 __name__,
7 url_prefix="/admin",
8 template_folder="templates",
9 static_folder="static",
10 )
11
12
13 @admin.route("/")
14 def index():
15 return render_template("admin/index.html")
16
17
18 @admin.route("/index2")
19 def index2():
20 return render_template("./admin/index.html")
tests/test_request.py
1 from __future__ import annotations
2
3 from flask import Flask
4 from flask import Request
5 from flask import request
6 from flask.testing import FlaskClient
7
8
80
tests/test_json_tag.py
1 from datetime import datetime
2 from datetime import timezone
3 from uuid import uuid4
4
5 import pytest
tests/test_json_tag.py 81
77 class Tag2(JSONTag):
78 key = " 2"
79
80 s = TaggedJSONSerializer()
81
82 s.register(Tag1, index=-1)
83 assert isinstance(s.order[-2], Tag1)
84
85 s.register(Tag2, index=None)
86 assert isinstance(s.order[-1], Tag2)
tests/test_logging.py
1 import logging
2 import sys
3 from io import StringIO
4
5 import pytest
6
7 from flask.logging import default_handler
8 from flask.logging import has_level_handler
9 from flask.logging import wsgi_errors_stream
10
11
12 @pytest.fixture(autouse=True)
13 def reset_logging(pytestconfig):
14 root_handlers = logging.root.handlers[:]
15 logging.root.handlers = []
16 root_level = logging.root.level
17
18 logger = logging.getLogger("flask_test")
19 logger.handlers = []
20 logger.setLevel(logging.NOTSET)
21
22 logging_plugin = pytestconfig.pluginmanager.unregister(name="logging-plugin")
23
24 yield
25
26 logging.root.handlers[:] = root_handlers
27 logging.root.setLevel(root_level)
28
29 logger.handlers = []
30 logger.setLevel(logging.NOTSET)
31
32 if logging_plugin:
33 pytestconfig.pluginmanager.register(logging_plugin, "logging-plugin")
34
35
36 def test_logger(app):
37 assert app.logger.name == "flask_test"
38 assert app.logger.level == logging.NOTSET
39 assert app.logger.handlers == [default_handler]
40
41
42 def test_logger_debug(app):
43 app.debug = True
44 assert app.logger.level == logging.DEBUG
45 assert app.logger.handlers == [default_handler]
46
47
48 def test_existing_handler(app):
49 logging.root.addHandler(logging.StreamHandler())
50 assert app.logger.level == logging.NOTSET
51 assert not app.logger.handlers
52
53
54 def test_wsgi_errors_stream(app, client):
55 @app.route("/")
56 def index():
57 app.logger.error("test")
83
58 return ""
59
60 stream = StringIO()
61 client.get("/", errors_stream=stream)
62 assert "ERROR in test_logging: test" in stream.getvalue()
63
64 assert wsgi_errors_stream._get_current_object() is sys.stderr
65
66 with app.test_request_context(errors_stream=stream):
67 assert wsgi_errors_stream._get_current_object() is stream
68
69
70 def test_has_level_handler():
71 logger = logging.getLogger("flask.app")
72 assert not has_level_handler(logger)
73
74 handler = logging.StreamHandler()
75 logging.root.addHandler(handler)
76 assert has_level_handler(logger)
77
78 logger.propagate = False
79 assert not has_level_handler(logger)
80 logger.propagate = True
81
82 handler.setLevel(logging.ERROR)
83 assert not has_level_handler(logger)
84
85
86 def test_log_view_exception(app, client):
87 @app.route("/")
88 def index():
89 raise Exception("test")
90
91 app.testing = False
92 stream = StringIO()
93 rv = client.get("/", errors_stream=stream)
94 assert rv.status_code == 500
95 assert rv.data
96 err = stream.getvalue()
97 assert "Exception on / [GET]" in err
98 assert "Exception: test" in err
tests/test_async.py
1 import asyncio
2
3 import pytest
4
5 from flask import Blueprint
6 from flask import Flask
7 from flask import request
8 from flask.views import MethodView
9 from flask.views import View
10
11 pytest.importorskip("asgiref")
12
13
14 class AppError(Exception):
15 pass
16
17
18 class BlueprintError(Exception):
19 pass
20
21
22 class AsyncView(View):
23 methods = ["GET", "POST"]
24
25 async def dispatch_request(self):
26 await asyncio.sleep(0)
tests/test_async.py 84
27 return request.method
28
29
30 class AsyncMethodView(MethodView):
31 async def get(self):
32 await asyncio.sleep(0)
33 return "GET"
34
35 async def post(self):
36 await asyncio.sleep(0)
37 return "POST"
38
39
40 @pytest.fixture(name="async_app")
41 def _async_app():
42 app = Flask(__name__)
43
44 @app.route("/", methods=["GET", "POST"])
45 @app.route("/home", methods=["GET", "POST"])
46 async def index():
47 await asyncio.sleep(0)
48 return request.method
49
50 @app.errorhandler(AppError)
51 async def handle(_):
52 return "", 412
53
54 @app.route("/error")
55 async def error():
56 raise AppError()
57
58 blueprint = Blueprint("bp", __name__)
59
60 @blueprint.route("/", methods=["GET", "POST"])
61 async def bp_index():
62 await asyncio.sleep(0)
63 return request.method
64
65 @blueprint.errorhandler(BlueprintError)
66 async def bp_handle(_):
67 return "", 412
68
69 @blueprint.route("/error")
70 async def bp_error():
71 raise BlueprintError()
72
73 app.register_blueprint(blueprint, url_prefix="/bp")
74
75 app.add_url_rule("/view", view_func=AsyncView.as_view("view"))
76 app.add_url_rule("/methodview", view_func=AsyncMethodView.as_view("methodview"))
77
78 return app
79
80
81 @pytest.mark.parametrize("path", ["/", "/home", "/bp/", "/view", "/methodview"])
82 def test_async_route(path, async_app):
83 test_client = async_app.test_client()
84 response = test_client.get(path)
85 assert b"GET" in response.get_data()
86 response = test_client.post(path)
87 assert b"POST" in response.get_data()
88
89
90 @pytest.mark.parametrize("path", ["/error", "/bp/error"])
91 def test_async_error_handler(path, async_app):
92 test_client = async_app.test_client()
93 response = test_client.get(path)
94 assert response.status_code == 412
95
96
97 def test_async_before_after_request():
85
98 app_before_called = False
99 app_after_called = False
100 bp_before_called = False
101 bp_after_called = False
102
103 app = Flask(__name__)
104
105 @app.route("/")
106 def index():
107 return ""
108
109 @app.before_request
110 async def before():
111 nonlocal app_before_called
112 app_before_called = True
113
114 @app.after_request
115 async def after(response):
116 nonlocal app_after_called
117 app_after_called = True
118 return response
119
120 blueprint = Blueprint("bp", __name__)
121
122 @blueprint.route("/")
123 def bp_index():
124 return ""
125
126 @blueprint.before_request
127 async def bp_before():
128 nonlocal bp_before_called
129 bp_before_called = True
130
131 @blueprint.after_request
132 async def bp_after(response):
133 nonlocal bp_after_called
134 bp_after_called = True
135 return response
136
137 app.register_blueprint(blueprint, url_prefix="/bp")
138
139 test_client = app.test_client()
140 test_client.get("/")
141 assert app_before_called
142 assert app_after_called
143 test_client.get("/bp/")
144 assert bp_before_called
145 assert bp_after_called
tests/test_instance_config.py
1 import os
2
3 import pytest
4
5 import flask
6
7
8 def test_explicit_instance_paths(modules_tmp_path):
9 with pytest.raises(ValueError, match=".*must be absolute"):
10 flask.Flask(__name__, instance_path="instance")
11
12 app = flask.Flask(__name__, instance_path=os.fspath(modules_tmp_path))
13 assert app.instance_path == os.fspath(modules_tmp_path)
14
15
16 def test_uninstalled_module_paths(modules_tmp_path, purge_module):
17 (modules_tmp_path / "config_module_app.py").write_text(
18 "import os\n"
19 "import flask\n"
tests/test_instance_config.py 86
20 "here = os.path.abspath(os.path.dirname(__file__))\n"
21 "app = flask.Flask(__name__)\n"
22 )
23 purge_module("config_module_app")
24
25 from config_module_app import app
26
27 assert app.instance_path == os.fspath(modules_tmp_path / "instance")
28
29
30 def test_uninstalled_package_paths(modules_tmp_path, purge_module):
31 app = modules_tmp_path / "config_package_app"
32 app.mkdir()
33 (app / "__init__.py").write_text(
34 "import os\n"
35 "import flask\n"
36 "here = os.path.abspath(os.path.dirname(__file__))\n"
37 "app = flask.Flask(__name__)\n"
38 )
39 purge_module("config_package_app")
40
41 from config_package_app import app
42
43 assert app.instance_path == os.fspath(modules_tmp_path / "instance")
44
45
46 def test_uninstalled_namespace_paths(tmp_path, monkeypatch, purge_module):
47 def create_namespace(package):
48 project = tmp_path / f"project-{package}"
49 monkeypatch.syspath_prepend(os.fspath(project))
50 ns = project / "namespace" / package
51 ns.mkdir(parents=True)
52 (ns / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n")
53 return project
54
55 _ = create_namespace("package1")
56 project2 = create_namespace("package2")
57 purge_module("namespace.package2")
58 purge_module("namespace")
59
60 from namespace.package2 import app
61
62 assert app.instance_path == os.fspath(project2 / "instance")
63
64
65 def test_installed_module_paths(
66 modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages, limit_loader
67 ):
68 (site_packages / "site_app.py").write_text(
69 "import flask\napp = flask.Flask(__name__)\n"
70 )
71 purge_module("site_app")
72
73 from site_app import app
74
75 assert app.instance_path == os.fspath(
76 modules_tmp_path / "var" / "site_app-instance"
77 )
78
79
80 def test_installed_package_paths(
81 limit_loader, modules_tmp_path, modules_tmp_path_prefix, purge_module, monkeypatch
82 ):
83 installed_path = modules_tmp_path / "path"
84 installed_path.mkdir()
85 monkeypatch.syspath_prepend(installed_path)
86
87 app = installed_path / "installed_package"
88 app.mkdir()
89 (app / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n")
90 purge_module("installed_package")
87
91
92 from installed_package import app
93
94 assert app.instance_path == os.fspath(
95 modules_tmp_path / "var" / "installed_package-instance"
96 )
97
98
99 def test_prefix_package_paths(
100 limit_loader, modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages
101 ):
102 app = site_packages / "site_package"
103 app.mkdir()
104 (app / "__init__.py").write_text("import flask\napp = flask.Flask(__name__)\n")
105 purge_module("site_package")
106
107 import site_package
108
109 assert site_package.app.instance_path == os.fspath(
110 modules_tmp_path / "var" / "site_package-instance"
111 )
tests/type_check/typing_app_decorators.py
1 from __future__ import annotations
2
3 from flask import Flask
4 from flask import Response
5
6 app = Flask(__name__)
7
8
9 @app.after_request
10 def after_sync(response: Response) -> Response:
11 return Response()
12
13
14 @app.after_request
15 async def after_async(response: Response) -> Response:
16 return Response()
17
18
19 @app.before_request
20 def before_sync() -> None: ...
21
22
23 @app.before_request
24 async def before_async() -> None: ...
25
26
27 @app.teardown_appcontext
28 def teardown_sync(exc: BaseException | None) -> None: ...
29
30
31 @app.teardown_appcontext
32 async def teardown_async(exc: BaseException | None) -> None: ...
tests/type_check/typing_error_handler.py
1 from __future__ import annotations
2
3 from http import HTTPStatus
4
5 from werkzeug.exceptions import BadRequest
6 from werkzeug.exceptions import NotFound
7
8 from flask import Flask
9
88
10 app = Flask(__name__)
11
12
13 @app.errorhandler(400)
14 @app.errorhandler(HTTPStatus.BAD_REQUEST)
15 @app.errorhandler(BadRequest)
16 def handle_400(e: BadRequest) -> str:
17 return ""
18
19
20 @app.errorhandler(ValueError)
21 def handle_custom(e: ValueError) -> str:
22 return ""
23
24
25 @app.errorhandler(ValueError)
26 def handle_accept_base(e: Exception) -> str:
27 return ""
28
29
30 @app.errorhandler(BadRequest)
31 @app.errorhandler(404)
32 def handle_multiple(e: BadRequest | NotFound) -> str:
33 return ""
tests/type_check/typing_route.py
1 from __future__ import annotations
2
3 import typing as t
4 from http import HTTPStatus
5
6 from flask import Flask
7 from flask import jsonify
8 from flask import stream_template
9 from flask.templating import render_template
10 from flask.views import View
11 from flask.wrappers import Response
12
13 app = Flask(__name__)
14
15
16 @app.route("/str")
17 def hello_str() -> str:
18 return "<p>Hello, World!</p>"
19
20
21 @app.route("/bytes")
22 def hello_bytes() -> bytes:
23 return b"<p>Hello, World!</p>"
24
25
26 @app.route("/json")
27 def hello_json() -> Response:
28 return jsonify("Hello, World!")
29
30
31 @app.route("/json/dict")
32 def hello_json_dict() -> dict[str, t.Any]:
33 return {"response": "Hello, World!"}
34
35
36 @app.route("/json/dict")
37 def hello_json_list() -> list[t.Any]:
38 return [{"message": "Hello"}, {"message": "World"}]
39
40
41 class StatusJSON(t.TypedDict):
42 status: str
43
89
44
45 @app.route("/typed-dict")
46 def typed_dict() -> StatusJSON:
47 return {"status": "ok"}
48
49
50 @app.route("/generator")
51 def hello_generator() -> t.Generator[str, None, None]:
52 def show() -> t.Generator[str, None, None]:
53 for x in range(100):
54 yield f"data:{x}\n\n"
55
56 return show()
57
58
59 @app.route("/generator-expression")
60 def hello_generator_expression() -> t.Iterator[bytes]:
61 return (f"data:{x}\n\n".encode() for x in range(100))
62
63
64 @app.route("/iterator")
65 def hello_iterator() -> t.Iterator[str]:
66 return iter([f"data:{x}\n\n" for x in range(100)])
67
68
69 @app.route("/status")
70 @app.route("/status/<int:code>")
71 def tuple_status(code: int = 200) -> tuple[str, int]:
72 return "hello", code
73
74
75 @app.route("/status-enum")
76 def tuple_status_enum() -> tuple[str, int]:
77 return "hello", HTTPStatus.OK
78
79
80 @app.route("/headers")
81 def tuple_headers() -> tuple[str, dict[str, str]]:
82 return "Hello, World!", {"Content-Type": "text/plain"}
83
84
85 @app.route("/template")
86 @app.route("/template/<name>")
87 def return_template(name: str | None = None) -> str:
88 return render_template("index.html", name=name)
89
90
91 @app.route("/template")
92 def return_template_stream() -> t.Iterator[str]:
93 return stream_template("index.html", name="Hello")
94
95
96 @app.route("/async")
97 async def async_route() -> str:
98 return "Hello"
99
100
101 class RenderTemplateView(View):
102 def __init__(self: RenderTemplateView, template_name: str) -> None:
103 self.template_name = template_name
104
105 def dispatch_request(self: RenderTemplateView) -> str:
106 return render_template(self.template_name)
107
108
109 app.add_url_rule(
110 "/about",
111 view_func=RenderTemplateView.as_view("about_page", template_name="about.html"),
112 )
tests/conftest.py 90
tests/conftest.py
1 import os
2 import pkgutil
3 import sys
4
5 import pytest
6 from _pytest import monkeypatch
7
8 from flask import Flask
9 from flask.globals import request_ctx
10
11
12 @pytest.fixture(scope="session", autouse=True)
13 def _standard_os_environ():
14 """Set up ``os.environ`` at the start of the test session to have
15 standard values. Returns a list of operations that is used by
16 :func:`._reset_os_environ` after each test.
17 """
18 mp = monkeypatch.MonkeyPatch()
19 out = (
20 (os.environ, "FLASK_ENV_FILE", monkeypatch.notset),
21 (os.environ, "FLASK_APP", monkeypatch.notset),
22 (os.environ, "FLASK_DEBUG", monkeypatch.notset),
23 (os.environ, "FLASK_RUN_FROM_CLI", monkeypatch.notset),
24 (os.environ, "WERKZEUG_RUN_MAIN", monkeypatch.notset),
25 )
26
27 for _, key, value in out:
28 if value is monkeypatch.notset:
29 mp.delenv(key, False)
30 else:
31 mp.setenv(key, value)
32
33 yield out
34 mp.undo()
35
36
37 @pytest.fixture(autouse=True)
38 def _reset_os_environ(monkeypatch, _standard_os_environ):
39 """Reset ``os.environ`` to the standard environ after each test,
40 in case a test changed something without cleaning up.
41 """
42 monkeypatch._setitem.extend(_standard_os_environ)
43
44
45 @pytest.fixture
46 def app():
47 app = Flask("flask_test", root_path=os.path.dirname(__file__))
48 app.config.update(
49 TESTING=True,
50 SECRET_KEY="test key",
51 )
52 return app
53
54
55 @pytest.fixture
56 def app_ctx(app):
57 with app.app_context() as ctx:
58 yield ctx
59
60
61 @pytest.fixture
62 def req_ctx(app):
63 with app.test_request_context() as ctx:
64 yield ctx
65
66
67 @pytest.fixture
68 def client(app):
69 return app.test_client()
tests/conftest.py 91
70
71
72 @pytest.fixture
73 def test_apps(monkeypatch):
74 monkeypatch.syspath_prepend(os.path.join(os.path.dirname(__file__), "test_apps"))
75 original_modules = set(sys.modules.keys())
76
77 yield
78
79 # Remove any imports cached during the test. Otherwise "import app"
80 # will work in the next test even though it's no longer on the path.
81 for key in sys.modules.keys() - original_modules:
82 sys.modules.pop(key)
83
84
85 @pytest.fixture(autouse=True)
86 def leak_detector():
87 yield
88
89 # make sure we're not leaking a request context since we are
90 # testing flask internally in debug mode in a few cases
91 leaks = []
92 while request_ctx:
93 leaks.append(request_ctx._get_current_object())
94 request_ctx.pop()
95
96 assert leaks == []
97
98
99 @pytest.fixture(params=(True, False))
100 def limit_loader(request, monkeypatch):
101 """Patch pkgutil.get_loader to give loader without get_filename or archive.
102
103 This provides for tests where a system has custom loaders, e.g. Google App
104 Engine's HardenedModulesHook, which have neither the `get_filename` method
105 nor the `archive` attribute.
106
107 This fixture will run the testcase twice, once with and once without the
108 limitation/mock.
109 """
110 if not request.param:
111 return
112
113 class LimitedLoader:
114 def __init__(self, loader):
115 self.loader = loader
116
117 def __getattr__(self, name):
118 if name in {"archive", "get_filename"}:
119 raise AttributeError(f"Mocking a loader which does not have {name!r}.")
120 return getattr(self.loader, name)
121
122 old_get_loader = pkgutil.get_loader
123
124 def get_loader(*args, **kwargs):
125 return LimitedLoader(old_get_loader(*args, **kwargs))
126
127 monkeypatch.setattr(pkgutil, "get_loader", get_loader)
128
129
130 @pytest.fixture
131 def modules_tmp_path(tmp_path, monkeypatch):
132 """A temporary directory added to sys.path."""
133 rv = tmp_path / "modules_tmp"
134 rv.mkdir()
135 monkeypatch.syspath_prepend(os.fspath(rv))
136 return rv
137
138
139 @pytest.fixture
140 def modules_tmp_path_prefix(modules_tmp_path, monkeypatch):
92
tests/test_signals.py
1 import flask
2
3
4 def test_template_rendered(app, client):
5 @app.route("/")
6 def index():
7 return flask.render_template("simple_template.html", whiskey=42)
8
9 recorded = []
10
11 def record(sender, template, context):
12 recorded.append((template, context))
13
14 flask.template_rendered.connect(record, app)
15 try:
16 client.get("/")
17 assert len(recorded) == 1
18 template, context = recorded[0]
19 assert template.name == "simple_template.html"
20 assert context["whiskey"] == 42
21 finally:
22 flask.template_rendered.disconnect(record, app)
23
24
25 def test_before_render_template():
26 app = flask.Flask(__name__)
27
28 @app.route("/")
29 def index():
30 return flask.render_template("simple_template.html", whiskey=42)
31
32 recorded = []
33
34 def record(sender, template, context):
35 context["whiskey"] = 43
36 recorded.append((template, context))
37
38 flask.before_render_template.connect(record, app)
39 try:
40 rv = app.test_client().get("/")
41 assert len(recorded) == 1
42 template, context = recorded[0]
43 assert template.name == "simple_template.html"
44 assert context["whiskey"] == 43
45 assert rv.data == b"<h1>43</h1>"
46 finally:
47 flask.before_render_template.disconnect(record, app)
tests/test_signals.py 93
48
49
50 def test_request_signals():
51 app = flask.Flask(__name__)
52 calls = []
53
54 def before_request_signal(sender):
55 calls.append("before-signal")
56
57 def after_request_signal(sender, response):
58 assert response.data == b"stuff"
59 calls.append("after-signal")
60
61 @app.before_request
62 def before_request_handler():
63 calls.append("before-handler")
64
65 @app.after_request
66 def after_request_handler(response):
67 calls.append("after-handler")
68 response.data = "stuff"
69 return response
70
71 @app.route("/")
72 def index():
73 calls.append("handler")
74 return "ignored anyway"
75
76 flask.request_started.connect(before_request_signal, app)
77 flask.request_finished.connect(after_request_signal, app)
78
79 try:
80 rv = app.test_client().get("/")
81 assert rv.data == b"stuff"
82
83 assert calls == [
84 "before-signal",
85 "before-handler",
86 "handler",
87 "after-handler",
88 "after-signal",
89 ]
90 finally:
91 flask.request_started.disconnect(before_request_signal, app)
92 flask.request_finished.disconnect(after_request_signal, app)
93
94
95 def test_request_exception_signal():
96 app = flask.Flask(__name__)
97 recorded = []
98
99 @app.route("/")
100 def index():
101 raise ZeroDivisionError
102
103 def record(sender, exception):
104 recorded.append(exception)
105
106 flask.got_request_exception.connect(record, app)
107 try:
108 assert app.test_client().get("/").status_code == 500
109 assert len(recorded) == 1
110 assert isinstance(recorded[0], ZeroDivisionError)
111 finally:
112 flask.got_request_exception.disconnect(record, app)
113
114
115 def test_appcontext_signals(app, client):
116 recorded = []
117
118 def record_push(sender, **kwargs):
94
119 recorded.append("push")
120
121 def record_pop(sender, **kwargs):
122 recorded.append("pop")
123
124 @app.route("/")
125 def index():
126 return "Hello"
127
128 flask.appcontext_pushed.connect(record_push, app)
129 flask.appcontext_popped.connect(record_pop, app)
130 try:
131 rv = client.get("/")
132 assert rv.data == b"Hello"
133 assert recorded == ["push", "pop"]
134 finally:
135 flask.appcontext_pushed.disconnect(record_push, app)
136 flask.appcontext_popped.disconnect(record_pop, app)
137
138
139 def test_flash_signal(app):
140 @app.route("/")
141 def index():
142 flask.flash("This is a flash message", category="notice")
143 return flask.redirect("/other")
144
145 recorded = []
146
147 def record(sender, message, category):
148 recorded.append((message, category))
149
150 flask.message_flashed.connect(record, app)
151 try:
152 client = app.test_client()
153 with client.session_transaction():
154 client.get("/")
155 assert len(recorded) == 1
156 message, category = recorded[0]
157 assert message == "This is a flash message"
158 assert category == "notice"
159 finally:
160 flask.message_flashed.disconnect(record, app)
161
162
163 def test_appcontext_tearing_down_signal(app, client):
164 app.testing = False
165 recorded = []
166
167 def record_teardown(sender, exc):
168 recorded.append(exc)
169
170 @app.route("/")
171 def index():
172 raise ZeroDivisionError
173
174 flask.appcontext_tearing_down.connect(record_teardown, app)
175 try:
176 rv = client.get("/")
177 assert rv.status_code == 500
178 assert len(recorded) == 1
179 assert isinstance(recorded[0], ZeroDivisionError)
180 finally:
181 flask.appcontext_tearing_down.disconnect(record_teardown, app)
tests/test_appctx.py
1 import pytest
2
3 import flask
4 from flask.globals import app_ctx
tests/test_appctx.py 95
76 cleanup_stuff = []
77
78 @app.teardown_appcontext
79 def cleanup(exception):
80 cleanup_stuff.append(exception)
81
82 with app.app_context():
83 try:
84 raise Exception("dummy")
85 except Exception:
86 pass
87
88 assert cleanup_stuff == [None]
89
90
91 def test_app_tearing_down_with_handled_exception_by_app_handler(app, client):
92 app.config["PROPAGATE_EXCEPTIONS"] = True
93 cleanup_stuff = []
94
95 @app.teardown_appcontext
96 def cleanup(exception):
97 cleanup_stuff.append(exception)
98
99 @app.route("/")
100 def index():
101 raise Exception("dummy")
102
103 @app.errorhandler(Exception)
104 def handler(f):
105 return flask.jsonify(str(f))
106
107 with app.app_context():
108 client.get("/")
109
110 assert cleanup_stuff == [None]
111
112
113 def test_app_tearing_down_with_unhandled_exception(app, client):
114 app.config["PROPAGATE_EXCEPTIONS"] = True
115 cleanup_stuff = []
116
117 @app.teardown_appcontext
118 def cleanup(exception):
119 cleanup_stuff.append(exception)
120
121 @app.route("/")
122 def index():
123 raise ValueError("dummy")
124
125 with pytest.raises(ValueError, match="dummy"):
126 with app.app_context():
127 client.get("/")
128
129 assert len(cleanup_stuff) == 1
130 assert isinstance(cleanup_stuff[0], ValueError)
131 assert str(cleanup_stuff[0]) == "dummy"
132
133
134 def test_app_ctx_globals_methods(app, app_ctx):
135 # get
136 assert flask.g.get("foo") is None
137 assert flask.g.get("foo", "bar") == "bar"
138 # __contains__
139 assert "foo" not in flask.g
140 flask.g.foo = "bar"
141 assert "foo" in flask.g
142 # setdefault
143 flask.g.setdefault("bar", "the cake is a lie")
144 flask.g.setdefault("bar", "hello world")
145 assert flask.g.bar == "the cake is a lie"
146 # pop
97
tests/test_views.py
1 import pytest
2 from werkzeug.http import parse_set_header
3
4 import flask.views
tests/test_views.py 98
5
6
7 def common_test(app):
8 c = app.test_client()
9
10 assert c.get("/").data == b"GET"
11 assert c.post("/").data == b"POST"
12 assert c.put("/").status_code == 405
13 meths = parse_set_header(c.open("/", method="OPTIONS").headers["Allow"])
14 assert sorted(meths) == ["GET", "HEAD", "OPTIONS", "POST"]
15
16
17 def test_basic_view(app):
18 class Index(flask.views.View):
19 methods = ["GET", "POST"]
20
21 def dispatch_request(self):
22 return flask.request.method
23
24 app.add_url_rule("/", view_func=Index.as_view("index"))
25 common_test(app)
26
27
28 def test_method_based_view(app):
29 class Index(flask.views.MethodView):
30 def get(self):
31 return "GET"
32
33 def post(self):
34 return "POST"
35
36 app.add_url_rule("/", view_func=Index.as_view("index"))
37
38 common_test(app)
39
40
41 def test_view_patching(app):
42 class Index(flask.views.MethodView):
43 def get(self):
44 raise ZeroDivisionError
45
46 def post(self):
47 raise ZeroDivisionError
48
49 class Other(Index):
50 def get(self):
51 return "GET"
52
53 def post(self):
54 return "POST"
55
56 view = Index.as_view("index")
57 view.view_class = Other
58 app.add_url_rule("/", view_func=view)
59 common_test(app)
60
61
62 def test_view_inheritance(app, client):
63 class Index(flask.views.MethodView):
64 def get(self):
65 return "GET"
66
67 def post(self):
68 return "POST"
69
70 class BetterIndex(Index):
71 def delete(self):
72 return "DELETE"
73
74 app.add_url_rule("/", view_func=BetterIndex.as_view("index"))
75
tests/test_views.py 99
147 rv = client.get("/")
148 assert rv.data == b"Blub"
149 assert rv.headers["X-Method"] == "GET"
150 rv = client.head("/")
151 assert rv.data == b""
152 assert rv.headers["X-Method"] == "HEAD"
153
154
155 def test_explicit_head(app, client):
156 class Index(flask.views.MethodView):
157 def get(self):
158 return "GET"
159
160 def head(self):
161 return flask.Response("", headers={"X-Method": "HEAD"})
162
163 app.add_url_rule("/", view_func=Index.as_view("index"))
164 rv = client.get("/")
165 assert rv.data == b"GET"
166 rv = client.head("/")
167 assert rv.data == b""
168 assert rv.headers["X-Method"] == "HEAD"
169
170
171 def test_endpoint_override(app):
172 app.debug = True
173
174 class Index(flask.views.View):
175 methods = ["GET", "POST"]
176
177 def dispatch_request(self):
178 return flask.request.method
179
180 app.add_url_rule("/", view_func=Index.as_view("index"))
181
182 with pytest.raises(AssertionError):
183 app.add_url_rule("/", view_func=Index.as_view("index"))
184
185 # But these tests should still pass. We just log a warning.
186 common_test(app)
187
188
189 def test_methods_var_inheritance(app, client):
190 class BaseView(flask.views.MethodView):
191 methods = ["GET", "PROPFIND"]
192
193 class ChildView(BaseView):
194 def get(self):
195 return "GET"
196
197 def propfind(self):
198 return "PROPFIND"
199
200 app.add_url_rule("/", view_func=ChildView.as_view("index"))
201
202 assert client.get("/").data == b"GET"
203 assert client.open("/", method="PROPFIND").data == b"PROPFIND"
204 assert ChildView.methods == {"PROPFIND", "GET"}
205
206
207 def test_multiple_inheritance(app, client):
208 class GetView(flask.views.MethodView):
209 def get(self):
210 return "GET"
211
212 class DeleteView(flask.views.MethodView):
213 def delete(self):
214 return "DELETE"
215
216 class GetDeleteView(GetView, DeleteView):
217 pass
101
218
219 app.add_url_rule("/", view_func=GetDeleteView.as_view("index"))
220
221 assert client.get("/").data == b"GET"
222 assert client.delete("/").data == b"DELETE"
223 assert sorted(GetDeleteView.methods) == ["DELETE", "GET"]
224
225
226 def test_remove_method_from_parent(app, client):
227 class GetView(flask.views.MethodView):
228 def get(self):
229 return "GET"
230
231 class OtherView(flask.views.MethodView):
232 def post(self):
233 return "POST"
234
235 class View(GetView, OtherView):
236 methods = ["GET"]
237
238 app.add_url_rule("/", view_func=View.as_view("index"))
239
240 assert client.get("/").data == b"GET"
241 assert client.post("/").status_code == 405
242 assert sorted(View.methods) == ["GET"]
243
244
245 def test_init_once(app, client):
246 n = 0
247
248 class CountInit(flask.views.View):
249 init_every_request = False
250
251 def __init__(self):
252 nonlocal n
253 n += 1
254
255 def dispatch_request(self):
256 return str(n)
257
258 app.add_url_rule("/", view_func=CountInit.as_view("index"))
259 assert client.get("/").data == b"1"
260 assert client.get("/").data == b"1"
tests/test_config.py
1 import json
2 import os
3
4 import pytest
5
6 import flask
7
8 # config keys used for the TestConfig
9 TEST_KEY = "foo"
10 SECRET_KEY = "config"
11
12
13 def common_object_test(app):
14 assert app.secret_key == "config"
15 assert app.config["TEST_KEY"] == "foo"
16 assert "TestConfig" not in app.config
17
18
19 def test_config_from_pyfile():
20 app = flask.Flask(__name__)
21 app.config.from_pyfile(f"{__file__.rsplit('.', 1)[0]}.py")
22 common_object_test(app)
23
24
tests/test_config.py 102
25 def test_config_from_object():
26 app = flask.Flask(__name__)
27 app.config.from_object(__name__)
28 common_object_test(app)
29
30
31 def test_config_from_file_json():
32 app = flask.Flask(__name__)
33 current_dir = os.path.dirname(os.path.abspath(__file__))
34 app.config.from_file(os.path.join(current_dir, "static", "config.json"), json.load)
35 common_object_test(app)
36
37
38 def test_config_from_file_toml():
39 tomllib = pytest.importorskip("tomllib", reason="tomllib added in 3.11")
40 app = flask.Flask(__name__)
41 current_dir = os.path.dirname(os.path.abspath(__file__))
42 app.config.from_file(
43 os.path.join(current_dir, "static", "config.toml"), tomllib.load, text=False
44 )
45 common_object_test(app)
46
47
48 def test_from_prefixed_env(monkeypatch):
49 monkeypatch.setenv("FLASK_STRING", "value")
50 monkeypatch.setenv("FLASK_BOOL", "true")
51 monkeypatch.setenv("FLASK_INT", "1")
52 monkeypatch.setenv("FLASK_FLOAT", "1.2")
53 monkeypatch.setenv("FLASK_LIST", "[1, 2]")
54 monkeypatch.setenv("FLASK_DICT", '{"k": "v"}')
55 monkeypatch.setenv("NOT_FLASK_OTHER", "other")
56
57 app = flask.Flask(__name__)
58 app.config.from_prefixed_env()
59
60 assert app.config["STRING"] == "value"
61 assert app.config["BOOL"] is True
62 assert app.config["INT"] == 1
63 assert app.config["FLOAT"] == 1.2
64 assert app.config["LIST"] == [1, 2]
65 assert app.config["DICT"] == {"k": "v"}
66 assert "OTHER" not in app.config
67
68
69 def test_from_prefixed_env_custom_prefix(monkeypatch):
70 monkeypatch.setenv("FLASK_A", "a")
71 monkeypatch.setenv("NOT_FLASK_A", "b")
72
73 app = flask.Flask(__name__)
74 app.config.from_prefixed_env("NOT_FLASK")
75
76 assert app.config["A"] == "b"
77
78
79 def test_from_prefixed_env_nested(monkeypatch):
80 monkeypatch.setenv("FLASK_EXIST__ok", "other")
81 monkeypatch.setenv("FLASK_EXIST__inner__ik", "2")
82 monkeypatch.setenv("FLASK_EXIST__new__more", '{"k": false}')
83 monkeypatch.setenv("FLASK_NEW__K", "v")
84
85 app = flask.Flask(__name__)
86 app.config["EXIST"] = {"ok": "value", "flag": True, "inner": {"ik": 1}}
87 app.config.from_prefixed_env()
88
89 if os.name != "nt":
90 assert app.config["EXIST"] == {
91 "ok": "other",
92 "flag": True,
93 "inner": {"ik": 2},
94 "new": {"more": {"k": False}},
95 }
tests/test_config.py 103
96 else:
97 # Windows env var keys are always uppercase.
98 assert app.config["EXIST"] == {
99 "ok": "value",
100 "OK": "other",
101 "flag": True,
102 "inner": {"ik": 1},
103 "INNER": {"IK": 2},
104 "NEW": {"MORE": {"k": False}},
105 }
106
107 assert app.config["NEW"] == {"K": "v"}
108
109
110 def test_config_from_mapping():
111 app = flask.Flask(__name__)
112 app.config.from_mapping({"SECRET_KEY": "config", "TEST_KEY": "foo"})
113 common_object_test(app)
114
115 app = flask.Flask(__name__)
116 app.config.from_mapping([("SECRET_KEY", "config"), ("TEST_KEY", "foo")])
117 common_object_test(app)
118
119 app = flask.Flask(__name__)
120 app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo")
121 common_object_test(app)
122
123 app = flask.Flask(__name__)
124 app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo", skip_key="skip")
125 common_object_test(app)
126
127 app = flask.Flask(__name__)
128 with pytest.raises(TypeError):
129 app.config.from_mapping({}, {})
130
131
132 def test_config_from_class():
133 class Base:
134 TEST_KEY = "foo"
135
136 class Test(Base):
137 SECRET_KEY = "config"
138
139 app = flask.Flask(__name__)
140 app.config.from_object(Test)
141 common_object_test(app)
142
143
144 def test_config_from_envvar(monkeypatch):
145 monkeypatch.setattr("os.environ", {})
146 app = flask.Flask(__name__)
147
148 with pytest.raises(RuntimeError) as e:
149 app.config.from_envvar("FOO_SETTINGS")
150
151 assert "'FOO_SETTINGS' is not set" in str(e.value)
152 assert not app.config.from_envvar("FOO_SETTINGS", silent=True)
153
154 monkeypatch.setattr(
155 "os.environ", {"FOO_SETTINGS": f"{__file__.rsplit('.', 1)[0]}.py"}
156 )
157 assert app.config.from_envvar("FOO_SETTINGS")
158 common_object_test(app)
159
160
161 def test_config_from_envvar_missing(monkeypatch):
162 monkeypatch.setattr("os.environ", {"FOO_SETTINGS": "missing.cfg"})
163 app = flask.Flask(__name__)
164 with pytest.raises(IOError) as e:
165 app.config.from_envvar("FOO_SETTINGS")
166 msg = str(e.value)
tests/test_config.py 104
tests/test_user_error_handler.py
1 import pytest
2 from werkzeug.exceptions import Forbidden
3 from werkzeug.exceptions import HTTPException
4 from werkzeug.exceptions import InternalServerError
5 from werkzeug.exceptions import NotFound
6
7 import flask
8
9
10 def test_error_handler_no_match(app, client):
11 class CustomException(Exception):
12 pass
13
14 @app.errorhandler(CustomException)
15 def custom_exception_handler(e):
16 assert isinstance(e, CustomException)
17 return "custom"
18
19 with pytest.raises(TypeError) as exc_info:
20 app.register_error_handler(CustomException(), None)
21
22 assert "CustomException() is an instance, not a class." in str(exc_info.value)
23
24 with pytest.raises(ValueError) as exc_info:
25 app.register_error_handler(list, None)
26
27 assert "'list' is not a subclass of Exception." in str(exc_info.value)
28
29 @app.errorhandler(500)
30 def handle_500(e):
31 assert isinstance(e, InternalServerError)
32
33 if e.original_exception is not None:
34 return f"wrapped {type(e.original_exception).__name__}"
35
36 return "direct"
37
38 with pytest.raises(ValueError) as exc_info:
39 app.register_error_handler(999, None)
40
41 assert "Use a subclass of HTTPException" in str(exc_info.value)
42
43 @app.route("/custom")
44 def custom_test():
45 raise CustomException()
46
47 @app.route("/keyerror")
48 def key_error():
49 raise KeyError()
50
51 @app.route("/abort")
52 def do_abort():
53 flask.abort(500)
54
tests/test_user_error_handler.py 106
55 app.testing = False
56 assert client.get("/custom").data == b"custom"
57 assert client.get("/keyerror").data == b"wrapped KeyError"
58 assert client.get("/abort").data == b"direct"
59
60
61 def test_error_handler_subclass(app):
62 class ParentException(Exception):
63 pass
64
65 class ChildExceptionUnregistered(ParentException):
66 pass
67
68 class ChildExceptionRegistered(ParentException):
69 pass
70
71 @app.errorhandler(ParentException)
72 def parent_exception_handler(e):
73 assert isinstance(e, ParentException)
74 return "parent"
75
76 @app.errorhandler(ChildExceptionRegistered)
77 def child_exception_handler(e):
78 assert isinstance(e, ChildExceptionRegistered)
79 return "child-registered"
80
81 @app.route("/parent")
82 def parent_test():
83 raise ParentException()
84
85 @app.route("/child-unregistered")
86 def unregistered_test():
87 raise ChildExceptionUnregistered()
88
89 @app.route("/child-registered")
90 def registered_test():
91 raise ChildExceptionRegistered()
92
93 c = app.test_client()
94
95 assert c.get("/parent").data == b"parent"
96 assert c.get("/child-unregistered").data == b"parent"
97 assert c.get("/child-registered").data == b"child-registered"
98
99
100 def test_error_handler_http_subclass(app):
101 class ForbiddenSubclassRegistered(Forbidden):
102 pass
103
104 class ForbiddenSubclassUnregistered(Forbidden):
105 pass
106
107 @app.errorhandler(403)
108 def code_exception_handler(e):
109 assert isinstance(e, Forbidden)
110 return "forbidden"
111
112 @app.errorhandler(ForbiddenSubclassRegistered)
113 def subclass_exception_handler(e):
114 assert isinstance(e, ForbiddenSubclassRegistered)
115 return "forbidden-registered"
116
117 @app.route("/forbidden")
118 def forbidden_test():
119 raise Forbidden()
120
121 @app.route("/forbidden-registered")
122 def registered_test():
123 raise ForbiddenSubclassRegistered()
124
125 @app.route("/forbidden-unregistered")
tests/test_user_error_handler.py 107
197
198 @app.route("/forbidden")
199 def forbidden():
200 raise Forbidden()
201
202 @app.route("/slash/")
203 def slash():
204 return "slash"
205
206 app.register_blueprint(bp, url_prefix="/bp")
207
208 c = app.test_client()
209 assert c.get("/bp/undefined").data == b"bp-default"
210 assert c.get("/bp/forbidden").data == b"bp-forbidden"
211 assert c.get("/undefined").data == b"default"
212 assert c.get("/forbidden").data == b"forbidden"
213 # Don't handle RequestRedirect raised when adding slash.
214 assert c.get("/slash", follow_redirects=True).data == b"slash"
215
216
217 class TestGenericHandlers:
218 """Test how very generic handlers are dispatched to."""
219
220 class Custom(Exception):
221 pass
222
223 @pytest.fixture()
224 def app(self, app):
225 @app.route("/custom")
226 def do_custom():
227 raise self.Custom()
228
229 @app.route("/error")
230 def do_error():
231 raise KeyError()
232
233 @app.route("/abort")
234 def do_abort():
235 flask.abort(500)
236
237 @app.route("/raise")
238 def do_raise():
239 raise InternalServerError()
240
241 app.config["PROPAGATE_EXCEPTIONS"] = False
242 return app
243
244 def report_error(self, e):
245 original = getattr(e, "original_exception", None)
246
247 if original is not None:
248 return f"wrapped {type(original).__name__}"
249
250 return f"direct {type(e).__name__}"
251
252 @pytest.mark.parametrize("to_handle", (InternalServerError, 500))
253 def test_handle_class_or_code(self, app, client, to_handle):
254 """``InternalServerError`` and ``500`` are aliases, they should
255 have the same behavior. Both should only receive
256 ``InternalServerError``, which might wrap another error.
257 """
258
259 @app.errorhandler(to_handle)
260 def handle_500(e):
261 assert isinstance(e, InternalServerError)
262 return self.report_error(e)
263
264 assert client.get("/custom").data == b"wrapped Custom"
265 assert client.get("/error").data == b"wrapped KeyError"
266 assert client.get("/abort").data == b"direct InternalServerError"
267 assert client.get("/raise").data == b"direct InternalServerError"
109
268
269 def test_handle_generic_http(self, app, client):
270 """``HTTPException`` should only receive ``HTTPException``
271 subclasses. It will receive ``404`` routing exceptions.
272 """
273
274 @app.errorhandler(HTTPException)
275 def handle_http(e):
276 assert isinstance(e, HTTPException)
277 return str(e.code)
278
279 assert client.get("/error").data == b"500"
280 assert client.get("/abort").data == b"500"
281 assert client.get("/not-found").data == b"404"
282
283 def test_handle_generic(self, app, client):
284 """Generic ``Exception`` will handle all exceptions directly,
285 including ``HTTPExceptions``.
286 """
287
288 @app.errorhandler(Exception)
289 def handle_exception(e):
290 return self.report_error(e)
291
292 assert client.get("/custom").data == b"direct Custom"
293 assert client.get("/error").data == b"direct KeyError"
294 assert client.get("/abort").data == b"direct InternalServerError"
295 assert client.get("/not-found").data == b"direct NotFound"
tests/test_reqctx.py
1 import warnings
2
3 import pytest
4
5 import flask
6 from flask.globals import request_ctx
7 from flask.sessions import SecureCookieSessionInterface
8 from flask.sessions import SessionInterface
9
10 try:
11 from greenlet import greenlet
12 except ImportError:
13 greenlet = None
14
15
16 def test_teardown_on_pop(app):
17 buffer = []
18
19 @app.teardown_request
20 def end_of_request(exception):
21 buffer.append(exception)
22
23 ctx = app.test_request_context()
24 ctx.push()
25 assert buffer == []
26 ctx.pop()
27 assert buffer == [None]
28
29
30 def test_teardown_with_previous_exception(app):
31 buffer = []
32
33 @app.teardown_request
34 def end_of_request(exception):
35 buffer.append(exception)
36
37 try:
38 raise Exception("dummy")
39 except Exception:
tests/test_reqctx.py 110
40 pass
41
42 with app.test_request_context():
43 assert buffer == []
44 assert buffer == [None]
45
46
47 def test_teardown_with_handled_exception(app):
48 buffer = []
49
50 @app.teardown_request
51 def end_of_request(exception):
52 buffer.append(exception)
53
54 with app.test_request_context():
55 assert buffer == []
56 try:
57 raise Exception("dummy")
58 except Exception:
59 pass
60 assert buffer == [None]
61
62
63 def test_proper_test_request_context(app):
64 app.config.update(SERVER_NAME="localhost.localdomain:5000")
65
66 @app.route("/")
67 def index():
68 return None
69
70 @app.route("/", subdomain="foo")
71 def sub():
72 return None
73
74 with app.test_request_context("/"):
75 assert (
76 flask.url_for("index", _external=True)
77 == "http://localhost.localdomain:5000/"
78 )
79
80 with app.test_request_context("/"):
81 assert (
82 flask.url_for("sub", _external=True)
83 == "http://foo.localhost.localdomain:5000/"
84 )
85
86 # suppress Werkzeug 0.15 warning about name mismatch
87 with warnings.catch_warnings():
88 warnings.filterwarnings(
89 "ignore", "Current server name", UserWarning, "flask.app"
90 )
91 with app.test_request_context(
92 "/", environ_overrides={"HTTP_HOST": "localhost"}
93 ):
94 pass
95
96 app.config.update(SERVER_NAME="localhost")
97 with app.test_request_context("/", environ_overrides={"SERVER_NAME": "localhost"}):
98 pass
99
100 app.config.update(SERVER_NAME="localhost:80")
101 with app.test_request_context(
102 "/", environ_overrides={"SERVER_NAME": "localhost:80"}
103 ):
104 pass
105
106
107 def test_context_binding(app):
108 @app.route("/")
109 def index():
110 return f"Hello {flask.request.args['name']}!"
tests/test_reqctx.py 111
111
112 @app.route("/meh")
113 def meh():
114 return flask.request.url
115
116 with app.test_request_context("/?name=World"):
117 assert index() == "Hello World!"
118 with app.test_request_context("/meh"):
119 assert meh() == "http://localhost/meh"
120 assert not flask.request
121
122
123 def test_context_test(app):
124 assert not flask.request
125 assert not flask.has_request_context()
126 ctx = app.test_request_context()
127 ctx.push()
128 try:
129 assert flask.request
130 assert flask.has_request_context()
131 finally:
132 ctx.pop()
133
134
135 def test_manual_context_binding(app):
136 @app.route("/")
137 def index():
138 return f"Hello {flask.request.args['name']}!"
139
140 ctx = app.test_request_context("/?name=World")
141 ctx.push()
142 assert index() == "Hello World!"
143 ctx.pop()
144 with pytest.raises(RuntimeError):
145 index()
146
147
148 @pytest.mark.skipif(greenlet is None, reason="greenlet not installed")
149 class TestGreenletContextCopying:
150 def test_greenlet_context_copying(self, app, client):
151 greenlets = []
152
153 @app.route("/")
154 def index():
155 flask.session["fizz"] = "buzz"
156 reqctx = request_ctx.copy()
157
158 def g():
159 assert not flask.request
160 assert not flask.current_app
161 with reqctx:
162 assert flask.request
163 assert flask.current_app == app
164 assert flask.request.path == "/"
165 assert flask.request.args["foo"] == "bar"
166 assert flask.session.get("fizz") == "buzz"
167 assert not flask.request
168 return 42
169
170 greenlets.append(greenlet(g))
171 return "Hello World!"
172
173 rv = client.get("/?foo=bar")
174 assert rv.data == b"Hello World!"
175
176 result = greenlets[0].run()
177 assert result == 42
178
179 def test_greenlet_context_copying_api(self, app, client):
180 greenlets = []
181
tests/test_reqctx.py 112
182 @app.route("/")
183 def index():
184 flask.session["fizz"] = "buzz"
185
186 @flask.copy_current_request_context
187 def g():
188 assert flask.request
189 assert flask.current_app == app
190 assert flask.request.path == "/"
191 assert flask.request.args["foo"] == "bar"
192 assert flask.session.get("fizz") == "buzz"
193 return 42
194
195 greenlets.append(greenlet(g))
196 return "Hello World!"
197
198 rv = client.get("/?foo=bar")
199 assert rv.data == b"Hello World!"
200
201 result = greenlets[0].run()
202 assert result == 42
203
204
205 def test_session_error_pops_context():
206 class SessionError(Exception):
207 pass
208
209 class FailingSessionInterface(SessionInterface):
210 def open_session(self, app, request):
211 raise SessionError()
212
213 class CustomFlask(flask.Flask):
214 session_interface = FailingSessionInterface()
215
216 app = CustomFlask(__name__)
217
218 @app.route("/")
219 def index():
220 # shouldn't get here
221 AssertionError()
222
223 response = app.test_client().get("/")
224 assert response.status_code == 500
225 assert not flask.request
226 assert not flask.current_app
227
228
229 def test_session_dynamic_cookie_name():
230 # This session interface will use a cookie with a different name if the
231 # requested url ends with the string "dynamic_cookie"
232 class PathAwareSessionInterface(SecureCookieSessionInterface):
233 def get_cookie_name(self, app):
234 if flask.request.url.endswith("dynamic_cookie"):
235 return "dynamic_cookie_name"
236 else:
237 return super().get_cookie_name(app)
238
239 class CustomFlask(flask.Flask):
240 session_interface = PathAwareSessionInterface()
241
242 app = CustomFlask(__name__)
243 app.secret_key = "secret_key"
244
245 @app.route("/set", methods=["POST"])
246 def set():
247 flask.session["value"] = flask.request.form["value"]
248 return "value set"
249
250 @app.route("/get")
251 def get():
252 v = flask.session.get("value", "None")
tests/test_reqctx.py 113
253 return v
254
255 @app.route("/set_dynamic_cookie", methods=["POST"])
256 def set_dynamic_cookie():
257 flask.session["value"] = flask.request.form["value"]
258 return "value set"
259
260 @app.route("/get_dynamic_cookie")
261 def get_dynamic_cookie():
262 v = flask.session.get("value", "None")
263 return v
264
265 test_client = app.test_client()
266
267 # first set the cookie in both /set urls but each with a different value
268 assert test_client.post("/set", data={"value": "42"}).data == b"value set"
269 assert (
270 test_client.post("/set_dynamic_cookie", data={"value": "616"}).data
271 == b"value set"
272 )
273
274 # now check that the relevant values come back - meaning that different
275 # cookies are being used for the urls that end with "dynamic cookie"
276 assert test_client.get("/get").data == b"42"
277 assert test_client.get("/get_dynamic_cookie").data == b"616"
278
279
280 def test_bad_environ_raises_bad_request():
281 app = flask.Flask(__name__)
282
283 from flask.testing import EnvironBuilder
284
285 builder = EnvironBuilder(app)
286 environ = builder.get_environ()
287
288 # use a non-printable character in the Host - this is key to this test
289 environ["HTTP_HOST"] = "\x8a"
290
291 with app.request_context(environ):
292 response = app.full_dispatch_request()
293 assert response.status_code == 400
294
295
296 def test_environ_for_valid_idna_completes():
297 app = flask.Flask(__name__)
298
299 @app.route("/")
300 def index():
301 return "Hello World!"
302
303 from flask.testing import EnvironBuilder
304
305 builder = EnvironBuilder(app)
306 environ = builder.get_environ()
307
308 # these characters are all IDNA-compatible
309 environ["HTTP_HOST"] = "ąśźäü�Šß�.com"
310
311 with app.request_context(environ):
312 response = app.full_dispatch_request()
313
314 assert response.status_code == 200
315
316
317 def test_normal_environ_completes():
318 app = flask.Flask(__name__)
319
320 @app.route("/")
321 def index():
322 return "Hello World!"
323
114
tests/test_json.py
1 import datetime
2 import decimal
3 import io
4 import uuid
5
6 import pytest
7 from werkzeug.http import http_date
8
9 import flask
10 from flask import json
11 from flask.json.provider import DefaultJSONProvider
12
13
14 @pytest.mark.parametrize("debug", (True, False))
15 def test_bad_request_debug_message(app, client, debug):
16 app.config["DEBUG"] = debug
17 app.config["TRAP_BAD_REQUEST_ERRORS"] = False
18
19 @app.route("/json", methods=["POST"])
20 def post_json():
21 flask.request.get_json()
22 return None
23
24 rv = client.post("/json", data=None, content_type="application/json")
25 assert rv.status_code == 400
26 contains = b"Failed to decode JSON object" in rv.data
27 assert contains == debug
28
29
30 def test_json_bad_requests(app, client):
31 @app.route("/json", methods=["POST"])
32 def return_json():
33 return flask.jsonify(foo=str(flask.request.get_json()))
34
35 rv = client.post("/json", data="malformed", content_type="application/json")
36 assert rv.status_code == 400
37
38
39 def test_json_custom_mimetypes(app, client):
40 @app.route("/json", methods=["POST"])
41 def return_json():
42 return flask.request.get_json()
43
44 rv = client.post("/json", data='"foo"', content_type="application/x+json")
45 assert rv.data == b"foo"
46
47
48 @pytest.mark.parametrize(
49 "test_value,expected", [(True, '"\\u2603"'), (False, '"\u2603"')]
50 )
51 def test_json_as_unicode(test_value, expected, app, app_ctx):
52 app.json.ensure_ascii = test_value
53 rv = app.json.dumps("\N{SNOWMAN}")
54 assert rv == expected
55
56
57 def test_json_dump_to_file(app, app_ctx):
58 test_data = {"name": "Flask"}
59 out = io.StringIO()
60
61 flask.json.dump(test_data, out)
62 out.seek(0)
63 rv = flask.json.load(out)
64 assert rv == test_data
65
tests/test_json.py 115
66
67 @pytest.mark.parametrize(
68 "test_value", [0, -1, 1, 23, 3.14, "s", "longer string", True, False, None]
69 )
70 def test_jsonify_basic_types(test_value, app, client):
71 url = "/jsonify_basic_types"
72 app.add_url_rule(url, url, lambda x=test_value: flask.jsonify(x))
73 rv = client.get(url)
74 assert rv.mimetype == "application/json"
75 assert flask.json.loads(rv.data) == test_value
76
77
78 def test_jsonify_dicts(app, client):
79 d = {
80 "a": 0,
81 "b": 23,
82 "c": 3.14,
83 "d": "t",
84 "e": "Hi",
85 "f": True,
86 "g": False,
87 "h": ["test list", 10, False],
88 "i": {"test": "dict"},
89 }
90
91 @app.route("/kw")
92 def return_kwargs():
93 return flask.jsonify(**d)
94
95 @app.route("/dict")
96 def return_dict():
97 return flask.jsonify(d)
98
99 for url in "/kw", "/dict":
100 rv = client.get(url)
101 assert rv.mimetype == "application/json"
102 assert flask.json.loads(rv.data) == d
103
104
105 def test_jsonify_arrays(app, client):
106 """Test jsonify of lists and args unpacking."""
107 a_list = [
108 0,
109 42,
110 3.14,
111 "t",
112 "hello",
113 True,
114 False,
115 ["test list", 2, False],
116 {"test": "dict"},
117 ]
118
119 @app.route("/args_unpack")
120 def return_args_unpack():
121 return flask.jsonify(*a_list)
122
123 @app.route("/array")
124 def return_array():
125 return flask.jsonify(a_list)
126
127 for url in "/args_unpack", "/array":
128 rv = client.get(url)
129 assert rv.mimetype == "application/json"
130 assert flask.json.loads(rv.data) == a_list
131
132
133 @pytest.mark.parametrize(
134 "value", [datetime.datetime(1973, 3, 11, 6, 30, 45), datetime.date(1975, 1, 5)]
135 )
136 def test_jsonify_datetime(app, client, value):
tests/test_json.py 116
137 @app.route("/")
138 def index():
139 return flask.jsonify(value=value)
140
141 r = client.get()
142 assert r.json["value"] == http_date(value)
143
144
145 class FixedOffset(datetime.tzinfo):
146 """Fixed offset in hours east from UTC.
147
148 This is a slight adaptation of the ``FixedOffset`` example found in
149 https://docs.python.org/2.7/library/datetime.html.
150 """
151
152 def __init__(self, hours, name):
153 self.__offset = datetime.timedelta(hours=hours)
154 self.__name = name
155
156 def utcoffset(self, dt):
157 return self.__offset
158
159 def tzname(self, dt):
160 return self.__name
161
162 def dst(self, dt):
163 return datetime.timedelta()
164
165
166 @pytest.mark.parametrize("tz", (("UTC", 0), ("PST", -8), ("KST", 9)))
167 def test_jsonify_aware_datetimes(tz):
168 """Test if aware datetime.datetime objects are converted into GMT."""
169 tzinfo = FixedOffset(hours=tz[1], name=tz[0])
170 dt = datetime.datetime(2017, 1, 1, 12, 34, 56, tzinfo=tzinfo)
171 gmt = FixedOffset(hours=0, name="GMT")
172 expected = dt.astimezone(gmt).strftime('"%a, %d %b %Y %H:%M:%S %Z"')
173 assert flask.json.dumps(dt) == expected
174
175
176 def test_jsonify_uuid_types(app, client):
177 """Test jsonify with uuid.UUID types"""
178
179 test_uuid = uuid.UUID(bytes=b"\xde\xad\xbe\xef" * 4)
180 url = "/uuid_test"
181 app.add_url_rule(url, url, lambda: flask.jsonify(x=test_uuid))
182
183 rv = client.get(url)
184
185 rv_x = flask.json.loads(rv.data)["x"]
186 assert rv_x == str(test_uuid)
187 rv_uuid = uuid.UUID(rv_x)
188 assert rv_uuid == test_uuid
189
190
191 def test_json_decimal():
192 rv = flask.json.dumps(decimal.Decimal("0.003"))
193 assert rv == '"0.003"'
194
195
196 def test_json_attr(app, client):
197 @app.route("/add", methods=["POST"])
198 def add():
199 json = flask.request.get_json()
200 return str(json["a"] + json["b"])
201
202 rv = client.post(
203 "/add",
204 data=flask.json.dumps({"a": 1, "b": 2}),
205 content_type="application/json",
206 )
207 assert rv.data == b"3"
tests/test_json.py 117
208
209
210 def test_tojson_filter(app, req_ctx):
211 # The tojson filter is tested in Jinja, this confirms that it's
212 # using Flask's dumps.
213 rv = flask.render_template_string(
214 "const data = {{ data|tojson }};",
215 data={"name": "</script>", "time": datetime.datetime(2021, 2, 1, 7, 15)},
216 )
217 assert rv == (
218 'const data = {"name": "\\u003c/script\\u003e",'
219 ' "time": "Mon, 01 Feb 2021 07:15:00 GMT"};'
220 )
221
222
223 def test_json_customization(app, client):
224 class X: # noqa: B903, for Python2 compatibility
225 def __init__(self, val):
226 self.val = val
227
228 def default(o):
229 if isinstance(o, X):
230 return f"<{o.val}>"
231
232 return DefaultJSONProvider.default(o)
233
234 class CustomProvider(DefaultJSONProvider):
235 def object_hook(self, obj):
236 if len(obj) == 1 and "_foo" in obj:
237 return X(obj["_foo"])
238
239 return obj
240
241 def loads(self, s, **kwargs):
242 kwargs.setdefault("object_hook", self.object_hook)
243 return super().loads(s, **kwargs)
244
245 app.json = CustomProvider(app)
246 app.json.default = default
247
248 @app.route("/", methods=["POST"])
249 def index():
250 return flask.json.dumps(flask.request.get_json()["x"])
251
252 rv = client.post(
253 "/",
254 data=flask.json.dumps({"x": {"_foo": 42}}),
255 content_type="application/json",
256 )
257 assert rv.data == b'"<42>"'
258
259
260 def _has_encoding(name):
261 try:
262 import codecs
263
264 codecs.lookup(name)
265 return True
266 except LookupError:
267 return False
268
269
270 def test_json_key_sorting(app, client):
271 app.debug = True
272 assert app.json.sort_keys
273 d = dict.fromkeys(range(20), "foo")
274
275 @app.route("/")
276 def index():
277 return flask.jsonify(values=d)
278
118
279 rv = client.get("/")
280 lines = [x.strip() for x in rv.data.strip().decode("utf-8").splitlines()]
281 sorted_by_str = [
282 "{",
283 '"values": {',
284 '"0": "foo",',
285 '"1": "foo",',
286 '"10": "foo",',
287 '"11": "foo",',
288 '"12": "foo",',
289 '"13": "foo",',
290 '"14": "foo",',
291 '"15": "foo",',
292 '"16": "foo",',
293 '"17": "foo",',
294 '"18": "foo",',
295 '"19": "foo",',
296 '"2": "foo",',
297 '"3": "foo",',
298 '"4": "foo",',
299 '"5": "foo",',
300 '"6": "foo",',
301 '"7": "foo",',
302 '"8": "foo",',
303 '"9": "foo"',
304 "}",
305 "}",
306 ]
307 sorted_by_int = [
308 "{",
309 '"values": {',
310 '"0": "foo",',
311 '"1": "foo",',
312 '"2": "foo",',
313 '"3": "foo",',
314 '"4": "foo",',
315 '"5": "foo",',
316 '"6": "foo",',
317 '"7": "foo",',
318 '"8": "foo",',
319 '"9": "foo",',
320 '"10": "foo",',
321 '"11": "foo",',
322 '"12": "foo",',
323 '"13": "foo",',
324 '"14": "foo",',
325 '"15": "foo",',
326 '"16": "foo",',
327 '"17": "foo",',
328 '"18": "foo",',
329 '"19": "foo"',
330 "}",
331 "}",
332 ]
333
334 try:
335 assert lines == sorted_by_int
336 except AssertionError:
337 assert lines == sorted_by_str
338
339
340 def test_html_method():
341 class ObjectWithHTML:
342 def __html__(self):
343 return "<p>test</p>"
344
345 result = json.dumps(ObjectWithHTML())
346 assert result == '"<p>test</p>"'
tests/test_helpers.py 119
tests/test_helpers.py
1 import io
2 import os
3
4 import pytest
5 import werkzeug.exceptions
6
7 import flask
8 from flask.helpers import get_debug_flag
9
10
11 class FakePath:
12 """Fake object to represent a ``PathLike object``.
13
14 This represents a ``pathlib.Path`` object in python 3.
15 See: https://www.python.org/dev/peps/pep-0519/
16 """
17
18 def __init__(self, path):
19 self.path = path
20
21 def __fspath__(self):
22 return self.path
23
24
25 class PyBytesIO:
26 def __init__(self, *args, **kwargs):
27 self._io = io.BytesIO(*args, **kwargs)
28
29 def __getattr__(self, name):
30 return getattr(self._io, name)
31
32
33 class TestSendfile:
34 def test_send_file(self, app, req_ctx):
35 rv = flask.send_file("static/index.html")
36 assert rv.direct_passthrough
37 assert rv.mimetype == "text/html"
38
39 with app.open_resource("static/index.html") as f:
40 rv.direct_passthrough = False
41 assert rv.data == f.read()
42
43 rv.close()
44
45 def test_static_file(self, app, req_ctx):
46 # Default max_age is None.
47
48 # Test with static file handler.
49 rv = app.send_static_file("index.html")
50 assert rv.cache_control.max_age is None
51 rv.close()
52
53 # Test with direct use of send_file.
54 rv = flask.send_file("static/index.html")
55 assert rv.cache_control.max_age is None
56 rv.close()
57
58 app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 3600
59
60 # Test with static file handler.
61 rv = app.send_static_file("index.html")
62 assert rv.cache_control.max_age == 3600
63 rv.close()
64
65 # Test with direct use of send_file.
66 rv = flask.send_file("static/index.html")
67 assert rv.cache_control.max_age == 3600
68 rv.close()
69
tests/test_helpers.py 120
212
213 with app.app_context(), pytest.raises(My900Error):
214 flask.abort(900)
215
216
217 class TestNoImports:
218 """Test Flasks are created without import.
219
220 Avoiding ``__import__`` helps create Flask instances where there are errors
221 at import time. Those runtime errors will be apparent to the user soon
222 enough, but tools which build Flask instances meta-programmatically benefit
223 from a Flask which does not ``__import__``. Instead of importing to
224 retrieve file paths or metadata on a module or package, use the pkgutil and
225 imp modules in the Python standard library.
226 """
227
228 def test_name_with_import_error(self, modules_tmp_path):
229 (modules_tmp_path / "importerror.py").write_text("raise NotImplementedError()")
230 try:
231 flask.Flask("importerror")
232 except NotImplementedError:
233 AssertionError("Flask(import_name) is importing import_name.")
234
235
236 class TestStreaming:
237 def test_streaming_with_context(self, app, client):
238 @app.route("/")
239 def index():
240 def generate():
241 yield "Hello "
242 yield flask.request.args["name"]
243 yield "!"
244
245 return flask.Response(flask.stream_with_context(generate()))
246
247 rv = client.get("/?name=World")
248 assert rv.data == b"Hello World!"
249
250 def test_streaming_with_context_as_decorator(self, app, client):
251 @app.route("/")
252 def index():
253 @flask.stream_with_context
254 def generate(hello):
255 yield hello
256 yield flask.request.args["name"]
257 yield "!"
258
259 return flask.Response(generate("Hello "))
260
261 rv = client.get("/?name=World")
262 assert rv.data == b"Hello World!"
263
264 def test_streaming_with_context_and_custom_close(self, app, client):
265 called = []
266
267 class Wrapper:
268 def __init__(self, gen):
269 self._gen = gen
270
271 def __iter__(self):
272 return self
273
274 def close(self):
275 called.append(42)
276
277 def __next__(self):
278 return next(self._gen)
279
280 next = __next__
281
282 @app.route("/")
tests/test_helpers.py 123
tests/test_testing.py
1 import importlib.metadata
2
3 import click
4 import pytest
5
6 import flask
7 from flask import appcontext_popped
8 from flask.cli import ScriptInfo
9 from flask.globals import _cv_request
10 from flask.json import jsonify
11 from flask.testing import EnvironBuilder
12 from flask.testing import FlaskCliRunner
13
14
15 def test_environ_defaults_from_config(app, client):
16 app.config["SERVER_NAME"] = "example.com:1234"
17 app.config["APPLICATION_ROOT"] = "/foo"
18
19 @app.route("/")
20 def index():
21 return flask.request.url
22
23 ctx = app.test_request_context()
24 assert ctx.request.url == "http://example.com:1234/foo/"
25
26 rv = client.get("/")
27 assert rv.data == b"http://example.com:1234/foo/"
28
29
30 def test_environ_defaults(app, client, app_ctx, req_ctx):
31 @app.route("/")
32 def index():
33 return flask.request.url
34
35 ctx = app.test_request_context()
36 assert ctx.request.url == "http://localhost/"
37 with client:
38 rv = client.get("/")
39 assert rv.data == b"http://localhost/"
40
41
42 def test_environ_base_default(app, client):
43 @app.route("/")
44 def index():
45 flask.g.remote_addr = flask.request.remote_addr
46 flask.g.user_agent = flask.request.user_agent.string
47 return ""
48
49 with client:
50 client.get("/")
51 assert flask.g.remote_addr == "127.0.0.1"
52 assert flask.g.user_agent == (
53 f"Werkzeug/{importlib.metadata.version('werkzeug')}"
54 )
55
56
57 def test_environ_base_modified(app, client):
58 @app.route("/")
59 def index():
60 flask.g.remote_addr = flask.request.remote_addr
tests/test_testing.py 125
61 flask.g.user_agent = flask.request.user_agent.string
62 return ""
63
64 client.environ_base["REMOTE_ADDR"] = "192.168.0.22"
65 client.environ_base["HTTP_USER_AGENT"] = "Foo"
66
67 with client:
68 client.get("/")
69 assert flask.g.remote_addr == "192.168.0.22"
70 assert flask.g.user_agent == "Foo"
71
72
73 def test_client_open_environ(app, client, request):
74 @app.route("/index")
75 def index():
76 return flask.request.remote_addr
77
78 builder = EnvironBuilder(app, path="/index", method="GET")
79 request.addfinalizer(builder.close)
80
81 rv = client.open(builder)
82 assert rv.data == b"127.0.0.1"
83
84 environ = builder.get_environ()
85 client.environ_base["REMOTE_ADDR"] = "127.0.0.2"
86 rv = client.open(environ)
87 assert rv.data == b"127.0.0.2"
88
89
90 def test_specify_url_scheme(app, client):
91 @app.route("/")
92 def index():
93 return flask.request.url
94
95 ctx = app.test_request_context(url_scheme="https")
96 assert ctx.request.url == "https://localhost/"
97
98 rv = client.get("/", url_scheme="https")
99 assert rv.data == b"https://localhost/"
100
101
102 def test_path_is_url(app):
103 eb = EnvironBuilder(app, "https://example.com/")
104 assert eb.url_scheme == "https"
105 assert eb.host == "example.com"
106 assert eb.script_root == ""
107 assert eb.path == "/"
108
109
110 def test_environbuilder_json_dumps(app):
111 """EnvironBuilder.json_dumps() takes settings from the app."""
112 app.json.ensure_ascii = False
113 eb = EnvironBuilder(app, json="\u20ac")
114 assert eb.input_stream.read().decode("utf8") == '"\u20ac"'
115
116
117 def test_blueprint_with_subdomain():
118 app = flask.Flask(__name__, subdomain_matching=True)
119 app.config["SERVER_NAME"] = "example.com:1234"
120 app.config["APPLICATION_ROOT"] = "/foo"
121 client = app.test_client()
122
123 bp = flask.Blueprint("company", __name__, subdomain="xxx")
124
125 @bp.route("/")
126 def index():
127 return flask.request.url
128
129 app.register_blueprint(bp)
130
131 ctx = app.test_request_context("/", subdomain="xxx")
tests/test_testing.py 126
203
204 def test_session_transaction_needs_cookies(app):
205 c = app.test_client(use_cookies=False)
206
207 with pytest.raises(TypeError, match="Cookies are disabled."):
208 with c.session_transaction():
209 pass
210
211
212 def test_test_client_context_binding(app, client):
213 app.testing = False
214
215 @app.route("/")
216 def index():
217 flask.g.value = 42
218 return "Hello World!"
219
220 @app.route("/other")
221 def other():
222 raise ZeroDivisionError
223
224 with client:
225 resp = client.get("/")
226 assert flask.g.value == 42
227 assert resp.data == b"Hello World!"
228 assert resp.status_code == 200
229
230 with client:
231 resp = client.get("/other")
232 assert not hasattr(flask.g, "value")
233 assert b"Internal Server Error" in resp.data
234 assert resp.status_code == 500
235 flask.g.value = 23
236
237 with pytest.raises(RuntimeError):
238 flask.g.value # noqa: B018
239
240
241 def test_reuse_client(client):
242 c = client
243
244 with c:
245 assert client.get("/").status_code == 404
246
247 with c:
248 assert client.get("/").status_code == 404
249
250
251 def test_full_url_request(app, client):
252 @app.route("/action", methods=["POST"])
253 def action():
254 return "x"
255
256 with client:
257 rv = client.post("http://domain.com/action?vodka=42", data={"gin": 43})
258 assert rv.status_code == 200
259 assert "gin" in flask.request.form
260 assert "vodka" in flask.request.args
261
262
263 def test_json_request_and_response(app, client):
264 @app.route("/echo", methods=["POST"])
265 def echo():
266 return jsonify(flask.request.get_json())
267
268 with client:
269 json_data = {"drink": {"gin": 1, "tonic": True}, "price": 10}
270 rv = client.post("/echo", json=json_data)
271
272 # Request should be in JSON
273 assert flask.request.is_json
tests/test_testing.py 128
tests/test_templating.py
1 import logging
2
3 import pytest
4 import werkzeug.serving
5 from jinja2 import TemplateNotFound
6 from markupsafe import Markup
7
8 import flask
9
10
11 def test_context_processing(app, client):
12 @app.context_processor
13 def context_processor():
14 return {"injected_value": 42}
15
tests/test_templating.py 130
16 @app.route("/")
17 def index():
18 return flask.render_template("context_template.html", value=23)
19
20 rv = client.get("/")
21 assert rv.data == b"<p>23|42"
22
23
24 def test_original_win(app, client):
25 @app.route("/")
26 def index():
27 return flask.render_template_string("{{ config }}", config=42)
28
29 rv = client.get("/")
30 assert rv.data == b"42"
31
32
33 def test_simple_stream(app, client):
34 @app.route("/")
35 def index():
36 return flask.stream_template_string("{{ config }}", config=42)
37
38 rv = client.get("/")
39 assert rv.data == b"42"
40
41
42 def test_request_less_rendering(app, app_ctx):
43 app.config["WORLD_NAME"] = "Special World"
44
45 @app.context_processor
46 def context_processor():
47 return dict(foo=42)
48
49 rv = flask.render_template_string("Hello {{ config.WORLD_NAME }} {{ foo }}")
50 assert rv == "Hello Special World 42"
51
52
53 def test_standard_context(app, client):
54 @app.route("/")
55 def index():
56 flask.g.foo = 23
57 flask.session["test"] = "aha"
58 return flask.render_template_string(
59 """
60 {{ request.args.foo }}
61 {{ g.foo }}
62 {{ config.DEBUG }}
63 {{ session.test }}
64 """
65 )
66
67 rv = client.get("/?foo=42")
68 assert rv.data.split() == [b"42", b"23", b"False", b"aha"]
69
70
71 def test_escaping(app, client):
72 text = "<p>Hello World!"
73
74 @app.route("/")
75 def index():
76 return flask.render_template(
77 "escaping_template.html", text=text, html=Markup(text)
78 )
79
80 lines = client.get("/").data.splitlines()
81 assert lines == [
82 b"<p>Hello World!",
83 b"<p>Hello World!",
84 b"<p>Hello World!",
85 b"<p>Hello World!",
86 b"<p>Hello World!",
tests/test_templating.py 131
87 b"<p>Hello World!",
88 ]
89
90
91 def test_no_escaping(app, client):
92 text = "<p>Hello World!"
93
94 @app.route("/")
95 def index():
96 return flask.render_template(
97 "non_escaping_template.txt", text=text, html=Markup(text)
98 )
99
100 lines = client.get("/").data.splitlines()
101 assert lines == [
102 b"<p>Hello World!",
103 b"<p>Hello World!",
104 b"<p>Hello World!",
105 b"<p>Hello World!",
106 b"<p>Hello World!",
107 b"<p>Hello World!",
108 b"<p>Hello World!",
109 b"<p>Hello World!",
110 ]
111
112
113 def test_escaping_without_template_filename(app, client, req_ctx):
114 assert flask.render_template_string("{{ foo }}", foo="<test>") == "<test>"
115 assert flask.render_template("mail.txt", foo="<test>") == "<test> Mail"
116
117
118 def test_macros(app, req_ctx):
119 macro = flask.get_template_attribute("_macro.html", "hello")
120 assert macro("World") == "Hello World!"
121
122
123 def test_template_filter(app):
124 @app.template_filter()
125 def my_reverse(s):
126 return s[::-1]
127
128 assert "my_reverse" in app.jinja_env.filters.keys()
129 assert app.jinja_env.filters["my_reverse"] == my_reverse
130 assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba"
131
132
133 def test_add_template_filter(app):
134 def my_reverse(s):
135 return s[::-1]
136
137 app.add_template_filter(my_reverse)
138 assert "my_reverse" in app.jinja_env.filters.keys()
139 assert app.jinja_env.filters["my_reverse"] == my_reverse
140 assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba"
141
142
143 def test_template_filter_with_name(app):
144 @app.template_filter("strrev")
145 def my_reverse(s):
146 return s[::-1]
147
148 assert "strrev" in app.jinja_env.filters.keys()
149 assert app.jinja_env.filters["strrev"] == my_reverse
150 assert app.jinja_env.filters["strrev"]("abcd") == "dcba"
151
152
153 def test_add_template_filter_with_name(app):
154 def my_reverse(s):
155 return s[::-1]
156
157 app.add_template_filter(my_reverse, "strrev")
tests/test_templating.py 132
300
301 app.add_template_test(is_boolean, "boolean")
302
303 @app.route("/")
304 def index():
305 return flask.render_template("template_test.html", value=False)
306
307 rv = client.get("/")
308 assert b"Success!" in rv.data
309
310
311 def test_add_template_global(app, app_ctx):
312 @app.template_global()
313 def get_stuff():
314 return 42
315
316 assert "get_stuff" in app.jinja_env.globals.keys()
317 assert app.jinja_env.globals["get_stuff"] == get_stuff
318 assert app.jinja_env.globals["get_stuff"](), 42
319
320 rv = flask.render_template_string("{{ get_stuff() }}")
321 assert rv == "42"
322
323
324 def test_custom_template_loader(client):
325 class MyFlask(flask.Flask):
326 def create_global_jinja_loader(self):
327 from jinja2 import DictLoader
328
329 return DictLoader({"index.html": "Hello Custom World!"})
330
331 app = MyFlask(__name__)
332
333 @app.route("/")
334 def index():
335 return flask.render_template("index.html")
336
337 c = app.test_client()
338 rv = c.get("/")
339 assert rv.data == b"Hello Custom World!"
340
341
342 def test_iterable_loader(app, client):
343 @app.context_processor
344 def context_processor():
345 return {"whiskey": "Jameson"}
346
347 @app.route("/")
348 def index():
349 return flask.render_template(
350 [
351 "no_template.xml", # should skip this one
352 "simple_template.html", # should render this
353 "context_template.html",
354 ],
355 value=23,
356 )
357
358 rv = client.get("/")
359 assert rv.data == b"<h1>Jameson</h1>"
360
361
362 def test_templates_auto_reload(app):
363 # debug is False, config option is None
364 assert app.debug is False
365 assert app.config["TEMPLATES_AUTO_RELOAD"] is None
366 assert app.jinja_env.auto_reload is False
367 # debug is False, config option is False
368 app = flask.Flask(__name__)
369 app.config["TEMPLATES_AUTO_RELOAD"] = False
370 assert app.debug is False
tests/test_templating.py 135
442
443 def test_custom_jinja_env():
444 class CustomEnvironment(flask.templating.Environment):
445 pass
446
447 class CustomFlask(flask.Flask):
448 jinja_environment = CustomEnvironment
449
450 app = CustomFlask(__name__)
451 assert isinstance(app.jinja_env, CustomEnvironment)
tests/test_cli.py
1 # This file was part of Flask-CLI and was modified under the terms of
2 # its Revised BSD License. Copyright © 2015 CERN.
3 import importlib.metadata
4 import os
5 import platform
6 import ssl
7 import sys
8 import types
9 from functools import partial
10 from pathlib import Path
11
12 import click
13 import pytest
14 from _pytest.monkeypatch import notset
15 from click.testing import CliRunner
16
17 from flask import Blueprint
18 from flask import current_app
19 from flask import Flask
20 from flask.cli import AppGroup
21 from flask.cli import find_best_app
22 from flask.cli import FlaskGroup
23 from flask.cli import get_version
24 from flask.cli import load_dotenv
25 from flask.cli import locate_app
26 from flask.cli import NoAppException
27 from flask.cli import prepare_import
28 from flask.cli import run_command
29 from flask.cli import ScriptInfo
30 from flask.cli import with_appcontext
31
32 cwd = Path.cwd()
33 test_path = (Path(__file__) / ".." / "test_apps").resolve()
34
35
36 @pytest.fixture
37 def runner():
38 return CliRunner()
39
40
41 def test_cli_name(test_apps):
42 """Make sure the CLI object's name is the app's name and not the app itself"""
43 from cliapp.app import testapp
44
45 assert testapp.cli.name == testapp.name
46
47
48 def test_find_best_app(test_apps):
49 class Module:
50 app = Flask("appname")
51
52 assert find_best_app(Module) == Module.app
53
54 class Module:
55 application = Flask("appname")
56
57 assert find_best_app(Module) == Module.application
tests/test_cli.py 137
58
59 class Module:
60 myapp = Flask("appname")
61
62 assert find_best_app(Module) == Module.myapp
63
64 class Module:
65 @staticmethod
66 def create_app():
67 return Flask("appname")
68
69 app = find_best_app(Module)
70 assert isinstance(app, Flask)
71 assert app.name == "appname"
72
73 class Module:
74 @staticmethod
75 def create_app(**kwargs):
76 return Flask("appname")
77
78 app = find_best_app(Module)
79 assert isinstance(app, Flask)
80 assert app.name == "appname"
81
82 class Module:
83 @staticmethod
84 def make_app():
85 return Flask("appname")
86
87 app = find_best_app(Module)
88 assert isinstance(app, Flask)
89 assert app.name == "appname"
90
91 class Module:
92 myapp = Flask("appname1")
93
94 @staticmethod
95 def create_app():
96 return Flask("appname2")
97
98 assert find_best_app(Module) == Module.myapp
99
100 class Module:
101 myapp = Flask("appname1")
102
103 @staticmethod
104 def create_app():
105 return Flask("appname2")
106
107 assert find_best_app(Module) == Module.myapp
108
109 class Module:
110 pass
111
112 pytest.raises(NoAppException, find_best_app, Module)
113
114 class Module:
115 myapp1 = Flask("appname1")
116 myapp2 = Flask("appname2")
117
118 pytest.raises(NoAppException, find_best_app, Module)
119
120 class Module:
121 @staticmethod
122 def create_app(foo, bar):
123 return Flask("appname2")
124
125 pytest.raises(NoAppException, find_best_app, Module)
126
127 class Module:
128 @staticmethod
tests/test_cli.py 138
200 "iname,aname",
201 (
202 ("notanapp.py", None),
203 ("cliapp/app", None),
204 ("cliapp.app", "notanapp"),
205 # not enough arguments
206 ("cliapp.factory", 'create_app2("foo")'),
207 # invalid identifier
208 ("cliapp.factory", "create_app("),
209 # no app returned
210 ("cliapp.factory", "no_app"),
211 # nested import error
212 ("cliapp.importerrorapp", None),
213 # not a Python file
214 ("cliapp.message.txt", None),
215 ),
216 )
217 def test_locate_app_raises(test_apps, iname, aname):
218 with pytest.raises(NoAppException):
219 locate_app(iname, aname)
220
221
222 def test_locate_app_suppress_raise(test_apps):
223 app = locate_app("notanapp.py", None, raise_if_not_found=False)
224 assert app is None
225
226 # only direct import error is suppressed
227 with pytest.raises(NoAppException):
228 locate_app("cliapp.importerrorapp", None, raise_if_not_found=False)
229
230
231 def test_get_version(test_apps, capsys):
232 class MockCtx:
233 resilient_parsing = False
234 color = None
235
236 def exit(self):
237 return
238
239 ctx = MockCtx()
240 get_version(ctx, None, "test")
241 out, err = capsys.readouterr()
242 assert f"Python {platform.python_version()}" in out
243 assert f"Flask {importlib.metadata.version('flask')}" in out
244 assert f"Werkzeug {importlib.metadata.version('werkzeug')}" in out
245
246
247 def test_scriptinfo(test_apps, monkeypatch):
248 obj = ScriptInfo(app_import_path="cliapp.app:testapp")
249 app = obj.load_app()
250 assert app.name == "testapp"
251 assert obj.load_app() is app
252
253 # import app with module's absolute path
254 cli_app_path = str(test_path / "cliapp" / "app.py")
255
256 obj = ScriptInfo(app_import_path=cli_app_path)
257 app = obj.load_app()
258 assert app.name == "testapp"
259 assert obj.load_app() is app
260 obj = ScriptInfo(app_import_path=f"{cli_app_path}:testapp")
261 app = obj.load_app()
262 assert app.name == "testapp"
263 assert obj.load_app() is app
264
265 def create_app():
266 return Flask("createapp")
267
268 obj = ScriptInfo(create_app=create_app)
269 app = obj.load_app()
270 assert app.name == "createapp"
tests/test_cli.py 140
342
343 result = runner.invoke(cli, ["subgroup", "test2"], obj=obj)
344 assert result.exit_code == 0
345 assert result.output == "testappgroup\n"
346
347
348 def test_flaskgroup_app_context(runner):
349 def create_app():
350 return Flask("flaskgroup")
351
352 @click.group(cls=FlaskGroup, create_app=create_app)
353 def cli(**params):
354 pass
355
356 @cli.command()
357 def test():
358 click.echo(current_app.name)
359
360 result = runner.invoke(cli, ["test"])
361 assert result.exit_code == 0
362 assert result.output == "flaskgroup\n"
363
364
365 @pytest.mark.parametrize("set_debug_flag", (True, False))
366 def test_flaskgroup_debug(runner, set_debug_flag):
367 def create_app():
368 app = Flask("flaskgroup")
369 app.debug = True
370 return app
371
372 @click.group(cls=FlaskGroup, create_app=create_app, set_debug_flag=set_debug_flag)
373 def cli(**params):
374 pass
375
376 @cli.command()
377 def test():
378 click.echo(str(current_app.debug))
379
380 result = runner.invoke(cli, ["test"])
381 assert result.exit_code == 0
382 assert result.output == f"{not set_debug_flag}\n"
383
384
385 def test_flaskgroup_nested(app, runner):
386 cli = click.Group("cli")
387 flask_group = FlaskGroup(name="flask", create_app=lambda: app)
388 cli.add_command(flask_group)
389
390 @flask_group.command()
391 def show():
392 click.echo(current_app.name)
393
394 result = runner.invoke(cli, ["flask", "show"])
395 assert result.output == "flask_test\n"
396
397
398 def test_no_command_echo_loading_error():
399 from flask.cli import cli
400
401 try:
402 runner = CliRunner(mix_stderr=False)
403 except (DeprecationWarning, TypeError):
404 # Click >= 8.2
405 runner = CliRunner()
406
407 result = runner.invoke(cli, ["missing"])
408 assert result.exit_code == 2
409 assert "FLASK_APP" in result.stderr
410 assert "Usage:" in result.stderr
411
412
tests/test_cli.py 142
tests/test_blueprints.py
1 import pytest
2 from jinja2 import TemplateNotFound
3 from werkzeug.http import parse_cache_control_header
4
5 import flask
6
7
8 def test_blueprint_specific_error_handling(app, client):
9 frontend = flask.Blueprint("frontend", __name__)
10 backend = flask.Blueprint("backend", __name__)
11 sideend = flask.Blueprint("sideend", __name__)
12
13 @frontend.errorhandler(403)
14 def frontend_forbidden(e):
15 return "frontend says no", 403
16
17 @frontend.route("/frontend-no")
18 def frontend_no():
19 flask.abort(403)
20
21 @backend.errorhandler(403)
22 def backend_forbidden(e):
23 return "backend says no", 403
24
25 @backend.route("/backend-no")
26 def backend_no():
27 flask.abort(403)
28
29 @sideend.route("/what-is-a-sideend")
30 def sideend_no():
31 flask.abort(403)
32
33 app.register_blueprint(frontend)
34 app.register_blueprint(backend)
35 app.register_blueprint(sideend)
36
37 @app.errorhandler(403)
38 def app_forbidden(e):
39 return "application itself says no", 403
40
41 assert client.get("/frontend-no").data == b"frontend says no"
42 assert client.get("/backend-no").data == b"backend says no"
43 assert client.get("/what-is-a-sideend").data == b"application itself says no"
44
45
46 def test_blueprint_specific_user_error_handling(app, client):
47 class MyDecoratorException(Exception):
48 pass
49
50 class MyFunctionException(Exception):
51 pass
52
53 blue = flask.Blueprint("blue", __name__)
54
55 @blue.errorhandler(MyDecoratorException)
56 def my_decorator_exception_handler(e):
57 assert isinstance(e, MyDecoratorException)
58 return "boom"
59
60 def my_function_exception_handler(e):
61 assert isinstance(e, MyFunctionException)
tests/test_blueprints.py 147
62 return "bam"
63
64 blue.register_error_handler(MyFunctionException, my_function_exception_handler)
65
66 @blue.route("/decorator")
67 def blue_deco_test():
68 raise MyDecoratorException()
69
70 @blue.route("/function")
71 def blue_func_test():
72 raise MyFunctionException()
73
74 app.register_blueprint(blue)
75
76 assert client.get("/decorator").data == b"boom"
77 assert client.get("/function").data == b"bam"
78
79
80 def test_blueprint_app_error_handling(app, client):
81 errors = flask.Blueprint("errors", __name__)
82
83 @errors.app_errorhandler(403)
84 def forbidden_handler(e):
85 return "you shall not pass", 403
86
87 @app.route("/forbidden")
88 def app_forbidden():
89 flask.abort(403)
90
91 forbidden_bp = flask.Blueprint("forbidden_bp", __name__)
92
93 @forbidden_bp.route("/nope")
94 def bp_forbidden():
95 flask.abort(403)
96
97 app.register_blueprint(errors)
98 app.register_blueprint(forbidden_bp)
99
100 assert client.get("/forbidden").data == b"you shall not pass"
101 assert client.get("/nope").data == b"you shall not pass"
102
103
104 @pytest.mark.parametrize(
105 ("prefix", "rule", "url"),
106 (
107 ("", "/", "/"),
108 ("/", "", "/"),
109 ("/", "/", "/"),
110 ("/foo", "", "/foo"),
111 ("/foo/", "", "/foo/"),
112 ("", "/bar", "/bar"),
113 ("/foo/", "/bar", "/foo/bar"),
114 ("/foo/", "bar", "/foo/bar"),
115 ("/foo", "/bar", "/foo/bar"),
116 ("/foo/", "//bar", "/foo/bar"),
117 ("/foo//", "/bar", "/foo/bar"),
118 ),
119 )
120 def test_blueprint_prefix_slash(app, client, prefix, rule, url):
121 bp = flask.Blueprint("test", __name__, url_prefix=prefix)
122
123 @bp.route(rule)
124 def index():
125 return "", 204
126
127 app.register_blueprint(bp)
128 assert client.get(url).status_code == 204
129
130
131 def test_blueprint_url_defaults(app, client):
132 bp = flask.Blueprint("test", __name__)
tests/test_blueprints.py 148
133
134 @bp.route("/foo", defaults={"baz": 42})
135 def foo(bar, baz):
136 return f"{bar}/{baz:d}"
137
138 @bp.route("/bar")
139 def bar(bar):
140 return str(bar)
141
142 app.register_blueprint(bp, url_prefix="/1", url_defaults={"bar": 23})
143 app.register_blueprint(bp, name="test2", url_prefix="/2", url_defaults={"bar": 19})
144
145 assert client.get("/1/foo").data == b"23/42"
146 assert client.get("/2/foo").data == b"19/42"
147 assert client.get("/1/bar").data == b"23"
148 assert client.get("/2/bar").data == b"19"
149
150
151 def test_blueprint_url_processors(app, client):
152 bp = flask.Blueprint("frontend", __name__, url_prefix="/<lang_code>")
153
154 @bp.url_defaults
155 def add_language_code(endpoint, values):
156 values.setdefault("lang_code", flask.g.lang_code)
157
158 @bp.url_value_preprocessor
159 def pull_lang_code(endpoint, values):
160 flask.g.lang_code = values.pop("lang_code")
161
162 @bp.route("/")
163 def index():
164 return flask.url_for(".about")
165
166 @bp.route("/about")
167 def about():
168 return flask.url_for(".index")
169
170 app.register_blueprint(bp)
171
172 assert client.get("/de/").data == b"/de/about"
173 assert client.get("/de/about").data == b"/de/"
174
175
176 def test_templates_and_static(test_apps):
177 from blueprintapp import app
178
179 client = app.test_client()
180
181 rv = client.get("/")
182 assert rv.data == b"Hello from the Frontend"
183 rv = client.get("/admin/")
184 assert rv.data == b"Hello from the Admin"
185 rv = client.get("/admin/index2")
186 assert rv.data == b"Hello from the Admin"
187 rv = client.get("/admin/static/test.txt")
188 assert rv.data.strip() == b"Admin File"
189 rv.close()
190 rv = client.get("/admin/static/css/test.css")
191 assert rv.data.strip() == b"/* nested file */"
192 rv.close()
193
194 # try/finally, in case other tests use this app for Blueprint tests.
195 max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"]
196 try:
197 expected_max_age = 3600
198 if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == expected_max_age:
199 expected_max_age = 7200
200 app.config["SEND_FILE_MAX_AGE_DEFAULT"] = expected_max_age
201 rv = client.get("/admin/static/css/test.css")
202 cc = parse_cache_control_header(rv.headers["Cache-Control"])
203 assert cc.max_age == expected_max_age
tests/test_blueprints.py 149
204 rv.close()
205 finally:
206 app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default
207
208 with app.test_request_context():
209 assert (
210 flask.url_for("admin.static", filename="test.txt")
211 == "/admin/static/test.txt"
212 )
213
214 with app.test_request_context():
215 with pytest.raises(TemplateNotFound) as e:
216 flask.render_template("missing.html")
217 assert e.value.name == "missing.html"
218
219 with flask.Flask(__name__).test_request_context():
220 assert flask.render_template("nested/nested.txt") == "I'm nested"
221
222
223 def test_default_static_max_age(app):
224 class MyBlueprint(flask.Blueprint):
225 def get_send_file_max_age(self, filename):
226 return 100
227
228 blueprint = MyBlueprint("blueprint", __name__, static_folder="static")
229 app.register_blueprint(blueprint)
230
231 # try/finally, in case other tests use this app for Blueprint tests.
232 max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"]
233 try:
234 with app.test_request_context():
235 unexpected_max_age = 3600
236 if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == unexpected_max_age:
237 unexpected_max_age = 7200
238 app.config["SEND_FILE_MAX_AGE_DEFAULT"] = unexpected_max_age
239 rv = blueprint.send_static_file("index.html")
240 cc = parse_cache_control_header(rv.headers["Cache-Control"])
241 assert cc.max_age == 100
242 rv.close()
243 finally:
244 app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default
245
246
247 def test_templates_list(test_apps):
248 from blueprintapp import app
249
250 templates = sorted(app.jinja_env.list_templates())
251 assert templates == ["admin/index.html", "frontend/index.html"]
252
253
254 def test_dotted_name_not_allowed(app, client):
255 with pytest.raises(ValueError):
256 flask.Blueprint("app.ui", __name__)
257
258
259 def test_empty_name_not_allowed(app, client):
260 with pytest.raises(ValueError):
261 flask.Blueprint("", __name__)
262
263
264 def test_dotted_names_from_app(app, client):
265 test = flask.Blueprint("test", __name__)
266
267 @app.route("/")
268 def app_index():
269 return flask.url_for("test.index")
270
271 @test.route("/test/")
272 def index():
273 return flask.url_for("app_index")
274
tests/test_blueprints.py 150
275 app.register_blueprint(test)
276
277 rv = client.get("/")
278 assert rv.data == b"/test/"
279
280
281 def test_empty_url_defaults(app, client):
282 bp = flask.Blueprint("bp", __name__)
283
284 @bp.route("/", defaults={"page": 1})
285 @bp.route("/page/<int:page>")
286 def something(page):
287 return str(page)
288
289 app.register_blueprint(bp)
290
291 assert client.get("/").data == b"1"
292 assert client.get("/page/2").data == b"2"
293
294
295 def test_route_decorator_custom_endpoint(app, client):
296 bp = flask.Blueprint("bp", __name__)
297
298 @bp.route("/foo")
299 def foo():
300 return flask.request.endpoint
301
302 @bp.route("/bar", endpoint="bar")
303 def foo_bar():
304 return flask.request.endpoint
305
306 @bp.route("/bar/123", endpoint="123")
307 def foo_bar_foo():
308 return flask.request.endpoint
309
310 @bp.route("/bar/foo")
311 def bar_foo():
312 return flask.request.endpoint
313
314 app.register_blueprint(bp, url_prefix="/py")
315
316 @app.route("/")
317 def index():
318 return flask.request.endpoint
319
320 assert client.get("/").data == b"index"
321 assert client.get("/py/foo").data == b"bp.foo"
322 assert client.get("/py/bar").data == b"bp.bar"
323 assert client.get("/py/bar/123").data == b"bp.123"
324 assert client.get("/py/bar/foo").data == b"bp.bar_foo"
325
326
327 def test_route_decorator_custom_endpoint_with_dots(app, client):
328 bp = flask.Blueprint("bp", __name__)
329
330 with pytest.raises(ValueError):
331 bp.route("/", endpoint="a.b")(lambda: "")
332
333 with pytest.raises(ValueError):
334 bp.add_url_rule("/", endpoint="a.b")
335
336 def view():
337 return ""
338
339 view.__name__ = "a.b"
340
341 with pytest.raises(ValueError):
342 bp.add_url_rule("/", view_func=view)
343
344
345 def test_endpoint_decorator(app, client):
tests/test_blueprints.py 151
417 @bp.app_template_filter()
418 def super_reverse(s):
419 return s[::-1]
420
421 app.register_blueprint(bp, url_prefix="/py")
422
423 @app.route("/")
424 def index():
425 return flask.render_template("template_filter.html", value="abcd")
426
427 rv = client.get("/")
428 assert rv.data == b"dcba"
429
430
431 def test_template_filter_after_route_with_template(app, client):
432 @app.route("/")
433 def index():
434 return flask.render_template("template_filter.html", value="abcd")
435
436 bp = flask.Blueprint("bp", __name__)
437
438 @bp.app_template_filter()
439 def super_reverse(s):
440 return s[::-1]
441
442 app.register_blueprint(bp, url_prefix="/py")
443 rv = client.get("/")
444 assert rv.data == b"dcba"
445
446
447 def test_add_template_filter_with_template(app, client):
448 bp = flask.Blueprint("bp", __name__)
449
450 def super_reverse(s):
451 return s[::-1]
452
453 bp.add_app_template_filter(super_reverse)
454 app.register_blueprint(bp, url_prefix="/py")
455
456 @app.route("/")
457 def index():
458 return flask.render_template("template_filter.html", value="abcd")
459
460 rv = client.get("/")
461 assert rv.data == b"dcba"
462
463
464 def test_template_filter_with_name_and_template(app, client):
465 bp = flask.Blueprint("bp", __name__)
466
467 @bp.app_template_filter("super_reverse")
468 def my_reverse(s):
469 return s[::-1]
470
471 app.register_blueprint(bp, url_prefix="/py")
472
473 @app.route("/")
474 def index():
475 return flask.render_template("template_filter.html", value="abcd")
476
477 rv = client.get("/")
478 assert rv.data == b"dcba"
479
480
481 def test_add_template_filter_with_name_and_template(app, client):
482 bp = flask.Blueprint("bp", __name__)
483
484 def my_reverse(s):
485 return s[::-1]
486
487 bp.add_app_template_filter(my_reverse, "super_reverse")
tests/test_blueprints.py 153
559 @app.route("/")
560 def index():
561 return flask.render_template("template_test.html", value=False)
562
563 rv = client.get("/")
564 assert b"Success!" in rv.data
565
566
567 def test_template_test_after_route_with_template(app, client):
568 @app.route("/")
569 def index():
570 return flask.render_template("template_test.html", value=False)
571
572 bp = flask.Blueprint("bp", __name__)
573
574 @bp.app_template_test()
575 def boolean(value):
576 return isinstance(value, bool)
577
578 app.register_blueprint(bp, url_prefix="/py")
579 rv = client.get("/")
580 assert b"Success!" in rv.data
581
582
583 def test_add_template_test_with_template(app, client):
584 bp = flask.Blueprint("bp", __name__)
585
586 def boolean(value):
587 return isinstance(value, bool)
588
589 bp.add_app_template_test(boolean)
590 app.register_blueprint(bp, url_prefix="/py")
591
592 @app.route("/")
593 def index():
594 return flask.render_template("template_test.html", value=False)
595
596 rv = client.get("/")
597 assert b"Success!" in rv.data
598
599
600 def test_template_test_with_name_and_template(app, client):
601 bp = flask.Blueprint("bp", __name__)
602
603 @bp.app_template_test("boolean")
604 def is_boolean(value):
605 return isinstance(value, bool)
606
607 app.register_blueprint(bp, url_prefix="/py")
608
609 @app.route("/")
610 def index():
611 return flask.render_template("template_test.html", value=False)
612
613 rv = client.get("/")
614 assert b"Success!" in rv.data
615
616
617 def test_add_template_test_with_name_and_template(app, client):
618 bp = flask.Blueprint("bp", __name__)
619
620 def is_boolean(value):
621 return isinstance(value, bool)
622
623 bp.add_app_template_test(is_boolean, "boolean")
624 app.register_blueprint(bp, url_prefix="/py")
625
626 @app.route("/")
627 def index():
628 return flask.render_template("template_test.html", value=False)
629
tests/test_blueprints.py 155
630 rv = client.get("/")
631 assert b"Success!" in rv.data
632
633
634 def test_context_processing(app, client):
635 answer_bp = flask.Blueprint("answer_bp", __name__)
636
637 def template_string():
638 return flask.render_template_string(
639 "{% if notanswer %}{{ notanswer }} is not the answer. {% endif %}"
640 "{% if answer %}{{ answer }} is the answer.{% endif %}"
641 )
642
643 # App global context processor
644 @answer_bp.app_context_processor
645 def not_answer_context_processor():
646 return {"notanswer": 43}
647
648 # Blueprint local context processor
649 @answer_bp.context_processor
650 def answer_context_processor():
651 return {"answer": 42}
652
653 # Setup endpoints for testing
654 @answer_bp.route("/bp")
655 def bp_page():
656 return template_string()
657
658 @app.route("/")
659 def app_page():
660 return template_string()
661
662 # Register the blueprint
663 app.register_blueprint(answer_bp)
664
665 app_page_bytes = client.get("/").data
666 answer_page_bytes = client.get("/bp").data
667
668 assert b"43" in app_page_bytes
669 assert b"42" not in app_page_bytes
670
671 assert b"42" in answer_page_bytes
672 assert b"43" in answer_page_bytes
673
674
675 def test_template_global(app):
676 bp = flask.Blueprint("bp", __name__)
677
678 @bp.app_template_global()
679 def get_answer():
680 return 42
681
682 # Make sure the function is not in the jinja_env already
683 assert "get_answer" not in app.jinja_env.globals.keys()
684 app.register_blueprint(bp)
685
686 # Tests
687 assert "get_answer" in app.jinja_env.globals.keys()
688 assert app.jinja_env.globals["get_answer"] is get_answer
689 assert app.jinja_env.globals["get_answer"]() == 42
690
691 with app.app_context():
692 rv = flask.render_template_string("{{ get_answer() }}")
693 assert rv == "42"
694
695
696 def test_request_processing(app, client):
697 bp = flask.Blueprint("bp", __name__)
698 evts = []
699
700 @bp.before_request
tests/test_blueprints.py 156
914 parent.register_blueprint(child)
915 app.register_blueprint(parent)
916 assert (
917 client.get("/a").data == b"app_1, app_2, parent_1, parent_2, child_1, child_2"
918 )
919 assert client.get("/b").data == b"child"
920
921
922 @pytest.mark.parametrize(
923 "parent_init, child_init, parent_registration, child_registration",
924 [
925 ("/parent", "/child", None, None),
926 ("/parent", None, None, "/child"),
927 (None, None, "/parent", "/child"),
928 ("/other", "/something", "/parent", "/child"),
929 ],
930 )
931 def test_nesting_url_prefixes(
932 parent_init,
933 child_init,
934 parent_registration,
935 child_registration,
936 app,
937 client,
938 ) -> None:
939 parent = flask.Blueprint("parent", __name__, url_prefix=parent_init)
940 child = flask.Blueprint("child", __name__, url_prefix=child_init)
941
942 @child.route("/")
943 def index():
944 return "index"
945
946 parent.register_blueprint(child, url_prefix=child_registration)
947 app.register_blueprint(parent, url_prefix=parent_registration)
948
949 response = client.get("/parent/child/")
950 assert response.status_code == 200
951
952
953 def test_nesting_subdomains(app, client) -> None:
954 app.subdomain_matching = True
955 app.config["SERVER_NAME"] = "example.test"
956 client.allow_subdomain_redirects = True
957
958 parent = flask.Blueprint("parent", __name__)
959 child = flask.Blueprint("child", __name__)
960
961 @child.route("/child/")
962 def index():
963 return "child"
964
965 parent.register_blueprint(child)
966 app.register_blueprint(parent, subdomain="api")
967
968 response = client.get("/child/", base_url="http://api.example.test")
969 assert response.status_code == 200
970
971
972 def test_child_and_parent_subdomain(app, client) -> None:
973 app.subdomain_matching = True
974 app.config["SERVER_NAME"] = "example.test"
975 client.allow_subdomain_redirects = True
976
977 parent = flask.Blueprint("parent", __name__)
978 child = flask.Blueprint("child", __name__, subdomain="api")
979
980 @child.route("/")
981 def index():
982 return "child"
983
984 parent.register_blueprint(child)
160
tests/test_basic.py
1 import gc
2 import re
3 import typing as t
4 import uuid
5 import warnings
tests/test_basic.py 161
6 import weakref
7 from contextlib import nullcontext
8 from datetime import datetime
9 from datetime import timezone
10 from platform import python_implementation
11
12 import pytest
13 import werkzeug.serving
14 from markupsafe import Markup
15 from werkzeug.exceptions import BadRequest
16 from werkzeug.exceptions import Forbidden
17 from werkzeug.exceptions import NotFound
18 from werkzeug.http import parse_date
19 from werkzeug.routing import BuildError
20 from werkzeug.routing import RequestRedirect
21
22 import flask
23
24 require_cpython_gc = pytest.mark.skipif(
25 python_implementation() != "CPython",
26 reason="Requires CPython GC behavior",
27 )
28
29
30 def test_options_work(app, client):
31 @app.route("/", methods=["GET", "POST"])
32 def index():
33 return "Hello World"
34
35 rv = client.open("/", method="OPTIONS")
36 assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST"]
37 assert rv.data == b""
38
39
40 def test_options_on_multiple_rules(app, client):
41 @app.route("/", methods=["GET", "POST"])
42 def index():
43 return "Hello World"
44
45 @app.route("/", methods=["PUT"])
46 def index_put():
47 return "Aha!"
48
49 rv = client.open("/", method="OPTIONS")
50 assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS", "POST", "PUT"]
51
52
53 @pytest.mark.parametrize("method", ["get", "post", "put", "delete", "patch"])
54 def test_method_route(app, client, method):
55 method_route = getattr(app, method)
56 client_method = getattr(client, method)
57
58 @method_route("/")
59 def hello():
60 return "Hello"
61
62 assert client_method("/").data == b"Hello"
63
64
65 def test_method_route_no_methods(app):
66 with pytest.raises(TypeError):
67 app.get("/", methods=["GET", "POST"])
68
69
70 def test_provide_automatic_options_attr():
71 app = flask.Flask(__name__)
72
73 def index():
74 return "Hello World!"
75
76 index.provide_automatic_options = False
tests/test_basic.py 162
77 app.route("/")(index)
78 rv = app.test_client().open("/", method="OPTIONS")
79 assert rv.status_code == 405
80
81 app = flask.Flask(__name__)
82
83 def index2():
84 return "Hello World!"
85
86 index2.provide_automatic_options = True
87 app.route("/", methods=["OPTIONS"])(index2)
88 rv = app.test_client().open("/", method="OPTIONS")
89 assert sorted(rv.allow) == ["OPTIONS"]
90
91
92 def test_provide_automatic_options_kwarg(app, client):
93 def index():
94 return flask.request.method
95
96 def more():
97 return flask.request.method
98
99 app.add_url_rule("/", view_func=index, provide_automatic_options=False)
100 app.add_url_rule(
101 "/more",
102 view_func=more,
103 methods=["GET", "POST"],
104 provide_automatic_options=False,
105 )
106 assert client.get("/").data == b"GET"
107
108 rv = client.post("/")
109 assert rv.status_code == 405
110 assert sorted(rv.allow) == ["GET", "HEAD"]
111
112 rv = client.open("/", method="OPTIONS")
113 assert rv.status_code == 405
114
115 rv = client.head("/")
116 assert rv.status_code == 200
117 assert not rv.data # head truncates
118 assert client.post("/more").data == b"POST"
119 assert client.get("/more").data == b"GET"
120
121 rv = client.delete("/more")
122 assert rv.status_code == 405
123 assert sorted(rv.allow) == ["GET", "HEAD", "POST"]
124
125 rv = client.open("/more", method="OPTIONS")
126 assert rv.status_code == 405
127
128
129 def test_request_dispatching(app, client):
130 @app.route("/")
131 def index():
132 return flask.request.method
133
134 @app.route("/more", methods=["GET", "POST"])
135 def more():
136 return flask.request.method
137
138 assert client.get("/").data == b"GET"
139 rv = client.post("/")
140 assert rv.status_code == 405
141 assert sorted(rv.allow) == ["GET", "HEAD", "OPTIONS"]
142 rv = client.head("/")
143 assert rv.status_code == 200
144 assert not rv.data # head truncates
145 assert client.post("/more").data == b"POST"
146 assert client.get("/more").data == b"GET"
147 rv = client.delete("/more")
tests/test_basic.py 163
290
291 def test_session_using_session_settings(app, client):
292 app.config.update(
293 SERVER_NAME="www.example.com:8080",
294 APPLICATION_ROOT="/test",
295 SESSION_COOKIE_DOMAIN=".example.com",
296 SESSION_COOKIE_HTTPONLY=False,
297 SESSION_COOKIE_SECURE=True,
298 SESSION_COOKIE_PARTITIONED=True,
299 SESSION_COOKIE_SAMESITE="Lax",
300 SESSION_COOKIE_PATH="/",
301 )
302
303 @app.route("/")
304 def index():
305 flask.session["testing"] = 42
306 return "Hello World"
307
308 @app.route("/clear")
309 def clear():
310 flask.session.pop("testing", None)
311 return "Goodbye World"
312
313 rv = client.get("/", "http://www.example.com:8080/test/")
314 cookie = rv.headers["set-cookie"].lower()
315 # or condition for Werkzeug < 2.3
316 assert "domain=example.com" in cookie or "domain=.example.com" in cookie
317 assert "path=/" in cookie
318 assert "secure" in cookie
319 assert "httponly" not in cookie
320 assert "samesite" in cookie
321 assert "partitioned" in cookie
322
323 rv = client.get("/clear", "http://www.example.com:8080/test/")
324 cookie = rv.headers["set-cookie"].lower()
325 assert "session=;" in cookie
326 # or condition for Werkzeug < 2.3
327 assert "domain=example.com" in cookie or "domain=.example.com" in cookie
328 assert "path=/" in cookie
329 assert "secure" in cookie
330 assert "samesite" in cookie
331 assert "partitioned" in cookie
332
333
334 def test_session_using_samesite_attribute(app, client):
335 @app.route("/")
336 def index():
337 flask.session["testing"] = 42
338 return "Hello World"
339
340 app.config.update(SESSION_COOKIE_SAMESITE="invalid")
341
342 with pytest.raises(ValueError):
343 client.get("/")
344
345 app.config.update(SESSION_COOKIE_SAMESITE=None)
346 rv = client.get("/")
347 cookie = rv.headers["set-cookie"].lower()
348 assert "samesite" not in cookie
349
350 app.config.update(SESSION_COOKIE_SAMESITE="Strict")
351 rv = client.get("/")
352 cookie = rv.headers["set-cookie"].lower()
353 assert "samesite=strict" in cookie
354
355 app.config.update(SESSION_COOKIE_SAMESITE="Lax")
356 rv = client.get("/")
357 cookie = rv.headers["set-cookie"].lower()
358 assert "samesite=lax" in cookie
359
360
tests/test_basic.py 166
432
433 @app.route("/")
434 def dump_session_contents():
435 return repr(flask.session.get("foo"))
436
437 assert client.get("/").data == b"None"
438 assert client.get("/").data == b"42"
439
440
441 def test_session_special_types(app, client):
442 now = datetime.now(timezone.utc).replace(microsecond=0)
443 the_uuid = uuid.uuid4()
444
445 @app.route("/")
446 def dump_session_contents():
447 flask.session["t"] = (1, 2, 3)
448 flask.session["b"] = b"\xff"
449 flask.session["m"] = Markup("<html>")
450 flask.session["u"] = the_uuid
451 flask.session["d"] = now
452 flask.session["t_tag"] = {" t": "not-a-tuple"}
453 flask.session["di_t_tag"] = {" t__": "not-a-tuple"}
454 flask.session["di_tag"] = {" di": "not-a-dict"}
455 return "", 204
456
457 with client:
458 client.get("/")
459 s = flask.session
460 assert s["t"] == (1, 2, 3)
461 assert type(s["b"]) is bytes # noqa: E721
462 assert s["b"] == b"\xff"
463 assert type(s["m"]) is Markup # noqa: E721
464 assert s["m"] == Markup("<html>")
465 assert s["u"] == the_uuid
466 assert s["d"] == now
467 assert s["t_tag"] == {" t": "not-a-tuple"}
468 assert s["di_t_tag"] == {" t__": "not-a-tuple"}
469 assert s["di_tag"] == {" di": "not-a-dict"}
470
471
472 def test_session_cookie_setting(app):
473 is_permanent = True
474
475 @app.route("/bump")
476 def bump():
477 rv = flask.session["foo"] = flask.session.get("foo", 0) + 1
478 flask.session.permanent = is_permanent
479 return str(rv)
480
481 @app.route("/read")
482 def read():
483 return str(flask.session.get("foo", 0))
484
485 def run_test(expect_header):
486 with app.test_client() as c:
487 assert c.get("/bump").data == b"1"
488 assert c.get("/bump").data == b"2"
489 assert c.get("/bump").data == b"3"
490
491 rv = c.get("/read")
492 set_cookie = rv.headers.get("set-cookie")
493 assert (set_cookie is not None) == expect_header
494 assert rv.data == b"3"
495
496 is_permanent = True
497 app.config["SESSION_REFRESH_EACH_REQUEST"] = True
498 run_test(expect_header=True)
499
500 is_permanent = True
501 app.config["SESSION_REFRESH_EACH_REQUEST"] = False
502 run_test(expect_header=False)
tests/test_basic.py 168
503
504 is_permanent = False
505 app.config["SESSION_REFRESH_EACH_REQUEST"] = True
506 run_test(expect_header=False)
507
508 is_permanent = False
509 app.config["SESSION_REFRESH_EACH_REQUEST"] = False
510 run_test(expect_header=False)
511
512
513 def test_session_vary_cookie(app, client):
514 @app.route("/set")
515 def set_session():
516 flask.session["test"] = "test"
517 return ""
518
519 @app.route("/get")
520 def get():
521 return flask.session.get("test")
522
523 @app.route("/getitem")
524 def getitem():
525 return flask.session["test"]
526
527 @app.route("/setdefault")
528 def setdefault():
529 return flask.session.setdefault("test", "default")
530
531 @app.route("/clear")
532 def clear():
533 flask.session.clear()
534 return ""
535
536 @app.route("/vary-cookie-header-set")
537 def vary_cookie_header_set():
538 response = flask.Response()
539 response.vary.add("Cookie")
540 flask.session["test"] = "test"
541 return response
542
543 @app.route("/vary-header-set")
544 def vary_header_set():
545 response = flask.Response()
546 response.vary.update(("Accept-Encoding", "Accept-Language"))
547 flask.session["test"] = "test"
548 return response
549
550 @app.route("/no-vary-header")
551 def no_vary_header():
552 return ""
553
554 def expect(path, header_value="Cookie"):
555 rv = client.get(path)
556
557 if header_value:
558 # The 'Vary' key should exist in the headers only once.
559 assert len(rv.headers.get_all("Vary")) == 1
560 assert rv.headers["Vary"] == header_value
561 else:
562 assert "Vary" not in rv.headers
563
564 expect("/set")
565 expect("/get")
566 expect("/getitem")
567 expect("/setdefault")
568 expect("/clear")
569 expect("/vary-cookie-header-set")
570 expect("/vary-header-set", "Accept-Encoding, Accept-Language, Cookie")
571 expect("/no-vary-header", None)
572
573
tests/test_basic.py 169
787
788 @app.teardown_request
789 def teardown_request1(exc):
790 assert type(exc) is ZeroDivisionError
791 called.append(True)
792 # This raises a new error and blows away sys.exc_info(), so we can
793 # test that all teardown_requests get passed the same original
794 # exception.
795 try:
796 raise TypeError()
797 except Exception:
798 pass
799
800 @app.teardown_request
801 def teardown_request2(exc):
802 assert type(exc) is ZeroDivisionError
803 called.append(True)
804 # This raises a new error and blows away sys.exc_info(), so we can
805 # test that all teardown_requests get passed the same original
806 # exception.
807 try:
808 raise TypeError()
809 except Exception:
810 pass
811
812 @app.route("/")
813 def fails():
814 raise ZeroDivisionError
815
816 rv = client.get("/")
817 assert rv.status_code == 500
818 assert b"Internal Server Error" in rv.data
819 assert len(called) == 2
820
821
822 def test_before_after_request_order(app, client):
823 called = []
824
825 @app.before_request
826 def before1():
827 called.append(1)
828
829 @app.before_request
830 def before2():
831 called.append(2)
832
833 @app.after_request
834 def after1(response):
835 called.append(4)
836 return response
837
838 @app.after_request
839 def after2(response):
840 called.append(3)
841 return response
842
843 @app.teardown_request
844 def finish1(exc):
845 called.append(6)
846
847 @app.teardown_request
848 def finish2(exc):
849 called.append(5)
850
851 @app.route("/")
852 def index():
853 return "42"
854
855 rv = client.get("/")
856 assert rv.data == b"42"
857 assert called == [1, 2, 3, 4, 5, 6]
tests/test_basic.py 173
858
859
860 def test_error_handling(app, client):
861 app.testing = False
862
863 @app.errorhandler(404)
864 def not_found(e):
865 return "not found", 404
866
867 @app.errorhandler(500)
868 def internal_server_error(e):
869 return "internal server error", 500
870
871 @app.errorhandler(Forbidden)
872 def forbidden(e):
873 return "forbidden", 403
874
875 @app.route("/")
876 def index():
877 flask.abort(404)
878
879 @app.route("/error")
880 def error():
881 raise ZeroDivisionError
882
883 @app.route("/forbidden")
884 def error2():
885 flask.abort(403)
886
887 rv = client.get("/")
888 assert rv.status_code == 404
889 assert rv.data == b"not found"
890 rv = client.get("/error")
891 assert rv.status_code == 500
892 assert b"internal server error" == rv.data
893 rv = client.get("/forbidden")
894 assert rv.status_code == 403
895 assert b"forbidden" == rv.data
896
897
898 def test_error_handling_processing(app, client):
899 app.testing = False
900
901 @app.errorhandler(500)
902 def internal_server_error(e):
903 return "internal server error", 500
904
905 @app.route("/")
906 def broken_func():
907 raise ZeroDivisionError
908
909 @app.after_request
910 def after_request(resp):
911 resp.mimetype = "text/x-special"
912 return resp
913
914 resp = client.get("/")
915 assert resp.mimetype == "text/x-special"
916 assert resp.data == b"internal server error"
917
918
919 def test_baseexception_error_handling(app, client):
920 app.testing = False
921
922 @app.route("/")
923 def broken_func():
924 raise KeyboardInterrupt()
925
926 with pytest.raises(KeyboardInterrupt):
927 client.get("/")
928
tests/test_basic.py 174
929
930 def test_before_request_and_routing_errors(app, client):
931 @app.before_request
932 def attach_something():
933 flask.g.something = "value"
934
935 @app.errorhandler(404)
936 def return_something(error):
937 return flask.g.something, 404
938
939 rv = client.get("/")
940 assert rv.status_code == 404
941 assert rv.data == b"value"
942
943
944 def test_user_error_handling(app, client):
945 class MyException(Exception):
946 pass
947
948 @app.errorhandler(MyException)
949 def handle_my_exception(e):
950 assert isinstance(e, MyException)
951 return "42"
952
953 @app.route("/")
954 def index():
955 raise MyException()
956
957 assert client.get("/").data == b"42"
958
959
960 def test_http_error_subclass_handling(app, client):
961 class ForbiddenSubclass(Forbidden):
962 pass
963
964 @app.errorhandler(ForbiddenSubclass)
965 def handle_forbidden_subclass(e):
966 assert isinstance(e, ForbiddenSubclass)
967 return "banana"
968
969 @app.errorhandler(403)
970 def handle_403(e):
971 assert not isinstance(e, ForbiddenSubclass)
972 assert isinstance(e, Forbidden)
973 return "apple"
974
975 @app.route("/1")
976 def index1():
977 raise ForbiddenSubclass()
978
979 @app.route("/2")
980 def index2():
981 flask.abort(403)
982
983 @app.route("/3")
984 def index3():
985 raise Forbidden()
986
987 assert client.get("/1").data == b"banana"
988 assert client.get("/2").data == b"apple"
989 assert client.get("/3").data == b"apple"
990
991
992 def test_errorhandler_precedence(app, client):
993 class E1(Exception):
994 pass
995
996 class E2(Exception):
997 pass
998
999 class E3(E1, E2):
tests/test_basic.py 175
1000 pass
1001
1002 @app.errorhandler(E2)
1003 def handle_e2(e):
1004 return "E2"
1005
1006 @app.errorhandler(Exception)
1007 def handle_exception(e):
1008 return "Exception"
1009
1010 @app.route("/E1")
1011 def raise_e1():
1012 raise E1
1013
1014 @app.route("/E3")
1015 def raise_e3():
1016 raise E3
1017
1018 rv = client.get("/E1")
1019 assert rv.data == b"Exception"
1020
1021 rv = client.get("/E3")
1022 assert rv.data == b"E2"
1023
1024
1025 @pytest.mark.parametrize(
1026 ("debug", "trap", "expect_key", "expect_abort"),
1027 [(False, None, True, True), (True, None, False, True), (False, True, False, False)],
1028 )
1029 def test_trap_bad_request_key_error(app, client, debug, trap, expect_key, expect_abort):
1030 app.config["DEBUG"] = debug
1031 app.config["TRAP_BAD_REQUEST_ERRORS"] = trap
1032
1033 @app.route("/key")
1034 def fail():
1035 flask.request.form["missing_key"]
1036
1037 @app.route("/abort")
1038 def allow_abort():
1039 flask.abort(400)
1040
1041 if expect_key:
1042 rv = client.get("/key")
1043 assert rv.status_code == 400
1044 assert b"missing_key" not in rv.data
1045 else:
1046 with pytest.raises(KeyError) as exc_info:
1047 client.get("/key")
1048
1049 assert exc_info.errisinstance(BadRequest)
1050 assert "missing_key" in exc_info.value.get_description()
1051
1052 if expect_abort:
1053 rv = client.get("/abort")
1054 assert rv.status_code == 400
1055 else:
1056 with pytest.raises(BadRequest):
1057 client.get("/abort")
1058
1059
1060 def test_trapping_of_all_http_exceptions(app, client):
1061 app.config["TRAP_HTTP_EXCEPTIONS"] = True
1062
1063 @app.route("/fail")
1064 def fail():
1065 flask.abort(404)
1066
1067 with pytest.raises(NotFound):
1068 client.get("/fail")
1069
1070
tests/test_basic.py 176
1142 return (
1143 flask.Response(
1144 "Hello world", 404, {"Content-Type": "text/html", "X-Foo": "Baz"}
1145 ),
1146 {"Content-Type": "text/plain", "X-Foo": "Bar", "X-Bar": "Foo"},
1147 )
1148
1149 @app.route("/response_status")
1150 def from_response_status():
1151 return app.response_class("Hello world", 400), 500
1152
1153 @app.route("/wsgi")
1154 def from_wsgi():
1155 return NotFound()
1156
1157 @app.route("/dict")
1158 def from_dict():
1159 return {"foo": "bar"}, 201
1160
1161 @app.route("/list")
1162 def from_list():
1163 return ["foo", "bar"], 201
1164
1165 assert client.get("/text").data == "Hällo Wörld".encode()
1166 assert client.get("/bytes").data == "Hällo Wörld".encode()
1167
1168 rv = client.get("/full_tuple")
1169 assert rv.data == b"Meh"
1170 assert rv.headers["X-Foo"] == "Testing"
1171 assert rv.status_code == 400
1172 assert rv.mimetype == "text/plain"
1173
1174 rv = client.get("/text_headers")
1175 assert rv.data == b"Hello"
1176 assert rv.headers["X-Foo"] == "Test"
1177 assert rv.status_code == 200
1178 assert rv.mimetype == "text/plain"
1179
1180 rv = client.get("/text_status")
1181 assert rv.data == b"Hi, status!"
1182 assert rv.status_code == 400
1183 assert rv.mimetype == "text/html"
1184
1185 rv = client.get("/response_headers")
1186 assert rv.data == b"Hello world"
1187 assert rv.content_type == "text/plain"
1188 assert rv.headers.getlist("X-Foo") == ["Bar"]
1189 assert rv.headers["X-Bar"] == "Foo"
1190 assert rv.status_code == 404
1191
1192 rv = client.get("/response_status")
1193 assert rv.data == b"Hello world"
1194 assert rv.status_code == 500
1195
1196 rv = client.get("/wsgi")
1197 assert b"Not Found" in rv.data
1198 assert rv.status_code == 404
1199
1200 rv = client.get("/dict")
1201 assert rv.json == {"foo": "bar"}
1202 assert rv.status_code == 201
1203
1204 rv = client.get("/list")
1205 assert rv.json == ["foo", "bar"]
1206 assert rv.status_code == 201
1207
1208
1209 def test_response_type_errors():
1210 app = flask.Flask(__name__)
1211 app.testing = True
1212
tests/test_basic.py 178
1213 @app.route("/none")
1214 def from_none():
1215 pass
1216
1217 @app.route("/small_tuple")
1218 def from_small_tuple():
1219 return ("Hello",)
1220
1221 @app.route("/large_tuple")
1222 def from_large_tuple():
1223 return "Hello", 234, {"X-Foo": "Bar"}, "???"
1224
1225 @app.route("/bad_type")
1226 def from_bad_type():
1227 return True
1228
1229 @app.route("/bad_wsgi")
1230 def from_bad_wsgi():
1231 return lambda: None
1232
1233 c = app.test_client()
1234
1235 with pytest.raises(TypeError) as e:
1236 c.get("/none")
1237
1238 assert "returned None" in str(e.value)
1239 assert "from_none" in str(e.value)
1240
1241 with pytest.raises(TypeError) as e:
1242 c.get("/small_tuple")
1243
1244 assert "tuple must have the form" in str(e.value)
1245
1246 with pytest.raises(TypeError):
1247 c.get("/large_tuple")
1248
1249 with pytest.raises(TypeError) as e:
1250 c.get("/bad_type")
1251
1252 assert "it was a bool" in str(e.value)
1253
1254 with pytest.raises(TypeError):
1255 c.get("/bad_wsgi")
1256
1257
1258 def test_make_response(app, req_ctx):
1259 rv = flask.make_response()
1260 assert rv.status_code == 200
1261 assert rv.data == b""
1262 assert rv.mimetype == "text/html"
1263
1264 rv = flask.make_response("Awesome")
1265 assert rv.status_code == 200
1266 assert rv.data == b"Awesome"
1267 assert rv.mimetype == "text/html"
1268
1269 rv = flask.make_response("W00t", 404)
1270 assert rv.status_code == 404
1271 assert rv.data == b"W00t"
1272 assert rv.mimetype == "text/html"
1273
1274 rv = flask.make_response(c for c in "Hello")
1275 assert rv.status_code == 200
1276 assert rv.data == b"Hello"
1277 assert rv.mimetype == "text/html"
1278
1279
1280 def test_make_response_with_response_instance(app, req_ctx):
1281 rv = flask.make_response(flask.jsonify({"msg": "W00t"}), 400)
1282 assert rv.status_code == 400
1283 assert rv.data == b'{"msg":"W00t"}\n'
tests/test_basic.py 179
1355 try:
1356 raise RuntimeError("Test case where BuildError is not current.")
1357 except RuntimeError:
1358 pytest.raises(BuildError, app.handle_url_build_error, error, "spam", {})
1359
1360 # Test a custom handler.
1361 def handler(error, endpoint, values):
1362 # Just a test.
1363 return "/test_handler/"
1364
1365 app.url_build_error_handlers.append(handler)
1366 with app.test_request_context():
1367 assert flask.url_for("spam") == "/test_handler/"
1368
1369
1370 def test_build_error_handler_reraise(app):
1371 # Test a custom handler which reraises the BuildError
1372 def handler_raises_build_error(error, endpoint, values):
1373 raise error
1374
1375 app.url_build_error_handlers.append(handler_raises_build_error)
1376
1377 with app.test_request_context():
1378 pytest.raises(BuildError, flask.url_for, "not.existing")
1379
1380
1381 def test_url_for_passes_special_values_to_build_error_handler(app):
1382 @app.url_build_error_handlers.append
1383 def handler(error, endpoint, values):
1384 assert values == {
1385 "_external": False,
1386 "_anchor": None,
1387 "_method": None,
1388 "_scheme": None,
1389 }
1390 return "handled"
1391
1392 with app.test_request_context():
1393 flask.url_for("/")
1394
1395
1396 def test_static_files(app, client):
1397 rv = client.get("/static/index.html")
1398 assert rv.status_code == 200
1399 assert rv.data.strip() == b"<h1>Hello World!</h1>"
1400 with app.test_request_context():
1401 assert flask.url_for("static", filename="index.html") == "/static/index.html"
1402 rv.close()
1403
1404
1405 def test_static_url_path():
1406 app = flask.Flask(__name__, static_url_path="/foo")
1407 app.testing = True
1408 rv = app.test_client().get("/foo/index.html")
1409 assert rv.status_code == 200
1410 rv.close()
1411
1412 with app.test_request_context():
1413 assert flask.url_for("static", filename="index.html") == "/foo/index.html"
1414
1415
1416 def test_static_url_path_with_ending_slash():
1417 app = flask.Flask(__name__, static_url_path="/foo/")
1418 app.testing = True
1419 rv = app.test_client().get("/foo/index.html")
1420 assert rv.status_code == 200
1421 rv.close()
1422
1423 with app.test_request_context():
1424 assert flask.url_for("static", filename="index.html") == "/foo/index.html"
1425
tests/test_basic.py 181
1426
1427 def test_static_url_empty_path(app):
1428 app = flask.Flask(__name__, static_folder="", static_url_path="")
1429 rv = app.test_client().open("/static/index.html", method="GET")
1430 assert rv.status_code == 200
1431 rv.close()
1432
1433
1434 def test_static_url_empty_path_default(app):
1435 app = flask.Flask(__name__, static_folder="")
1436 rv = app.test_client().open("/static/index.html", method="GET")
1437 assert rv.status_code == 200
1438 rv.close()
1439
1440
1441 def test_static_folder_with_pathlib_path(app):
1442 from pathlib import Path
1443
1444 app = flask.Flask(__name__, static_folder=Path("static"))
1445 rv = app.test_client().open("/static/index.html", method="GET")
1446 assert rv.status_code == 200
1447 rv.close()
1448
1449
1450 def test_static_folder_with_ending_slash():
1451 app = flask.Flask(__name__, static_folder="static/")
1452
1453 @app.route("/<path:path>")
1454 def catch_all(path):
1455 return path
1456
1457 rv = app.test_client().get("/catch/all")
1458 assert rv.data == b"catch/all"
1459
1460
1461 def test_static_route_with_host_matching():
1462 app = flask.Flask(__name__, host_matching=True, static_host="example.com")
1463 c = app.test_client()
1464 rv = c.get("http://example.com/static/index.html")
1465 assert rv.status_code == 200
1466 rv.close()
1467 with app.test_request_context():
1468 rv = flask.url_for("static", filename="index.html", _external=True)
1469 assert rv == "http://example.com/static/index.html"
1470 # Providing static_host without host_matching=True should error.
1471 with pytest.raises(AssertionError):
1472 flask.Flask(__name__, static_host="example.com")
1473 # Providing host_matching=True with static_folder
1474 # but without static_host should error.
1475 with pytest.raises(AssertionError):
1476 flask.Flask(__name__, host_matching=True)
1477 # Providing host_matching=True without static_host
1478 # but with static_folder=None should not error.
1479 flask.Flask(__name__, host_matching=True, static_folder=None)
1480
1481
1482 def test_request_locals():
1483 assert repr(flask.g) == "<LocalProxy unbound>"
1484 assert not flask.g
1485
1486
1487 @pytest.mark.parametrize(
1488 ("subdomain_matching", "host_matching", "expect_base", "expect_abc", "expect_xyz"),
1489 [
1490 (False, False, "default", "default", "default"),
1491 (True, False, "default", "abc", "<invalid>"),
1492 (False, True, "default", "abc", "default"),
1493 ],
1494 )
1495 def test_server_name_matching(
1496 subdomain_matching: bool,
tests/test_basic.py 182
1923
1924 @require_cpython_gc
1925 def test_app_freed_on_zero_refcount():
1926 # A Flask instance should not create a reference cycle that prevents CPython
1927 # from freeing it when all external references to it are released (see #3761).
1928 gc.disable()
1929 try:
1930 app = flask.Flask(__name__)
1931 assert app.view_functions["static"]
1932 weak = weakref.ref(app)
1933 assert weak() is not None
1934 del app
1935 assert weak() is None
1936 finally:
1937 gc.enable()
src/flask/py.typed
1
src/flask/__main__.py
1 from .cli import main
2
3 main()
src/flask/signals.py
1 from __future__ import annotations
2
3 from blinker import Namespace
4
5 # This namespace is only for signals provided by Flask itself.
6 _signals = Namespace()
7
8 template_rendered = _signals.signal("template-rendered")
9 before_render_template = _signals.signal("before-render-template")
10 request_started = _signals.signal("request-started")
11 request_finished = _signals.signal("request-finished")
12 request_tearing_down = _signals.signal("request-tearing-down")
13 got_request_exception = _signals.signal("got-request-exception")
14 appcontext_tearing_down = _signals.signal("appcontext-tearing-down")
15 appcontext_pushed = _signals.signal("appcontext-pushed")
16 appcontext_popped = _signals.signal("appcontext-popped")
17 message_flashed = _signals.signal("message-flashed")
src/flask/globals.py
1 from __future__ import annotations
2
3 import typing as t
4 from contextvars import ContextVar
5
6 from werkzeug.local import LocalProxy
7
8 if t.TYPE_CHECKING: # pragma: no cover
9 from .app import Flask
10 from .ctx import _AppCtxGlobals
11 from .ctx import AppContext
12 from .ctx import RequestContext
13 from .sessions import SessionMixin
14 from .wrappers import Request
15
16
189
17 _no_app_msg = """\
18 Working outside of application context.
19
20 This typically means that you attempted to use functionality that needed
21 the current application. To solve this, set up an application context
22 with app.app_context(). See the documentation for more information.\
23 """
24 _cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx")
25 app_ctx: AppContext = LocalProxy( # type: ignore[assignment]
26 _cv_app, unbound_message=_no_app_msg
27 )
28 current_app: Flask = LocalProxy( # type: ignore[assignment]
29 _cv_app, "app", unbound_message=_no_app_msg
30 )
31 g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment]
32 _cv_app, "g", unbound_message=_no_app_msg
33 )
34
35 _no_req_msg = """\
36 Working outside of request context.
37
38 This typically means that you attempted to use functionality that needed
39 an active HTTP request. Consult the documentation on testing for
40 information about how to avoid this problem.\
41 """
42 _cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx")
43 request_ctx: RequestContext = LocalProxy( # type: ignore[assignment]
44 _cv_request, unbound_message=_no_req_msg
45 )
46 request: Request = LocalProxy( # type: ignore[assignment]
47 _cv_request, "request", unbound_message=_no_req_msg
48 )
49 session: SessionMixin = LocalProxy( # type: ignore[assignment]
50 _cv_request, "session", unbound_message=_no_req_msg
51 )
src/flask/logging.py
1 from __future__ import annotations
2
3 import logging
4 import sys
5 import typing as t
6
7 from werkzeug.local import LocalProxy
8
9 from .globals import request
10
11 if t.TYPE_CHECKING: # pragma: no cover
12 from .sansio.app import App
13
14
15 @LocalProxy
16 def wsgi_errors_stream() -> t.TextIO:
17 """Find the most appropriate error stream for the application. If a request
18 is active, log to ``wsgi.errors``, otherwise use ``sys.stderr``.
19
20 If you configure your own :class:`logging.StreamHandler`, you may want to
21 use this for the stream. If you are using file or dict configuration and
22 can't import this directly, you can refer to it as
23 ``ext://flask.logging.wsgi_errors_stream``.
24 """
25 if request:
26 return request.environ["wsgi.errors"] # type: ignore[no-any-return]
27
28 return sys.stderr
29
30
31 def has_level_handler(logger: logging.Logger) -> bool:
32 """Check if there is a handler in the logging chain that will handle the
190
src/flask/__init__.py
1 from __future__ import annotations
2
3 import typing as t
4
5 from . import json as json
6 from .app import Flask as Flask
7 from .blueprints import Blueprint as Blueprint
8 from .config import Config as Config
9 from .ctx import after_this_request as after_this_request
10 from .ctx import copy_current_request_context as copy_current_request_context
11 from .ctx import has_app_context as has_app_context
12 from .ctx import has_request_context as has_request_context
13 from .globals import current_app as current_app
14 from .globals import g as g
15 from .globals import request as request
16 from .globals import session as session
17 from .helpers import abort as abort
18 from .helpers import flash as flash
19 from .helpers import get_flashed_messages as get_flashed_messages
20 from .helpers import get_template_attribute as get_template_attribute
191
src/flask/typing.py
1 from __future__ import annotations
2
3 import typing as t
4
5 if t.TYPE_CHECKING: # pragma: no cover
6 from _typeshed.wsgi import WSGIApplication # noqa: F401
7 from werkzeug.datastructures import Headers # noqa: F401
8 from werkzeug.sansio.response import Response # noqa: F401
9
10 # The possible types that are directly convertible or are a Response object.
11 ResponseValue = t.Union[
12 "Response",
13 str,
14 bytes,
15 list[t.Any],
16 # Only dict is actually accepted, but Mapping allows for TypedDict.
17 t.Mapping[str, t.Any],
18 t.Iterator[str],
19 t.Iterator[bytes],
20 ]
21
22 # the possible types for an individual HTTP header
23 # This should be a Union, but mypy doesn't pass unless it's a TypeVar.
24 HeaderValue = t.Union[str, list[str], tuple[str, ...]]
25
26 # the possible types for HTTP headers
27 HeadersValue = t.Union[
192
28 "Headers",
29 t.Mapping[str, HeaderValue],
30 t.Sequence[tuple[str, HeaderValue]],
31 ]
32
33 # The possible types returned by a route function.
34 ResponseReturnValue = t.Union[
35 ResponseValue,
36 tuple[ResponseValue, HeadersValue],
37 tuple[ResponseValue, int],
38 tuple[ResponseValue, int, HeadersValue],
39 "WSGIApplication",
40 ]
41
42 # Allow any subclass of werkzeug.Response, such as the one from Flask,
43 # as a callback argument. Using werkzeug.Response directly makes a
44 # callback annotated with flask.Response fail type checking.
45 ResponseClass = t.TypeVar("ResponseClass", bound="Response")
46
47 AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named
48 AfterRequestCallable = t.Union[
49 t.Callable[[ResponseClass], ResponseClass],
50 t.Callable[[ResponseClass], t.Awaitable[ResponseClass]],
51 ]
52 BeforeFirstRequestCallable = t.Union[
53 t.Callable[[], None], t.Callable[[], t.Awaitable[None]]
54 ]
55 BeforeRequestCallable = t.Union[
56 t.Callable[[], t.Optional[ResponseReturnValue]],
57 t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]],
58 ]
59 ShellContextProcessorCallable = t.Callable[[], dict[str, t.Any]]
60 TeardownCallable = t.Union[
61 t.Callable[[t.Optional[BaseException]], None],
62 t.Callable[[t.Optional[BaseException]], t.Awaitable[None]],
63 ]
64 TemplateContextProcessorCallable = t.Union[
65 t.Callable[[], dict[str, t.Any]],
66 t.Callable[[], t.Awaitable[dict[str, t.Any]]],
67 ]
68 TemplateFilterCallable = t.Callable[..., t.Any]
69 TemplateGlobalCallable = t.Callable[..., t.Any]
70 TemplateTestCallable = t.Callable[..., bool]
71 URLDefaultCallable = t.Callable[[str, dict[str, t.Any]], None]
72 URLValuePreprocessorCallable = t.Callable[
73 [t.Optional[str], t.Optional[dict[str, t.Any]]], None
74 ]
75
76 # This should take Exception, but that either breaks typing the argument
77 # with a specific exception, or decorating multiple times with different
78 # exceptions (and using a union type on the argument).
79 # https://github.com/pallets/flask/issues/4095
80 # https://github.com/pallets/flask/issues/4295
81 # https://github.com/pallets/flask/issues/4297
82 ErrorHandlerCallable = t.Union[
83 t.Callable[[t.Any], ResponseReturnValue],
84 t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]],
85 ]
86
87 RouteCallable = t.Union[
88 t.Callable[..., ResponseReturnValue],
89 t.Callable[..., t.Awaitable[ResponseReturnValue]],
90 ]
src/flask/blueprints.py
1 from __future__ import annotations
2
3 import os
4 import typing as t
src/flask/blueprints.py 193
76
77 if isinstance(value, timedelta):
78 return int(value.total_seconds())
79
80 return value # type: ignore[no-any-return]
81
82 def send_static_file(self, filename: str) -> Response:
83 """The view function used to serve files from
84 :attr:`static_folder`. A route is automatically registered for
85 this view at :attr:`static_url_path` if :attr:`static_folder` is
86 set.
87
88 Note this is a duplicate of the same method in the Flask
89 class.
90
91 .. versionadded:: 0.5
92
93 """
94 if not self.has_static_folder:
95 raise RuntimeError("'static_folder' must be set to serve static_files.")
96
97 # send_file only knows to call get_send_file_max_age on the app,
98 # call it here so it works for blueprints too.
99 max_age = self.get_send_file_max_age(filename)
100 return send_from_directory(
101 t.cast(str, self.static_folder), filename, max_age=max_age
102 )
103
104 def open_resource(
105 self, resource: str, mode: str = "rb", encoding: str | None = "utf-8"
106 ) -> t.IO[t.AnyStr]:
107 """Open a resource file relative to :attr:`root_path` for reading. The
108 blueprint-relative equivalent of the app's :meth:`~.Flask.open_resource`
109 method.
110
111 :param resource: Path to the resource relative to :attr:`root_path`.
112 :param mode: Open the file in this mode. Only reading is supported,
113 valid values are ``"r"`` (or ``"rt"``) and ``"rb"``.
114 :param encoding: Open the file with this encoding when opening in text
115 mode. This is ignored when opening in binary mode.
116
117 .. versionchanged:: 3.1
118 Added the ``encoding`` parameter.
119 """
120 if mode not in {"r", "rt", "rb"}:
121 raise ValueError("Resources can only be opened for reading.")
122
123 path = os.path.join(self.root_path, resource)
124
125 if mode == "rb":
126 return open(path, mode) # pyright: ignore
127
128 return open(path, mode, encoding=encoding)
src/flask/debughelpers.py
1 from __future__ import annotations
2
3 import typing as t
4
5 from jinja2.loaders import BaseLoader
6 from werkzeug.routing import RequestRedirect
7
8 from .blueprints import Blueprint
9 from .globals import request_ctx
10 from .sansio.app import App
11
12 if t.TYPE_CHECKING:
13 from .sansio.scaffold import Scaffold
14 from .wrappers import Request
src/flask/debughelpers.py 195
15
16
17 class UnexpectedUnicodeError(AssertionError, UnicodeError):
18 """Raised in places where we want some better error reporting for
19 unexpected unicode or binary data.
20 """
21
22
23 class DebugFilesKeyError(KeyError, AssertionError):
24 """Raised from request.files during debugging. The idea is that it can
25 provide a better error message than just a generic KeyError/BadRequest.
26 """
27
28 def __init__(self, request: Request, key: str) -> None:
29 form_matches = request.form.getlist(key)
30 buf = [
31 f"You tried to access the file {key!r} in the request.files"
32 " dictionary but it does not exist. The mimetype for the"
33 f" request is {request.mimetype!r} instead of"
34 " 'multipart/form-data' which means that no file contents"
35 " were transmitted. To fix this error you should provide"
36 ' enctype="multipart/form-data" in your form.'
37 ]
38 if form_matches:
39 names = ", ".join(repr(x) for x in form_matches)
40 buf.append(
41 "\n\nThe browser instead transmitted some file names. "
42 f"This was submitted: {names}"
43 )
44 self.msg = "".join(buf)
45
46 def __str__(self) -> str:
47 return self.msg
48
49
50 class FormDataRoutingRedirect(AssertionError):
51 """This exception is raised in debug mode if a routing redirect
52 would cause the browser to drop the method or body. This happens
53 when method is not GET, HEAD or OPTIONS and the status code is not
54 307 or 308.
55 """
56
57 def __init__(self, request: Request) -> None:
58 exc = request.routing_exception
59 assert isinstance(exc, RequestRedirect)
60 buf = [
61 f"A request was sent to '{request.url}', but routing issued"
62 f" a redirect to the canonical URL '{exc.new_url}'."
63 ]
64
65 if f"{request.base_url}/" == exc.new_url.partition("?")[0]:
66 buf.append(
67 " The URL was defined with a trailing slash. Flask"
68 " will redirect to the URL with a trailing slash if it"
69 " was accessed without one."
70 )
71
72 buf.append(
73 " Send requests to the canonical URL, or use 307 or 308 for"
74 " routing redirects. Otherwise, browsers will drop form"
75 " data.\n\n"
76 "This exception is only raised in debug mode."
77 )
78 super().__init__("".join(buf))
79
80
81 def attach_enctype_error_multidict(request: Request) -> None:
82 """Patch ``request.files.__getitem__`` to raise a descriptive error
83 about ``enctype=multipart/form-data``.
84
85 :param request: The request to patch.
src/flask/debughelpers.py 196
86 :meta private:
87 """
88 oldcls = request.files.__class__
89
90 class newcls(oldcls): # type: ignore[valid-type, misc]
91 def __getitem__(self, key: str) -> t.Any:
92 try:
93 return super().__getitem__(key)
94 except KeyError as e:
95 if key not in request.form:
96 raise
97
98 raise DebugFilesKeyError(request, key).with_traceback(
99 e.__traceback__
100 ) from None
101
102 newcls.__name__ = oldcls.__name__
103 newcls.__module__ = oldcls.__module__
104 request.files.__class__ = newcls
105
106
107 def _dump_loader_info(loader: BaseLoader) -> t.Iterator[str]:
108 yield f"class: {type(loader).__module__}.{type(loader).__name__}"
109 for key, value in sorted(loader.__dict__.items()):
110 if key.startswith("_"):
111 continue
112 if isinstance(value, (tuple, list)):
113 if not all(isinstance(x, str) for x in value):
114 continue
115 yield f"{key}:"
116 for item in value:
117 yield f" - {item}"
118 continue
119 elif not isinstance(value, (str, int, float, bool)):
120 continue
121 yield f"{key}: {value!r}"
122
123
124 def explain_template_loading_attempts(
125 app: App,
126 template: str,
127 attempts: list[
128 tuple[
129 BaseLoader,
130 Scaffold,
131 tuple[str, str | None, t.Callable[[], bool] | None] | None,
132 ]
133 ],
134 ) -> None:
135 """This should help developers understand what failed"""
136 info = [f"Locating template {template!r}:"]
137 total_found = 0
138 blueprint = None
139 if request_ctx and request_ctx.request.blueprint is not None:
140 blueprint = request_ctx.request.blueprint
141
142 for idx, (loader, srcobj, triple) in enumerate(attempts):
143 if isinstance(srcobj, App):
144 src_info = f"application {srcobj.import_name!r}"
145 elif isinstance(srcobj, Blueprint):
146 src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})"
147 else:
148 src_info = repr(srcobj)
149
150 info.append(f"{idx + 1:5}: trying loader of {src_info}")
151
152 for line in _dump_loader_info(loader):
153 info.append(f" {line}")
154
155 if triple is None:
156 detail = "no match"
197
157 else:
158 detail = f"found ({triple[1] or '<string>'!r})"
159 total_found += 1
160 info.append(f" -> {detail}")
161
162 seems_fishy = False
163 if total_found == 0:
164 info.append("Error: the template could not be found.")
165 seems_fishy = True
166 elif total_found > 1:
167 info.append("Warning: multiple loaders returned a match for the template.")
168 seems_fishy = True
169
170 if blueprint is not None and seems_fishy:
171 info.append(
172 " The template was looked up from an endpoint that belongs"
173 f" to the blueprint {blueprint!r}."
174 )
175 info.append(" Maybe you did not place a template in the right folder?")
176 info.append(" See https://flask.palletsprojects.com/blueprints/#templates")
177
178 app.logger.info("\n".join(info))
src/flask/views.py
1 from __future__ import annotations
2
3 import typing as t
4
5 from . import typing as ft
6 from .globals import current_app
7 from .globals import request
8
9 F = t.TypeVar("F", bound=t.Callable[..., t.Any])
10
11 http_method_funcs = frozenset(
12 ["get", "post", "head", "options", "delete", "put", "trace", "patch"]
13 )
14
15
16 class View:
17 """Subclass this class and override :meth:`dispatch_request` to
18 create a generic class-based view. Call :meth:`as_view` to create a
19 view function that creates an instance of the class with the given
20 arguments and calls its ``dispatch_request`` method with any URL
21 variables.
22
23 See :doc:`views` for a detailed guide.
24
25 .. code-block:: python
26
27 class Hello(View):
28 init_every_request = False
29
30 def dispatch_request(self, name):
31 return f"Hello, {name}!"
32
33 app.add_url_rule(
34 "/hello/<name>", view_func=Hello.as_view("hello")
35 )
36
37 Set :attr:`methods` on the class to change what methods the view
38 accepts.
39
40 Set :attr:`decorators` on the class to apply a list of decorators to
41 the generated view function. Decorators applied to the class itself
42 will not be applied to the generated view function!
43
44 Set :attr:`init_every_request` to ``False`` for efficiency, unless
45 you need to store request-global data on ``self``.
src/flask/views.py 198
46 """
47
48 #: The methods this view is registered for. Uses the same default
49 #: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and
50 #: ``add_url_rule`` by default.
51 methods: t.ClassVar[t.Collection[str] | None] = None
52
53 #: Control whether the ``OPTIONS`` method is handled automatically.
54 #: Uses the same default (``True``) as ``route`` and
55 #: ``add_url_rule`` by default.
56 provide_automatic_options: t.ClassVar[bool | None] = None
57
58 #: A list of decorators to apply, in order, to the generated view
59 #: function. Remember that ``@decorator`` syntax is applied bottom
60 #: to top, so the first decorator in the list would be the bottom
61 #: decorator.
62 #:
63 #: .. versionadded:: 0.8
64 decorators: t.ClassVar[list[t.Callable[..., t.Any]]] = []
65
66 #: Create a new instance of this view class for every request by
67 #: default. If a view subclass sets this to ``False``, the same
68 #: instance is used for every request.
69 #:
70 #: A single instance is more efficient, especially if complex setup
71 #: is done during init. However, storing data on ``self`` is no
72 #: longer safe across requests, and :data:`~flask.g` should be used
73 #: instead.
74 #:
75 #: .. versionadded:: 2.2
76 init_every_request: t.ClassVar[bool] = True
77
78 def dispatch_request(self) -> ft.ResponseReturnValue:
79 """The actual view function behavior. Subclasses must override
80 this and return a valid response. Any variables from the URL
81 rule are passed as keyword arguments.
82 """
83 raise NotImplementedError()
84
85 @classmethod
86 def as_view(
87 cls, name: str, *class_args: t.Any, **class_kwargs: t.Any
88 ) -> ft.RouteCallable:
89 """Convert the class into a view function that can be registered
90 for a route.
91
92 By default, the generated view will create a new instance of the
93 view class for every request and call its
94 :meth:`dispatch_request` method. If the view class sets
95 :attr:`init_every_request` to ``False``, the same instance will
96 be used for every request.
97
98 Except for ``name``, all other arguments passed to this method
99 are forwarded to the view class ``__init__`` method.
100
101 .. versionchanged:: 2.2
102 Added the ``init_every_request`` class attribute.
103 """
104 if cls.init_every_request:
105
106 def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
107 self = view.view_class( # type: ignore[attr-defined]
108 *class_args, **class_kwargs
109 )
110 return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return]
111
112 else:
113 self = cls(*class_args, **class_kwargs) # pyright: ignore
114
115 def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
116 return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return]
src/flask/views.py 199
117
118 if cls.decorators:
119 view.__name__ = name
120 view.__module__ = cls.__module__
121 for decorator in cls.decorators:
122 view = decorator(view)
123
124 # We attach the view class to the view function for two reasons:
125 # first of all it allows us to easily figure out what class-based
126 # view this thing came from, secondly it's also used for instantiating
127 # the view class so you can actually replace it with something else
128 # for testing purposes and debugging.
129 view.view_class = cls # type: ignore
130 view.__name__ = name
131 view.__doc__ = cls.__doc__
132 view.__module__ = cls.__module__
133 view.methods = cls.methods # type: ignore
134 view.provide_automatic_options = cls.provide_automatic_options # type: ignore
135 return view
136
137
138 class MethodView(View):
139 """Dispatches request methods to the corresponding instance methods.
140 For example, if you implement a ``get`` method, it will be used to
141 handle ``GET`` requests.
142
143 This can be useful for defining a REST API.
144
145 :attr:`methods` is automatically set based on the methods defined on
146 the class.
147
148 See :doc:`views` for a detailed guide.
149
150 .. code-block:: python
151
152 class CounterAPI(MethodView):
153 def get(self):
154 return str(session.get("counter", 0))
155
156 def post(self):
157 session["counter"] = session.get("counter", 0) + 1
158 return redirect(url_for("counter"))
159
160 app.add_url_rule(
161 "/counter", view_func=CounterAPI.as_view("counter")
162 )
163 """
164
165 def __init_subclass__(cls, **kwargs: t.Any) -> None:
166 super().__init_subclass__(**kwargs)
167
168 if "methods" not in cls.__dict__:
169 methods = set()
170
171 for base in cls.__bases__:
172 if getattr(base, "methods", None):
173 methods.update(base.methods) # type: ignore[attr-defined]
174
175 for key in http_method_funcs:
176 if hasattr(cls, key):
177 methods.add(key.upper())
178
179 if methods:
180 cls.methods = methods
181
182 def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue:
183 meth = getattr(self, request.method.lower(), None)
184
185 # If the request method is HEAD and we don't have a handler for it
186 # retry with GET.
187 if meth is None and request.method == "HEAD":
200
src/flask/templating.py
1 from __future__ import annotations
2
3 import typing as t
4
5 from jinja2 import BaseLoader
6 from jinja2 import Environment as BaseEnvironment
7 from jinja2 import Template
8 from jinja2 import TemplateNotFound
9
10 from .globals import _cv_app
11 from .globals import _cv_request
12 from .globals import current_app
13 from .globals import request
14 from .helpers import stream_with_context
15 from .signals import before_render_template
16 from .signals import template_rendered
17
18 if t.TYPE_CHECKING: # pragma: no cover
19 from .app import Flask
20 from .sansio.app import App
21 from .sansio.scaffold import Scaffold
22
23
24 def _default_template_ctx_processor() -> dict[str, t.Any]:
25 """Default template context processor. Injects `request`,
26 `session` and `g`.
27 """
28 appctx = _cv_app.get(None)
29 reqctx = _cv_request.get(None)
30 rv: dict[str, t.Any] = {}
31 if appctx is not None:
32 rv["g"] = appctx.g
33 if reqctx is not None:
34 rv["request"] = reqctx.request
35 rv["session"] = reqctx.session
36 return rv
37
38
39 class Environment(BaseEnvironment):
40 """Works like a regular Jinja2 environment but has some additional
41 knowledge of how Flask's blueprint works so that it can prepend the
42 name of the blueprint to referenced templates if necessary.
43 """
44
45 def __init__(self, app: App, **options: t.Any) -> None:
46 if "loader" not in options:
47 options["loader"] = app.create_global_jinja_loader()
48 BaseEnvironment.__init__(self, **options)
49 self.app = app
50
51
52 class DispatchingJinjaLoader(BaseLoader):
53 """A loader that looks for templates in the application and all
54 the blueprint folders.
55 """
56
57 def __init__(self, app: App) -> None:
58 self.app = app
59
60 def get_source(
61 self, environment: BaseEnvironment, template: str
62 ) -> tuple[str, str | None, t.Callable[[], bool] | None]:
63 if self.app.config["EXPLAIN_TEMPLATE_LOADING"]:
src/flask/templating.py 201
135 return rv
136
137
138 def render_template(
139 template_name_or_list: str | Template | list[str | Template],
140 **context: t.Any,
141 ) -> str:
142 """Render a template by name with the given context.
143
144 :param template_name_or_list: The name of the template to render. If
145 a list is given, the first name to exist will be rendered.
146 :param context: The variables to make available in the template.
147 """
148 app = current_app._get_current_object() # type: ignore[attr-defined]
149 template = app.jinja_env.get_or_select_template(template_name_or_list)
150 return _render(app, template, context)
151
152
153 def render_template_string(source: str, **context: t.Any) -> str:
154 """Render a template from the given source string with the given
155 context.
156
157 :param source: The source code of the template to render.
158 :param context: The variables to make available in the template.
159 """
160 app = current_app._get_current_object() # type: ignore[attr-defined]
161 template = app.jinja_env.from_string(source)
162 return _render(app, template, context)
163
164
165 def _stream(
166 app: Flask, template: Template, context: dict[str, t.Any]
167 ) -> t.Iterator[str]:
168 app.update_template_context(context)
169 before_render_template.send(
170 app, _async_wrapper=app.ensure_sync, template=template, context=context
171 )
172
173 def generate() -> t.Iterator[str]:
174 yield from template.generate(context)
175 template_rendered.send(
176 app, _async_wrapper=app.ensure_sync, template=template, context=context
177 )
178
179 rv = generate()
180
181 # If a request context is active, keep it while generating.
182 if request:
183 rv = stream_with_context(rv)
184
185 return rv
186
187
188 def stream_template(
189 template_name_or_list: str | Template | list[str | Template],
190 **context: t.Any,
191 ) -> t.Iterator[str]:
192 """Render a template by name with the given context as a stream.
193 This returns an iterator of strings, which can be used as a
194 streaming response from a view.
195
196 :param template_name_or_list: The name of the template to render. If
197 a list is given, the first name to exist will be rendered.
198 :param context: The variables to make available in the template.
199
200 .. versionadded:: 2.2
201 """
202 app = current_app._get_current_object() # type: ignore[attr-defined]
203 template = app.jinja_env.get_or_select_template(template_name_or_list)
204 return _stream(app, template, context)
205
203
206
207 def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]:
208 """Render a template from the given source string with the given
209 context as a stream. This returns an iterator of strings, which can
210 be used as a streaming response from a view.
211
212 :param source: The source code of the template to render.
213 :param context: The variables to make available in the template.
214
215 .. versionadded:: 2.2
216 """
217 app = current_app._get_current_object() # type: ignore[attr-defined]
218 template = app.jinja_env.from_string(source)
219 return _stream(app, template, context)
src/flask/wrappers.py
1 from __future__ import annotations
2
3 import typing as t
4
5 from werkzeug.exceptions import BadRequest
6 from werkzeug.exceptions import HTTPException
7 from werkzeug.wrappers import Request as RequestBase
8 from werkzeug.wrappers import Response as ResponseBase
9
10 from . import json
11 from .globals import current_app
12 from .helpers import _split_blueprint_path
13
14 if t.TYPE_CHECKING: # pragma: no cover
15 from werkzeug.routing import Rule
16
17
18 class Request(RequestBase):
19 """The request object used by default in Flask. Remembers the
20 matched endpoint and view arguments.
21
22 It is what ends up as :class:`~flask.request`. If you want to replace
23 the request object used you can subclass this and set
24 :attr:`~flask.Flask.request_class` to your subclass.
25
26 The request object is a :class:`~werkzeug.wrappers.Request` subclass and
27 provides all of the attributes Werkzeug defines plus a few Flask
28 specific ones.
29 """
30
31 json_module: t.Any = json
32
33 #: The internal URL rule that matched the request. This can be
34 #: useful to inspect which methods are allowed for the URL from
35 #: a before/after handler (``request.url_rule.methods``) etc.
36 #: Though if the request's method was invalid for the URL rule,
37 #: the valid list is available in ``routing_exception.valid_methods``
38 #: instead (an attribute of the Werkzeug exception
39 #: :exc:`~werkzeug.exceptions.MethodNotAllowed`)
40 #: because the request was never internally bound.
41 #:
42 #: .. versionadded:: 0.6
43 url_rule: Rule | None = None
44
45 #: A dict of view arguments that matched the request. If an exception
46 #: happened when matching, this will be ``None``.
47 view_args: dict[str, t.Any] | None = None
48
49 #: If matching the URL failed, this is the exception that will be
50 #: raised / was raised as part of the request handling. This is
51 #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or
52 #: something similar.
53 routing_exception: HTTPException | None = None
src/flask/wrappers.py 204
54
55 _max_content_length: int | None = None
56 _max_form_memory_size: int | None = None
57 _max_form_parts: int | None = None
58
59 @property
60 def max_content_length(self) -> int | None:
61 """The maximum number of bytes that will be read during this request. If
62 this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge`
63 error is raised. If it is set to ``None``, no limit is enforced at the
64 Flask application level. However, if it is ``None`` and the request has
65 no ``Content-Length`` header and the WSGI server does not indicate that
66 it terminates the stream, then no data is read to avoid an infinite
67 stream.
68
69 Each request defaults to the :data:`MAX_CONTENT_LENGTH` config, which
70 defaults to ``None``. It can be set on a specific ``request`` to apply
71 the limit to that specific view. This should be set appropriately based
72 on an application's or view's specific needs.
73
74 .. versionchanged:: 3.1
75 This can be set per-request.
76
77 .. versionchanged:: 0.6
78 This is configurable through Flask config.
79 """
80 if self._max_content_length is not None:
81 return self._max_content_length
82
83 if not current_app:
84 return super().max_content_length
85
86 return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return]
87
88 @max_content_length.setter
89 def max_content_length(self, value: int | None) -> None:
90 self._max_content_length = value
91
92 @property
93 def max_form_memory_size(self) -> int | None:
94 """The maximum size in bytes any non-file form field may be in a
95 ``multipart/form-data`` body. If this limit is exceeded, a 413
96 :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it
97 is set to ``None``, no limit is enforced at the Flask application level.
98
99 Each request defaults to the :data:`MAX_FORM_MEMORY_SIZE` config, which
100 defaults to ``500_000``. It can be set on a specific ``request`` to
101 apply the limit to that specific view. This should be set appropriately
102 based on an application's or view's specific needs.
103
104 .. versionchanged:: 3.1
105 This is configurable through Flask config.
106 """
107 if self._max_form_memory_size is not None:
108 return self._max_form_memory_size
109
110 if not current_app:
111 return super().max_form_memory_size
112
113 return current_app.config["MAX_FORM_MEMORY_SIZE"] # type: ignore[no-any-return]
114
115 @max_form_memory_size.setter
116 def max_form_memory_size(self, value: int | None) -> None:
117 self._max_form_memory_size = value
118
119 @property # type: ignore[override]
120 def max_form_parts(self) -> int | None:
121 """The maximum number of fields that may be present in a
122 ``multipart/form-data`` body. If this limit is exceeded, a 413
123 :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it
124 is set to ``None``, no limit is enforced at the Flask application level.
src/flask/wrappers.py 205
125
126 Each request defaults to the :data:`MAX_FORM_PARTS` config, which
127 defaults to ``1_000``. It can be set on a specific ``request`` to apply
128 the limit to that specific view. This should be set appropriately based
129 on an application's or view's specific needs.
130
131 .. versionchanged:: 3.1
132 This is configurable through Flask config.
133 """
134 if self._max_form_parts is not None:
135 return self._max_form_parts
136
137 if not current_app:
138 return super().max_form_parts
139
140 return current_app.config["MAX_FORM_PARTS"] # type: ignore[no-any-return]
141
142 @max_form_parts.setter
143 def max_form_parts(self, value: int | None) -> None:
144 self._max_form_parts = value
145
146 @property
147 def endpoint(self) -> str | None:
148 """The endpoint that matched the request URL.
149
150 This will be ``None`` if matching failed or has not been
151 performed yet.
152
153 This in combination with :attr:`view_args` can be used to
154 reconstruct the same URL or a modified URL.
155 """
156 if self.url_rule is not None:
157 return self.url_rule.endpoint # type: ignore[no-any-return]
158
159 return None
160
161 @property
162 def blueprint(self) -> str | None:
163 """The registered name of the current blueprint.
164
165 This will be ``None`` if the endpoint is not part of a
166 blueprint, or if URL matching failed or has not been performed
167 yet.
168
169 This does not necessarily match the name the blueprint was
170 created with. It may have been nested, or registered with a
171 different name.
172 """
173 endpoint = self.endpoint
174
175 if endpoint is not None and "." in endpoint:
176 return endpoint.rpartition(".")[0]
177
178 return None
179
180 @property
181 def blueprints(self) -> list[str]:
182 """The registered names of the current blueprint upwards through
183 parent blueprints.
184
185 This will be an empty list if there is no current blueprint, or
186 if URL matching failed.
187
188 .. versionadded:: 2.0.1
189 """
190 name = self.blueprint
191
192 if name is None:
193 return []
194
195 return _split_blueprint_path(name)
206
196
197 def _load_form_data(self) -> None:
198 super()._load_form_data()
199
200 # In debug mode we're replacing the files multidict with an ad-hoc
201 # subclass that raises a different error for key errors.
202 if (
203 current_app
204 and current_app.debug
205 and self.mimetype != "multipart/form-data"
206 and not self.files
207 ):
208 from .debughelpers import attach_enctype_error_multidict
209
210 attach_enctype_error_multidict(self)
211
212 def on_json_loading_failed(self, e: ValueError | None) -> t.Any:
213 try:
214 return super().on_json_loading_failed(e)
215 except BadRequest as ebr:
216 if current_app and current_app.debug:
217 raise
218
219 raise BadRequest() from ebr
220
221
222 class Response(ResponseBase):
223 """The response object that is used by default in Flask. Works like the
224 response object from Werkzeug but is set to have an HTML mimetype by
225 default. Quite often you don't have to create this object yourself because
226 :meth:`~flask.Flask.make_response` will take care of that for you.
227
228 If you want to replace the response object used you can subclass this and
229 set :attr:`~flask.Flask.response_class` to your subclass.
230
231 .. versionchanged:: 1.0
232 JSON support is added to the response, like the request. This is useful
233 when testing to get the test client response data as JSON.
234
235 .. versionchanged:: 1.0
236
237 Added :attr:`max_cookie_size`.
238 """
239
240 default_mimetype: str | None = "text/html"
241
242 json_module = json
243
244 autocorrect_location_header = False
245
246 @property
247 def max_cookie_size(self) -> int: # type: ignore
248 """Read-only view of the :data:`MAX_COOKIE_SIZE` config key.
249
250 See :attr:`~werkzeug.wrappers.Response.max_cookie_size` in
251 Werkzeug's docs.
252 """
253 if current_app:
254 return current_app.config["MAX_COOKIE_SIZE"] # type: ignore[no-any-return]
255
256 # return Werkzeug's default when not in an app context
257 return super().max_cookie_size
src/flask/testing.py
1 from __future__ import annotations
2
3 import importlib.metadata
4 import typing as t
5 from contextlib import contextmanager
src/flask/testing.py 207
77 f"/{app_root.lstrip('/')}"
78 )
79 path = url.path
80
81 if url.query:
82 path = f"{path}?{url.query}"
83
84 self.app = app
85 super().__init__(path, base_url, *args, **kwargs)
86
87 def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore
88 """Serialize ``obj`` to a JSON-formatted string.
89
90 The serialization will be configured according to the config associated
91 with this EnvironBuilder's ``app``.
92 """
93 return self.app.json.dumps(obj, **kwargs)
94
95
96 _werkzeug_version = ""
97
98
99 def _get_werkzeug_version() -> str:
100 global _werkzeug_version
101
102 if not _werkzeug_version:
103 _werkzeug_version = importlib.metadata.version("werkzeug")
104
105 return _werkzeug_version
106
107
108 class FlaskClient(Client):
109 """Works like a regular Werkzeug test client but has knowledge about
110 Flask's contexts to defer the cleanup of the request context until
111 the end of a ``with`` block. For general information about how to
112 use this class refer to :class:`werkzeug.test.Client`.
113
114 .. versionchanged:: 0.12
115 `app.test_client()` includes preset default environment, which can be
116 set after instantiation of the `app.test_client()` object in
117 `client.environ_base`.
118
119 Basic usage is outlined in the :doc:`/testing` chapter.
120 """
121
122 application: Flask
123
124 def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
125 super().__init__(*args, **kwargs)
126 self.preserve_context = False
127 self._new_contexts: list[t.ContextManager[t.Any]] = []
128 self._context_stack = ExitStack()
129 self.environ_base = {
130 "REMOTE_ADDR": "127.0.0.1",
131 "HTTP_USER_AGENT": f"Werkzeug/{_get_werkzeug_version()}",
132 }
133
134 @contextmanager
135 def session_transaction(
136 self, *args: t.Any, **kwargs: t.Any
137 ) -> t.Iterator[SessionMixin]:
138 """When used in combination with a ``with`` statement this opens a
139 session transaction. This can be used to modify the session that
140 the test client uses. Once the ``with`` block is left the session is
141 stored back.
142
143 ::
144
145 with client.session_transaction() as session:
146 session['value'] = 42
147
src/flask/testing.py 209
290 """
291 if cli is None:
292 cli = self.app.cli
293
294 if "obj" not in kwargs:
295 kwargs["obj"] = ScriptInfo(create_app=lambda: self.app)
296
297 return super().invoke(cli, args, **kwargs)
src/flask/config.py
1 from __future__ import annotations
2
3 import errno
4 import json
5 import os
6 import types
7 import typing as t
8
9 from werkzeug.utils import import_string
10
11 if t.TYPE_CHECKING:
12 import typing_extensions as te
13
14 from .sansio.app import App
15
16
17 T = t.TypeVar("T")
18
19
20 class ConfigAttribute(t.Generic[T]):
21 """Makes an attribute forward to the config"""
22
23 def __init__(
24 self, name: str, get_converter: t.Callable[[t.Any], T] | None = None
25 ) -> None:
26 self.__name__ = name
27 self.get_converter = get_converter
28
29 @t.overload
30 def __get__(self, obj: None, owner: None) -> te.Self: ...
31
32 @t.overload
33 def __get__(self, obj: App, owner: type[App]) -> T: ...
34
35 def __get__(self, obj: App | None, owner: type[App] | None = None) -> T | te.Self:
36 if obj is None:
37 return self
38
39 rv = obj.config[self.__name__]
40
41 if self.get_converter is not None:
42 rv = self.get_converter(rv)
43
44 return rv # type: ignore[no-any-return]
45
46 def __set__(self, obj: App, value: t.Any) -> None:
47 obj.config[self.__name__] = value
48
49
50 class Config(dict): # type: ignore[type-arg]
51 """Works exactly like a dict but provides ways to fill it from files
52 or special dictionaries. There are two common patterns to populate the
53 config.
54
55 Either you can fill the config from a config file::
56
57 app.config.from_pyfile('yourconfig.cfg')
58
59 Or alternatively you can define the configuration options in the
src/flask/config.py 212
344
345 :param namespace: a configuration namespace
346 :param lowercase: a flag indicating if the keys of the resulting
347 dictionary should be lowercase
348 :param trim_namespace: a flag indicating if the keys of the resulting
349 dictionary should not include the namespace
350
351 .. versionadded:: 0.11
352 """
353 rv = {}
354 for k, v in self.items():
355 if not k.startswith(namespace):
356 continue
357 if trim_namespace:
358 key = k[len(namespace) :]
359 else:
360 key = k
361 if lowercase:
362 key = key.lower()
363 rv[key] = v
364 return rv
365
366 def __repr__(self) -> str:
367 return f"<{type(self).__name__} {dict.__repr__(self)}>"
src/flask/ctx.py
1 from __future__ import annotations
2
3 import contextvars
4 import sys
5 import typing as t
6 from functools import update_wrapper
7 from types import TracebackType
8
9 from werkzeug.exceptions import HTTPException
10
11 from . import typing as ft
12 from .globals import _cv_app
13 from .globals import _cv_request
14 from .signals import appcontext_popped
15 from .signals import appcontext_pushed
16
17 if t.TYPE_CHECKING: # pragma: no cover
18 from _typeshed.wsgi import WSGIEnvironment
19
20 from .app import Flask
21 from .sessions import SessionMixin
22 from .wrappers import Request
23
24
25 # a singleton sentinel value for parameter defaults
26 _sentinel = object()
27
28
29 class _AppCtxGlobals:
30 """A plain object. Used as a namespace for storing data during an
31 application context.
32
33 Creating an app context automatically creates this object, which is
34 made available as the :data:`g` proxy.
35
36 .. describe:: 'key' in g
37
38 Check whether an attribute is present.
39
40 .. versionadded:: 0.10
41
42 .. describe:: iter(g)
43
src/flask/ctx.py 217
115
116
117 def after_this_request(
118 f: ft.AfterRequestCallable[t.Any],
119 ) -> ft.AfterRequestCallable[t.Any]:
120 """Executes a function after this request. This is useful to modify
121 response objects. The function is passed the response object and has
122 to return the same or a new one.
123
124 Example::
125
126 @app.route('/')
127 def index():
128 @after_this_request
129 def add_header(response):
130 response.headers['X-Foo'] = 'Parachute'
131 return response
132 return 'Hello World!'
133
134 This is more useful if a function other than the view function wants to
135 modify a response. For instance think of a decorator that wants to add
136 some headers without converting the return value into a response object.
137
138 .. versionadded:: 0.9
139 """
140 ctx = _cv_request.get(None)
141
142 if ctx is None:
143 raise RuntimeError(
144 "'after_this_request' can only be used when a request"
145 " context is active, such as in a view function."
146 )
147
148 ctx._after_request_functions.append(f)
149 return f
150
151
152 F = t.TypeVar("F", bound=t.Callable[..., t.Any])
153
154
155 def copy_current_request_context(f: F) -> F:
156 """A helper function that decorates a function to retain the current
157 request context. This is useful when working with greenlets. The moment
158 the function is decorated a copy of the request context is created and
159 then pushed when the function is called. The current session is also
160 included in the copied request context.
161
162 Example::
163
164 import gevent
165 from flask import copy_current_request_context
166
167 @app.route('/')
168 def index():
169 @copy_current_request_context
170 def do_some_work():
171 # do some work here, it can access flask.request or
172 # flask.session like you would otherwise in the view function.
173 ...
174 gevent.spawn(do_some_work)
175 return 'Regular response'
176
177 .. versionadded:: 0.10
178 """
179 ctx = _cv_request.get(None)
180
181 if ctx is None:
182 raise RuntimeError(
183 "'copy_current_request_context' can only be used when a"
184 " request context is active, such as in a view function."
185 )
src/flask/ctx.py 219
186
187 ctx = ctx.copy()
188
189 def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
190 with ctx: # type: ignore[union-attr]
191 return ctx.app.ensure_sync(f)(*args, **kwargs) # type: ignore[union-attr]
192
193 return update_wrapper(wrapper, f) # type: ignore[return-value]
194
195
196 def has_request_context() -> bool:
197 """If you have code that wants to test if a request context is there or
198 not this function can be used. For instance, you may want to take advantage
199 of request information if the request object is available, but fail
200 silently if it is unavailable.
201
202 ::
203
204 class User(db.Model):
205
206 def __init__(self, username, remote_addr=None):
207 self.username = username
208 if remote_addr is None and has_request_context():
209 remote_addr = request.remote_addr
210 self.remote_addr = remote_addr
211
212 Alternatively you can also just test any of the context bound objects
213 (such as :class:`request` or :class:`g`) for truthness::
214
215 class User(db.Model):
216
217 def __init__(self, username, remote_addr=None):
218 self.username = username
219 if remote_addr is None and request:
220 remote_addr = request.remote_addr
221 self.remote_addr = remote_addr
222
223 .. versionadded:: 0.7
224 """
225 return _cv_request.get(None) is not None
226
227
228 def has_app_context() -> bool:
229 """Works like :func:`has_request_context` but for the application
230 context. You can also just do a boolean check on the
231 :data:`current_app` object instead.
232
233 .. versionadded:: 0.9
234 """
235 return _cv_app.get(None) is not None
236
237
238 class AppContext:
239 """The app context contains application-specific information. An app
240 context is created and pushed at the beginning of each request if
241 one is not already active. An app context is also pushed when
242 running CLI commands.
243 """
244
245 def __init__(self, app: Flask) -> None:
246 self.app = app
247 self.url_adapter = app.create_url_adapter(None)
248 self.g: _AppCtxGlobals = app.app_ctx_globals_class()
249 self._cv_tokens: list[contextvars.Token[AppContext]] = []
250
251 def push(self) -> None:
252 """Binds the app context to the current context."""
253 self._cv_tokens.append(_cv_app.set(self))
254 appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync)
255
256 def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore
src/flask/ctx.py 220
328 # Functions that should be executed after the request on the response
329 # object. These will be called before the regular "after_request"
330 # functions.
331 self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = []
332
333 self._cv_tokens: list[
334 tuple[contextvars.Token[RequestContext], AppContext | None]
335 ] = []
336
337 def copy(self) -> RequestContext:
338 """Creates a copy of this request context with the same request object.
339 This can be used to move a request context to a different greenlet.
340 Because the actual request object is the same this cannot be used to
341 move a request context to a different thread unless access to the
342 request object is locked.
343
344 .. versionadded:: 0.10
345
346 .. versionchanged:: 1.1
347 The current session object is used instead of reloading the original
348 data. This prevents `flask.session` pointing to an out-of-date object.
349 """
350 return self.__class__(
351 self.app,
352 environ=self.request.environ,
353 request=self.request,
354 session=self.session,
355 )
356
357 def match_request(self) -> None:
358 """Can be overridden by a subclass to hook into the matching
359 of the request.
360 """
361 try:
362 result = self.url_adapter.match(return_rule=True) # type: ignore
363 self.request.url_rule, self.request.view_args = result # type: ignore
364 except HTTPException as e:
365 self.request.routing_exception = e
366
367 def push(self) -> None:
368 # Before we push the request context we have to ensure that there
369 # is an application context.
370 app_ctx = _cv_app.get(None)
371
372 if app_ctx is None or app_ctx.app is not self.app:
373 app_ctx = self.app.app_context()
374 app_ctx.push()
375 else:
376 app_ctx = None
377
378 self._cv_tokens.append((_cv_request.set(self), app_ctx))
379
380 # Open the session at the moment that the request context is available.
381 # This allows a custom open_session method to use the request context.
382 # Only open a new session if this is the first time the request was
383 # pushed, otherwise stream_with_context loses the session.
384 if self.session is None:
385 session_interface = self.app.session_interface
386 self.session = session_interface.open_session(self.app, self.request)
387
388 if self.session is None:
389 self.session = session_interface.make_null_session(self.app)
390
391 # Match the request URL after loading the session, so that the
392 # session is available in custom URL converters.
393 if self.url_adapter is not None:
394 self.match_request()
395
396 def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore
397 """Pops the request context and unbinds it by doing that. This will
398 also trigger the execution of functions registered by the
222
src/flask/sessions.py
1 from __future__ import annotations
2
3 import collections.abc as c
4 import hashlib
5 import typing as t
6 from collections.abc import MutableMapping
7 from datetime import datetime
8 from datetime import timezone
9
10 from itsdangerous import BadSignature
11 from itsdangerous import URLSafeTimedSerializer
12 from werkzeug.datastructures import CallbackDict
13
14 from .json.tag import TaggedJSONSerializer
15
16 if t.TYPE_CHECKING: # pragma: no cover
src/flask/sessions.py 223
17 import typing_extensions as te
18
19 from .app import Flask
20 from .wrappers import Request
21 from .wrappers import Response
22
23
24 class SessionMixin(MutableMapping[str, t.Any]):
25 """Expands a basic dictionary with session attributes."""
26
27 @property
28 def permanent(self) -> bool:
29 """This reflects the ``'_permanent'`` key in the dict."""
30 return self.get("_permanent", False)
31
32 @permanent.setter
33 def permanent(self, value: bool) -> None:
34 self["_permanent"] = bool(value)
35
36 #: Some implementations can detect whether a session is newly
37 #: created, but that is not guaranteed. Use with caution. The mixin
38 # default is hard-coded ``False``.
39 new = False
40
41 #: Some implementations can detect changes to the session and set
42 #: this when that happens. The mixin default is hard coded to
43 #: ``True``.
44 modified = True
45
46 #: Some implementations can detect when session data is read or
47 #: written and set this when that happens. The mixin default is hard
48 #: coded to ``True``.
49 accessed = True
50
51
52 class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
53 """Base class for sessions based on signed cookies.
54
55 This session backend will set the :attr:`modified` and
56 :attr:`accessed` attributes. It cannot reliably track whether a
57 session is new (vs. empty), so :attr:`new` remains hard coded to
58 ``False``.
59 """
60
61 #: When data is changed, this is set to ``True``. Only the session
62 #: dictionary itself is tracked; if the session contains mutable
63 #: data (for example a nested dict) then this must be set to
64 #: ``True`` manually when modifying that data. The session cookie
65 #: will only be written to the response if this is ``True``.
66 modified = False
67
68 #: When data is read or written, this is set to ``True``. Used by
69 # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
70 #: header, which allows caching proxies to cache different pages for
71 #: different users.
72 accessed = False
73
74 def __init__(
75 self,
76 initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None,
77 ) -> None:
78 def on_update(self: te.Self) -> None:
79 self.modified = True
80 self.accessed = True
81
82 super().__init__(initial, on_update)
83
84 def __getitem__(self, key: str) -> t.Any:
85 self.accessed = True
86 return super().__getitem__(key)
87
src/flask/sessions.py 224
301 """
302
303 #: the salt that should be applied on top of the secret key for the
304 #: signing of cookie based sessions.
305 salt = "cookie-session"
306 #: the hash function to use for the signature. The default is sha1
307 digest_method = staticmethod(_lazy_sha1)
308 #: the name of the itsdangerous supported key derivation. The default
309 #: is hmac.
310 key_derivation = "hmac"
311 #: A python serializer for the payload. The default is a compact
312 #: JSON derived serializer with support for some extra Python types
313 #: such as datetime objects or tuples.
314 serializer = session_json_serializer
315 session_class = SecureCookieSession
316
317 def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None:
318 if not app.secret_key:
319 return None
320
321 keys: list[str | bytes] = [app.secret_key]
322
323 if fallbacks := app.config["SECRET_KEY_FALLBACKS"]:
324 keys.extend(fallbacks)
325
326 return URLSafeTimedSerializer(
327 keys, # type: ignore[arg-type]
328 salt=self.salt,
329 serializer=self.serializer,
330 signer_kwargs={
331 "key_derivation": self.key_derivation,
332 "digest_method": self.digest_method,
333 },
334 )
335
336 def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None:
337 s = self.get_signing_serializer(app)
338 if s is None:
339 return None
340 val = request.cookies.get(self.get_cookie_name(app))
341 if not val:
342 return self.session_class()
343 max_age = int(app.permanent_session_lifetime.total_seconds())
344 try:
345 data = s.loads(val, max_age=max_age)
346 return self.session_class(data)
347 except BadSignature:
348 return self.session_class()
349
350 def save_session(
351 self, app: Flask, session: SessionMixin, response: Response
352 ) -> None:
353 name = self.get_cookie_name(app)
354 domain = self.get_cookie_domain(app)
355 path = self.get_cookie_path(app)
356 secure = self.get_cookie_secure(app)
357 partitioned = self.get_cookie_partitioned(app)
358 samesite = self.get_cookie_samesite(app)
359 httponly = self.get_cookie_httponly(app)
360
361 # Add a "Vary: Cookie" header if the session was accessed at all.
362 if session.accessed:
363 response.vary.add("Cookie")
364
365 # If the session is modified to be empty, remove the cookie.
366 # If the session is empty, return without setting the cookie.
367 if not session:
368 if session.modified:
369 response.delete_cookie(
370 name,
371 domain=domain,
228
372 path=path,
373 secure=secure,
374 partitioned=partitioned,
375 samesite=samesite,
376 httponly=httponly,
377 )
378 response.vary.add("Cookie")
379
380 return
381
382 if not self.should_set_cookie(app, session):
383 return
384
385 expires = self.get_expiration_time(app, session)
386 val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore[union-attr]
387 response.set_cookie(
388 name,
389 val,
390 expires=expires,
391 httponly=httponly,
392 domain=domain,
393 path=path,
394 secure=secure,
395 partitioned=partitioned,
396 samesite=samesite,
397 )
398 response.vary.add("Cookie")
src/flask/json/__init__.py
1 from __future__ import annotations
2
3 import json as _json
4 import typing as t
5
6 from ..globals import current_app
7 from .provider import _default
8
9 if t.TYPE_CHECKING: # pragma: no cover
10 from ..wrappers import Response
11
12
13 def dumps(obj: t.Any, **kwargs: t.Any) -> str:
14 """Serialize data as JSON.
15
16 If :data:`~flask.current_app` is available, it will use its
17 :meth:`app.json.dumps() <flask.json.provider.JSONProvider.dumps>`
18 method, otherwise it will use :func:`json.dumps`.
19
20 :param obj: The data to serialize.
21 :param kwargs: Arguments passed to the ``dumps`` implementation.
22
23 .. versionchanged:: 2.3
24 The ``app`` parameter was removed.
25
26 .. versionchanged:: 2.2
27 Calls ``current_app.json.dumps``, allowing an app to override
28 the behavior.
29
30 .. versionchanged:: 2.0.2
31 :class:`decimal.Decimal` is supported by converting to a string.
32
33 .. versionchanged:: 2.0
34 ``encoding`` will be removed in Flask 2.1.
35
36 .. versionchanged:: 1.0.3
37 ``app`` can be passed directly, rather than requiring an app
38 context for configuration.
39 """
40 if current_app:
src/flask/json/__init__.py 229
src/flask/json/provider.py
1 from __future__ import annotations
2
3 import dataclasses
4 import decimal
5 import json
6 import typing as t
7 import uuid
8 import weakref
src/flask/json/provider.py 231
80
81 if not args and not kwargs:
82 return None
83
84 if len(args) == 1:
85 return args[0]
86
87 return args or kwargs
88
89 def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
90 """Serialize the given arguments as JSON, and return a
91 :class:`~flask.Response` object with the ``application/json``
92 mimetype.
93
94 The :func:`~flask.json.jsonify` function calls this method for
95 the current application.
96
97 Either positional or keyword arguments can be given, not both.
98 If no arguments are given, ``None`` is serialized.
99
100 :param args: A single value to serialize, or multiple values to
101 treat as a list to serialize.
102 :param kwargs: Treat as a dict to serialize.
103 """
104 obj = self._prepare_response_obj(args, kwargs)
105 return self._app.response_class(self.dumps(obj), mimetype="application/json")
106
107
108 def _default(o: t.Any) -> t.Any:
109 if isinstance(o, date):
110 return http_date(o)
111
112 if isinstance(o, (decimal.Decimal, uuid.UUID)):
113 return str(o)
114
115 if dataclasses and dataclasses.is_dataclass(o):
116 return dataclasses.asdict(o) # type: ignore[arg-type]
117
118 if hasattr(o, "__html__"):
119 return str(o.__html__())
120
121 raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
122
123
124 class DefaultJSONProvider(JSONProvider):
125 """Provide JSON operations using Python's built-in :mod:`json`
126 library. Serializes the following additional data types:
127
128 - :class:`datetime.datetime` and :class:`datetime.date` are
129 serialized to :rfc:`822` strings. This is the same as the HTTP
130 date format.
131 - :class:`uuid.UUID` is serialized to a string.
132 - :class:`dataclasses.dataclass` is passed to
133 :func:`dataclasses.asdict`.
134 - :class:`~markupsafe.Markup` (or any object with a ``__html__``
135 method) will call the ``__html__`` method to get a string.
136 """
137
138 default: t.Callable[[t.Any], t.Any] = staticmethod(_default) # type: ignore[assignment]
139 """Apply this function to any object that :meth:`json.dumps` does
140 not know how to serialize. It should return a valid JSON type or
141 raise a ``TypeError``.
142 """
143
144 ensure_ascii = True
145 """Replace non-ASCII characters with escape sequences. This may be
146 more compatible with some clients, but can be disabled for better
147 performance and size.
148 """
149
150 sort_keys = True
233
151 """Sort the keys in any serialized dicts. This may be useful for
152 some caching situations, but can be disabled for better performance.
153 When enabled, keys must all be strings, they are not converted
154 before sorting.
155 """
156
157 compact: bool | None = None
158 """If ``True``, or ``None`` out of debug mode, the :meth:`response`
159 output will not add indentation, newlines, or spaces. If ``False``,
160 or ``None`` in debug mode, it will use a non-compact representation.
161 """
162
163 mimetype = "application/json"
164 """The mimetype set in :meth:`response`."""
165
166 def dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
167 """Serialize data as JSON to a string.
168
169 Keyword arguments are passed to :func:`json.dumps`. Sets some
170 parameter defaults from the :attr:`default`,
171 :attr:`ensure_ascii`, and :attr:`sort_keys` attributes.
172
173 :param obj: The data to serialize.
174 :param kwargs: Passed to :func:`json.dumps`.
175 """
176 kwargs.setdefault("default", self.default)
177 kwargs.setdefault("ensure_ascii", self.ensure_ascii)
178 kwargs.setdefault("sort_keys", self.sort_keys)
179 return json.dumps(obj, **kwargs)
180
181 def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any:
182 """Deserialize data as JSON from a string or bytes.
183
184 :param s: Text or UTF-8 bytes.
185 :param kwargs: Passed to :func:`json.loads`.
186 """
187 return json.loads(s, **kwargs)
188
189 def response(self, *args: t.Any, **kwargs: t.Any) -> Response:
190 """Serialize the given arguments as JSON, and return a
191 :class:`~flask.Response` object with it. The response mimetype
192 will be "application/json" and can be changed with
193 :attr:`mimetype`.
194
195 If :attr:`compact` is ``False`` or debug mode is enabled, the
196 output will be formatted to be easier to read.
197
198 Either positional or keyword arguments can be given, not both.
199 If no arguments are given, ``None`` is serialized.
200
201 :param args: A single value to serialize, or multiple values to
202 treat as a list to serialize.
203 :param kwargs: Treat as a dict to serialize.
204 """
205 obj = self._prepare_response_obj(args, kwargs)
206 dump_args: dict[str, t.Any] = {}
207
208 if (self.compact is None and self._app.debug) or self.compact is False:
209 dump_args.setdefault("indent", 2)
210 else:
211 dump_args.setdefault("separators", (",", ":"))
212
213 return self._app.response_class(
214 f"{self.dumps(obj, **dump_args)}\n", mimetype=self.mimetype
215 )
src/flask/json/tag.py
1 """
2 Tagged JSON
src/flask/json/tag.py 234
3 ~~~~~~~~~~~
4
5 A compact representation for lossless serialization of non-standard JSON
6 types. :class:`~flask.sessions.SecureCookieSessionInterface` uses this
7 to serialize the session data, but it may be useful in other places. It
8 can be extended to support other types.
9
10 .. autoclass:: TaggedJSONSerializer
11 :members:
12
13 .. autoclass:: JSONTag
14 :members:
15
16 Let's see an example that adds support for
17 :class:`~collections.OrderedDict`. Dicts don't have an order in JSON, so
18 to handle this we will dump the items as a list of ``[key, value]``
19 pairs. Subclass :class:`JSONTag` and give it the new key ``' od'`` to
20 identify the type. The session serializer processes dicts first, so
21 insert the new tag at the front of the order since ``OrderedDict`` must
22 be processed before ``dict``.
23
24 .. code-block:: python
25
26 from flask.json.tag import JSONTag
27
28 class TagOrderedDict(JSONTag):
29 __slots__ = ('serializer',)
30 key = ' od'
31
32 def check(self, value):
33 return isinstance(value, OrderedDict)
34
35 def to_json(self, value):
36 return [[k, self.serializer.tag(v)] for k, v in iteritems(value)]
37
38 def to_python(self, value):
39 return OrderedDict(value)
40
41 app.session_interface.serializer.register(TagOrderedDict, index=0)
42 """
43
44 from __future__ import annotations
45
46 import typing as t
47 from base64 import b64decode
48 from base64 import b64encode
49 from datetime import datetime
50 from uuid import UUID
51
52 from markupsafe import Markup
53 from werkzeug.http import http_date
54 from werkzeug.http import parse_date
55
56 from ..json import dumps
57 from ..json import loads
58
59
60 class JSONTag:
61 """Base class for defining type tags for :class:`TaggedJSONSerializer`."""
62
63 __slots__ = ("serializer",)
64
65 #: The tag to mark the serialized object with. If empty, this tag is
66 #: only used as an intermediate step during tagging.
67 key: str = ""
68
69 def __init__(self, serializer: TaggedJSONSerializer) -> None:
70 """Create a tagger for the given serializer."""
71 self.serializer = serializer
72
73 def check(self, value: t.Any) -> bool:
src/flask/json/tag.py 235
145
146
147 class PassList(JSONTag):
148 __slots__ = ()
149
150 def check(self, value: t.Any) -> bool:
151 return isinstance(value, list)
152
153 def to_json(self, value: t.Any) -> t.Any:
154 return [self.serializer.tag(item) for item in value]
155
156 tag = to_json
157
158
159 class TagBytes(JSONTag):
160 __slots__ = ()
161 key = " b"
162
163 def check(self, value: t.Any) -> bool:
164 return isinstance(value, bytes)
165
166 def to_json(self, value: t.Any) -> t.Any:
167 return b64encode(value).decode("ascii")
168
169 def to_python(self, value: t.Any) -> t.Any:
170 return b64decode(value)
171
172
173 class TagMarkup(JSONTag):
174 """Serialize anything matching the :class:`~markupsafe.Markup` API by
175 having a ``__html__`` method to the result of that method. Always
176 deserializes to an instance of :class:`~markupsafe.Markup`."""
177
178 __slots__ = ()
179 key = " m"
180
181 def check(self, value: t.Any) -> bool:
182 return callable(getattr(value, "__html__", None))
183
184 def to_json(self, value: t.Any) -> t.Any:
185 return str(value.__html__())
186
187 def to_python(self, value: t.Any) -> t.Any:
188 return Markup(value)
189
190
191 class TagUUID(JSONTag):
192 __slots__ = ()
193 key = " u"
194
195 def check(self, value: t.Any) -> bool:
196 return isinstance(value, UUID)
197
198 def to_json(self, value: t.Any) -> t.Any:
199 return value.hex
200
201 def to_python(self, value: t.Any) -> t.Any:
202 return UUID(value)
203
204
205 class TagDateTime(JSONTag):
206 __slots__ = ()
207 key = " d"
208
209 def check(self, value: t.Any) -> bool:
210 return isinstance(value, datetime)
211
212 def to_json(self, value: t.Any) -> t.Any:
213 return http_date(value)
214
215 def to_python(self, value: t.Any) -> t.Any:
src/flask/json/tag.py 237
src/flask/helpers.py
1 from __future__ import annotations
2
3 import importlib.util
4 import os
5 import sys
6 import typing as t
7 from datetime import datetime
8 from functools import cache
9 from functools import update_wrapper
10
11 import werkzeug.utils
12 from werkzeug.exceptions import abort as _wz_abort
13 from werkzeug.utils import redirect as _wz_redirect
14 from werkzeug.wrappers import Response as BaseResponse
15
16 from .globals import _cv_request
17 from .globals import current_app
18 from .globals import request
19 from .globals import request_ctx
20 from .globals import session
21 from .signals import message_flashed
22
23 if t.TYPE_CHECKING: # pragma: no cover
24 from .wrappers import Response
25
26
src/flask/helpers.py 239
98 try:
99 gen = iter(generator_or_function) # type: ignore[arg-type]
100 except TypeError:
101
102 def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any:
103 gen = generator_or_function(*args, **kwargs) # type: ignore[operator]
104 return stream_with_context(gen)
105
106 return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type]
107
108 def generator() -> t.Iterator[t.AnyStr | None]:
109 ctx = _cv_request.get(None)
110 if ctx is None:
111 raise RuntimeError(
112 "'stream_with_context' can only be used when a request"
113 " context is active, such as in a view function."
114 )
115 with ctx:
116 # Dummy sentinel. Has to be inside the context block or we're
117 # not actually keeping the context around.
118 yield None
119
120 # The try/finally is here so that if someone passes a WSGI level
121 # iterator in we're still running the cleanup logic. Generators
122 # don't need that because they are closed on their destruction
123 # automatically.
124 try:
125 yield from gen
126 finally:
127 if hasattr(gen, "close"):
128 gen.close()
129
130 # The trick is to start the generator. Then the code execution runs until
131 # the first dummy None is yielded at which point the context was already
132 # pushed. This item is discarded. Then when the iteration continues the
133 # real generator is executed.
134 wrapped_g = generator()
135 next(wrapped_g)
136 return wrapped_g # type: ignore[return-value]
137
138
139 def make_response(*args: t.Any) -> Response:
140 """Sometimes it is necessary to set additional headers in a view. Because
141 views do not have to return response objects but can return a value that
142 is converted into a response object by Flask itself, it becomes tricky to
143 add headers to it. This function can be called instead of using a return
144 and you will get a response object which you can use to attach headers.
145
146 If view looked like this and you want to add a new header::
147
148 def index():
149 return render_template('index.html', foo=42)
150
151 You can now do something like this::
152
153 def index():
154 response = make_response(render_template('index.html', foo=42))
155 response.headers['X-Parachutes'] = 'parachutes are cool'
156 return response
157
158 This function accepts the very same arguments you can return from a
159 view function. This for example creates a response with a 404 error
160 code::
161
162 response = make_response(render_template('not_found.html'), 404)
163
164 The other use case of this function is to force the return value of a
165 view function into a response which is helpful with view
166 decorators::
167
168 response = make_response(view_function())
src/flask/helpers.py 241
240
241
242 def redirect(
243 location: str, code: int = 302, Response: type[BaseResponse] | None = None
244 ) -> BaseResponse:
245 """Create a redirect response object.
246
247 If :data:`~flask.current_app` is available, it will use its
248 :meth:`~flask.Flask.redirect` method, otherwise it will use
249 :func:`werkzeug.utils.redirect`.
250
251 :param location: The URL to redirect to.
252 :param code: The status code for the redirect.
253 :param Response: The response class to use. Not used when
254 ``current_app`` is active, which uses ``app.response_class``.
255
256 .. versionadded:: 2.2
257 Calls ``current_app.redirect`` if available instead of always
258 using Werkzeug's default ``redirect``.
259 """
260 if current_app:
261 return current_app.redirect(location, code=code)
262
263 return _wz_redirect(location, code=code, Response=Response)
264
265
266 def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
267 """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given
268 status code.
269
270 If :data:`~flask.current_app` is available, it will call its
271 :attr:`~flask.Flask.aborter` object, otherwise it will use
272 :func:`werkzeug.exceptions.abort`.
273
274 :param code: The status code for the exception, which must be
275 registered in ``app.aborter``.
276 :param args: Passed to the exception.
277 :param kwargs: Passed to the exception.
278
279 .. versionadded:: 2.2
280 Calls ``current_app.aborter`` if available instead of always
281 using Werkzeug's default ``abort``.
282 """
283 if current_app:
284 current_app.aborter(code, *args, **kwargs)
285
286 _wz_abort(code, *args, **kwargs)
287
288
289 def get_template_attribute(template_name: str, attribute: str) -> t.Any:
290 """Loads a macro (or variable) a template exports. This can be used to
291 invoke a macro from within Python code. If you for example have a
292 template named :file:`_cider.html` with the following contents:
293
294 .. sourcecode:: html+jinja
295
296 {% macro hello(name) %}Hello {{ name }}!{% endmacro %}
297
298 You can access this from Python code like this::
299
300 hello = get_template_attribute('_cider.html', 'hello')
301 return hello('World')
302
303 .. versionadded:: 0.2
304
305 :param template_name: the name of the template
306 :param attribute: the name of the variable of macro to access
307 """
308 return getattr(current_app.jinja_env.get_template(template_name).module, attribute)
309
310
src/flask/helpers.py 243
524
525
526 def send_from_directory(
527 directory: os.PathLike[str] | str,
528 path: os.PathLike[str] | str,
529 **kwargs: t.Any,
530 ) -> Response:
531 """Send a file from within a directory using :func:`send_file`.
532
533 .. code-block:: python
534
535 @app.route("/uploads/<path:name>")
536 def download_file(name):
537 return send_from_directory(
538 app.config['UPLOAD_FOLDER'], name, as_attachment=True
539 )
540
541 This is a secure way to serve files from a folder, such as static
542 files or uploads. Uses :func:`~werkzeug.security.safe_join` to
543 ensure the path coming from the client is not maliciously crafted to
544 point outside the specified directory.
545
546 If the final path does not point to an existing regular file,
547 raises a 404 :exc:`~werkzeug.exceptions.NotFound` error.
548
549 :param directory: The directory that ``path`` must be located under,
550 relative to the current application's root path. This *must not*
551 be a value provided by the client, otherwise it becomes insecure.
552 :param path: The path to the file to send, relative to
553 ``directory``.
554 :param kwargs: Arguments to pass to :func:`send_file`.
555
556 .. versionchanged:: 2.0
557 ``path`` replaces the ``filename`` parameter.
558
559 .. versionadded:: 2.0
560 Moved the implementation to Werkzeug. This is now a wrapper to
561 pass some Flask-specific arguments.
562
563 .. versionadded:: 0.5
564 """
565 return werkzeug.utils.send_from_directory( # type: ignore[return-value]
566 directory, path, **_prepare_send_file_kwargs(**kwargs)
567 )
568
569
570 def get_root_path(import_name: str) -> str:
571 """Find the root path of a package, or the path that contains a
572 module. If it cannot be found, returns the current working
573 directory.
574
575 Not to be confused with the value returned by :func:`find_package`.
576
577 :meta private:
578 """
579 # Module already imported and has a file attribute. Use that first.
580 mod = sys.modules.get(import_name)
581
582 if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None:
583 return os.path.dirname(os.path.abspath(mod.__file__))
584
585 # Next attempt: check the loader.
586 try:
587 spec = importlib.util.find_spec(import_name)
588
589 if spec is None:
590 raise ValueError
591 except (ImportError, ValueError):
592 loader = None
593 else:
594 loader = spec.loader
247
595
596 # Loader does not exist or we're referring to an unloaded main
597 # module or a main module without path (interactive sessions), go
598 # with the current working directory.
599 if loader is None:
600 return os.getcwd()
601
602 if hasattr(loader, "get_filename"):
603 filepath = loader.get_filename(import_name) # pyright: ignore
604 else:
605 # Fall back to imports.
606 __import__(import_name)
607 mod = sys.modules[import_name]
608 filepath = getattr(mod, "__file__", None)
609
610 # If we don't have a file path it might be because it is a
611 # namespace package. In this case pick the root path from the
612 # first module that is contained in the package.
613 if filepath is None:
614 raise RuntimeError(
615 "No root path can be found for the provided module"
616 f" {import_name!r}. This can happen because the module"
617 " came from an import hook that does not provide file"
618 " name information or because it's a namespace package."
619 " In this case the root path needs to be explicitly"
620 " provided."
621 )
622
623 # filepath is import_name.py for a module, or __init__.py for a package.
624 return os.path.dirname(os.path.abspath(filepath)) # type: ignore[no-any-return]
625
626
627 @cache
628 def _split_blueprint_path(name: str) -> list[str]:
629 out: list[str] = [name]
630
631 if "." in name:
632 out.extend(_split_blueprint_path(name.rpartition(".")[0]))
633
634 return out
src/flask/cli.py
1 from __future__ import annotations
2
3 import ast
4 import collections.abc as cabc
5 import importlib.metadata
6 import inspect
7 import os
8 import platform
9 import re
10 import sys
11 import traceback
12 import typing as t
13 from functools import update_wrapper
14 from operator import itemgetter
15 from types import ModuleType
16
17 import click
18 from click.core import ParameterSource
19 from werkzeug import run_simple
20 from werkzeug.serving import is_running_from_reloader
21 from werkzeug.utils import import_string
22
23 from .globals import current_app
24 from .helpers import get_debug_flag
25 from .helpers import get_load_dotenv
26
27 if t.TYPE_CHECKING:
src/flask/cli.py 248
28 import ssl
29
30 from _typeshed.wsgi import StartResponse
31 from _typeshed.wsgi import WSGIApplication
32 from _typeshed.wsgi import WSGIEnvironment
33
34 from .app import Flask
35
36
37 class NoAppException(click.UsageError):
38 """Raised if an application cannot be found or loaded."""
39
40
41 def find_best_app(module: ModuleType) -> Flask:
42 """Given a module instance this tries to find the best possible
43 application in the module or raises an exception.
44 """
45 from . import Flask
46
47 # Search for the most common names first.
48 for attr_name in ("app", "application"):
49 app = getattr(module, attr_name, None)
50
51 if isinstance(app, Flask):
52 return app
53
54 # Otherwise find the only object that is a Flask instance.
55 matches = [v for v in module.__dict__.values() if isinstance(v, Flask)]
56
57 if len(matches) == 1:
58 return matches[0]
59 elif len(matches) > 1:
60 raise NoAppException(
61 "Detected multiple Flask applications in module"
62 f" '{module.__name__}'. Use '{module.__name__}:name'"
63 " to specify the correct one."
64 )
65
66 # Search for app factory functions.
67 for attr_name in ("create_app", "make_app"):
68 app_factory = getattr(module, attr_name, None)
69
70 if inspect.isfunction(app_factory):
71 try:
72 app = app_factory()
73
74 if isinstance(app, Flask):
75 return app
76 except TypeError as e:
77 if not _called_with_wrong_args(app_factory):
78 raise
79
80 raise NoAppException(
81 f"Detected factory '{attr_name}' in module '{module.__name__}',"
82 " but could not call it without arguments. Use"
83 f" '{module.__name__}:{attr_name}(args)'"
84 " to specify arguments."
85 ) from e
86
87 raise NoAppException(
88 "Failed to find Flask application or factory in module"
89 f" '{module.__name__}'. Use '{module.__name__}:name'"
90 " to specify one."
91 )
92
93
94 def _called_with_wrong_args(f: t.Callable[..., Flask]) -> bool:
95 """Check whether calling a function raised a ``TypeError`` because
96 the call failed or because something in the factory raised the
97 error.
98
src/flask/cli.py 249
383
384 Custom commands (and their options) registered under ``app.cli`` or
385 ``blueprint.cli`` will always have an app context available, this
386 decorator is not required in that case.
387
388 .. versionchanged:: 2.2
389 The app context is active for subcommands as well as the
390 decorated callback. The app context is always available to
391 ``app.cli`` command and parameter callbacks.
392 """
393
394 @click.pass_context
395 def decorator(ctx: click.Context, /, *args: t.Any, **kwargs: t.Any) -> t.Any:
396 if not current_app:
397 app = ctx.ensure_object(ScriptInfo).load_app()
398 ctx.with_resource(app.app_context())
399
400 return ctx.invoke(f, *args, **kwargs)
401
402 return update_wrapper(decorator, f) # type: ignore[return-value]
403
404
405 class AppGroup(click.Group):
406 """This works similar to a regular click :class:`~click.Group` but it
407 changes the behavior of the :meth:`command` decorator so that it
408 automatically wraps the functions in :func:`with_appcontext`.
409
410 Not to be confused with :class:`FlaskGroup`.
411 """
412
413 def command( # type: ignore[override]
414 self, *args: t.Any, **kwargs: t.Any
415 ) -> t.Callable[[t.Callable[..., t.Any]], click.Command]:
416 """This works exactly like the method of the same name on a regular
417 :class:`click.Group` but it wraps callbacks in :func:`with_appcontext`
418 unless it's disabled by passing ``with_appcontext=False``.
419 """
420 wrap_for_ctx = kwargs.pop("with_appcontext", True)
421
422 def decorator(f: t.Callable[..., t.Any]) -> click.Command:
423 if wrap_for_ctx:
424 f = with_appcontext(f)
425 return super(AppGroup, self).command(*args, **kwargs)(f) # type: ignore[no-any-return]
426
427 return decorator
428
429 def group( # type: ignore[override]
430 self, *args: t.Any, **kwargs: t.Any
431 ) -> t.Callable[[t.Callable[..., t.Any]], click.Group]:
432 """This works exactly like the method of the same name on a regular
433 :class:`click.Group` but it defaults the group class to
434 :class:`AppGroup`.
435 """
436 kwargs.setdefault("cls", AppGroup)
437 return super().group(*args, **kwargs) # type: ignore[no-any-return]
438
439
440 def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None:
441 if value is None:
442 return None
443
444 info = ctx.ensure_object(ScriptInfo)
445 info.app_import_path = value
446 return value
447
448
449 # This option is eager so the app will be available if --help is given.
450 # --help is also eager, so --app must be before it in the param list.
451 # no_args_is_help bypasses eager processing, so this option must be
452 # processed manually in that case to ensure FLASK_APP gets picked up.
453 _app_option = click.Option(
src/flask/cli.py 254
525 is_eager=True,
526 expose_value=False,
527 callback=_env_file_callback,
528 )
529
530
531 class FlaskGroup(AppGroup):
532 """Special subclass of the :class:`AppGroup` group that supports
533 loading more commands from the configured Flask app. Normally a
534 developer does not have to interface with this class but there are
535 some very advanced use cases for which it makes sense to create an
536 instance of this. see :ref:`custom-scripts`.
537
538 :param add_default_commands: if this is True then the default run and
539 shell commands will be added.
540 :param add_version_option: adds the ``--version`` option.
541 :param create_app: an optional callback that is passed the script info and
542 returns the loaded app.
543 :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv`
544 files to set environment variables. Will also change the working
545 directory to the directory containing the first file found.
546 :param set_debug_flag: Set the app's debug flag.
547
548 .. versionchanged:: 3.1
549 ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files.
550
551 .. versionchanged:: 2.2
552 Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options.
553
554 .. versionchanged:: 2.2
555 An app context is pushed when running ``app.cli`` commands, so
556 ``@with_appcontext`` is no longer required for those commands.
557
558 .. versionchanged:: 1.0
559 If installed, python-dotenv will be used to load environment variables
560 from :file:`.env` and :file:`.flaskenv` files.
561 """
562
563 def __init__(
564 self,
565 add_default_commands: bool = True,
566 create_app: t.Callable[..., Flask] | None = None,
567 add_version_option: bool = True,
568 load_dotenv: bool = True,
569 set_debug_flag: bool = True,
570 **extra: t.Any,
571 ) -> None:
572 params: list[click.Parameter] = list(extra.pop("params", None) or ())
573 # Processing is done with option callbacks instead of a group
574 # callback. This allows users to make a custom group callback
575 # without losing the behavior. --env-file must come first so
576 # that it is eagerly evaluated before --app.
577 params.extend((_env_file_option, _app_option, _debug_option))
578
579 if add_version_option:
580 params.append(version_option)
581
582 if "context_settings" not in extra:
583 extra["context_settings"] = {}
584
585 extra["context_settings"].setdefault("auto_envvar_prefix", "FLASK")
586
587 super().__init__(params=params, **extra)
588
589 self.create_app = create_app
590 self.load_dotenv = load_dotenv
591 self.set_debug_flag = set_debug_flag
592
593 if add_default_commands:
594 self.add_command(run_command)
595 self.add_command(shell_command)
src/flask/cli.py 256
596 self.add_command(routes_command)
597
598 self._loaded_plugin_commands = False
599
600 def _load_plugin_commands(self) -> None:
601 if self._loaded_plugin_commands:
602 return
603
604 if sys.version_info >= (3, 10):
605 from importlib import metadata
606 else:
607 # Use a backport on Python < 3.10. We technically have
608 # importlib.metadata on 3.8+, but the API changed in 3.10,
609 # so use the backport for consistency.
610 import importlib_metadata as metadata # pyright: ignore
611
612 for ep in metadata.entry_points(group="flask.commands"):
613 self.add_command(ep.load(), ep.name)
614
615 self._loaded_plugin_commands = True
616
617 def get_command(self, ctx: click.Context, name: str) -> click.Command | None:
618 self._load_plugin_commands()
619 # Look up built-in and plugin commands, which should be
620 # available even if the app fails to load.
621 rv = super().get_command(ctx, name)
622
623 if rv is not None:
624 return rv
625
626 info = ctx.ensure_object(ScriptInfo)
627
628 # Look up commands provided by the app, showing an error and
629 # continuing if the app couldn't be loaded.
630 try:
631 app = info.load_app()
632 except NoAppException as e:
633 click.secho(f"Error: {e.format_message()}\n", err=True, fg="red")
634 return None
635
636 # Push an app context for the loaded app unless it is already
637 # active somehow. This makes the context available to parameter
638 # and command callbacks without needing @with_appcontext.
639 if not current_app or current_app._get_current_object() is not app: # type: ignore[attr-defined]
640 ctx.with_resource(app.app_context())
641
642 return app.cli.get_command(ctx, name)
643
644 def list_commands(self, ctx: click.Context) -> list[str]:
645 self._load_plugin_commands()
646 # Start with the built-in and plugin commands.
647 rv = set(super().list_commands(ctx))
648 info = ctx.ensure_object(ScriptInfo)
649
650 # Add commands provided by the app, showing an error and
651 # continuing if the app couldn't be loaded.
652 try:
653 rv.update(info.load_app().cli.list_commands(ctx))
654 except NoAppException as e:
655 # When an app couldn't be loaded, show the error message
656 # without the traceback.
657 click.secho(f"Error: {e.format_message()}\n", err=True, fg="red")
658 except Exception:
659 # When any other errors occurred during loading, show the
660 # full traceback.
661 click.secho(f"{traceback.format_exc()}\n", err=True, fg="red")
662
663 return sorted(rv)
664
665 def make_context(
666 self,
src/flask/cli.py 257
738 try:
739 import dotenv
740 except ImportError:
741 if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"):
742 click.secho(
743 " * Tip: There are .env files present. Install python-dotenv"
744 " to use them.",
745 fg="yellow",
746 err=True,
747 )
748
749 return False
750
751 data: dict[str, str | None] = {}
752
753 if load_defaults:
754 for default_name in (".flaskenv", ".env"):
755 if not (default_path := dotenv.find_dotenv(default_name, usecwd=True)):
756 continue
757
758 data |= dotenv.dotenv_values(default_path, encoding="utf-8")
759
760 if path is not None and os.path.isfile(path):
761 data |= dotenv.dotenv_values(path, encoding="utf-8")
762
763 for key, value in data.items():
764 if key in os.environ or value is None:
765 continue
766
767 os.environ[key] = value
768
769 return bool(data) # True if at least one env var was loaded.
770
771
772 def show_server_banner(debug: bool, app_import_path: str | None) -> None:
773 """Show extra startup messages the first time the server is run,
774 ignoring the reloader.
775 """
776 if is_running_from_reloader():
777 return
778
779 if app_import_path is not None:
780 click.echo(f" * Serving Flask app '{app_import_path}'")
781
782 if debug is not None:
783 click.echo(f" * Debug mode: {'on' if debug else 'off'}")
784
785
786 class CertParamType(click.ParamType):
787 """Click option type for the ``--cert`` option. Allows either an
788 existing file, the string ``'adhoc'``, or an import for a
789 :class:`~ssl.SSLContext` object.
790 """
791
792 name = "path"
793
794 def __init__(self) -> None:
795 self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True)
796
797 def convert(
798 self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None
799 ) -> t.Any:
800 try:
801 import ssl
802 except ImportError:
803 raise click.BadParameter(
804 'Using "--cert" requires Python to be compiled with SSL support.',
805 ctx,
806 param,
807 ) from None
808
src/flask/cli.py 259
809 try:
810 return self.path_type(value, param, ctx)
811 except click.BadParameter:
812 value = click.STRING(value, param, ctx).lower()
813
814 if value == "adhoc":
815 try:
816 import cryptography # noqa: F401
817 except ImportError:
818 raise click.BadParameter(
819 "Using ad-hoc certificates requires the cryptography library.",
820 ctx,
821 param,
822 ) from None
823
824 return value
825
826 obj = import_string(value, silent=True)
827
828 if isinstance(obj, ssl.SSLContext):
829 return obj
830
831 raise
832
833
834 def _validate_key(ctx: click.Context, param: click.Parameter, value: t.Any) -> t.Any:
835 """The ``--key`` option must be specified when ``--cert`` is a file.
836 Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed.
837 """
838 cert = ctx.params.get("cert")
839 is_adhoc = cert == "adhoc"
840
841 try:
842 import ssl
843 except ImportError:
844 is_context = False
845 else:
846 is_context = isinstance(cert, ssl.SSLContext)
847
848 if value is not None:
849 if is_adhoc:
850 raise click.BadParameter(
851 'When "--cert" is "adhoc", "--key" is not used.', ctx, param
852 )
853
854 if is_context:
855 raise click.BadParameter(
856 'When "--cert" is an SSLContext object, "--key" is not used.',
857 ctx,
858 param,
859 )
860
861 if not cert:
862 raise click.BadParameter('"--cert" must also be specified.', ctx, param)
863
864 ctx.params["cert"] = cert, value
865
866 else:
867 if cert and not (is_adhoc or is_context):
868 raise click.BadParameter('Required when using "--cert".', ctx, param)
869
870 return value
871
872
873 class SeparatedPathType(click.Path):
874 """Click option type that accepts a list of values separated by the
875 OS's path separator (``:``, ``;`` on Windows). Each value is
876 validated as a :class:`click.Path` type.
877 """
878
879 def convert(
src/flask/cli.py 260
880 self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None
881 ) -> t.Any:
882 items = self.split_envvar_value(value)
883 # can't call no-arg super() inside list comprehension until Python 3.12
884 super_convert = super().convert
885 return [super_convert(item, param, ctx) for item in items]
886
887
888 @click.command("run", short_help="Run a development server.")
889 @click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.")
890 @click.option("--port", "-p", default=5000, help="The port to bind to.")
891 @click.option(
892 "--cert",
893 type=CertParamType(),
894 help="Specify a certificate file to use HTTPS.",
895 is_eager=True,
896 )
897 @click.option(
898 "--key",
899 type=click.Path(exists=True, dir_okay=False, resolve_path=True),
900 callback=_validate_key,
901 expose_value=False,
902 help="The key file to use when specifying a certificate.",
903 )
904 @click.option(
905 "--reload/--no-reload",
906 default=None,
907 help="Enable or disable the reloader. By default the reloader "
908 "is active if debug is enabled.",
909 )
910 @click.option(
911 "--debugger/--no-debugger",
912 default=None,
913 help="Enable or disable the debugger. By default the debugger "
914 "is active if debug is enabled.",
915 )
916 @click.option(
917 "--with-threads/--without-threads",
918 default=True,
919 help="Enable or disable multithreading.",
920 )
921 @click.option(
922 "--extra-files",
923 default=None,
924 type=SeparatedPathType(),
925 help=(
926 "Extra files that trigger a reload on change. Multiple paths"
927 f" are separated by {os.path.pathsep!r}."
928 ),
929 )
930 @click.option(
931 "--exclude-patterns",
932 default=None,
933 type=SeparatedPathType(),
934 help=(
935 "Files matching these fnmatch patterns will not trigger a reload"
936 " on change. Multiple patterns are separated by"
937 f" {os.path.pathsep!r}."
938 ),
939 )
940 @pass_script_info
941 def run_command(
942 info: ScriptInfo,
943 host: str,
944 port: int,
945 reload: bool,
946 debugger: bool,
947 with_threads: bool,
948 cert: ssl.SSLContext | tuple[str, str | None] | t.Literal["adhoc"] | None,
949 extra_files: list[str] | None,
950 exclude_patterns: list[str] | None,
src/flask/cli.py 261
src/flask/app.py
1 from __future__ import annotations
2
3 import collections.abc as cabc
4 import os
5 import sys
6 import typing as t
7 import weakref
8 from datetime import timedelta
9 from inspect import iscoroutinefunction
10 from itertools import chain
11 from types import TracebackType
12 from urllib.parse import quote as _url_quote
13
14 import click
15 from werkzeug.datastructures import Headers
16 from werkzeug.datastructures import ImmutableDict
17 from werkzeug.exceptions import BadRequestKeyError
18 from werkzeug.exceptions import HTTPException
19 from werkzeug.exceptions import InternalServerError
20 from werkzeug.routing import BuildError
21 from werkzeug.routing import MapAdapter
22 from werkzeug.routing import RequestRedirect
23 from werkzeug.routing import RoutingException
24 from werkzeug.routing import Rule
25 from werkzeug.serving import is_running_from_reloader
26 from werkzeug.wrappers import Response as BaseResponse
src/flask/app.py 264
98 app = Flask(__name__)
99
100 .. admonition:: About the First Parameter
101
102 The idea of the first parameter is to give Flask an idea of what
103 belongs to your application. This name is used to find resources
104 on the filesystem, can be used by extensions to improve debugging
105 information and a lot more.
106
107 So it's important what you provide there. If you are using a single
108 module, `__name__` is always the correct value. If you however are
109 using a package, it's usually recommended to hardcode the name of
110 your package there.
111
112 For example if your application is defined in :file:`yourapplication/app.py`
113 you should create it with one of the two versions below::
114
115 app = Flask('yourapplication')
116 app = Flask(__name__.split('.')[0])
117
118 Why is that? The application will work even with `__name__`, thanks
119 to how resources are looked up. However it will make debugging more
120 painful. Certain extensions can make assumptions based on the
121 import name of your application. For example the Flask-SQLAlchemy
122 extension will look for the code in your application that triggered
123 an SQL query in debug mode. If the import name is not properly set
124 up, that debugging information is lost. (For example it would only
125 pick up SQL queries in `yourapplication.app` and not
126 `yourapplication.views.frontend`)
127
128 .. versionadded:: 0.7
129 The `static_url_path`, `static_folder`, and `template_folder`
130 parameters were added.
131
132 .. versionadded:: 0.8
133 The `instance_path` and `instance_relative_config` parameters were
134 added.
135
136 .. versionadded:: 0.11
137 The `root_path` parameter was added.
138
139 .. versionadded:: 1.0
140 The ``host_matching`` and ``static_host`` parameters were added.
141
142 .. versionadded:: 1.0
143 The ``subdomain_matching`` parameter was added. Subdomain
144 matching needs to be enabled manually now. Setting
145 :data:`SERVER_NAME` does not implicitly enable it.
146
147 :param import_name: the name of the application package
148 :param static_url_path: can be used to specify a different path for the
149 static files on the web. Defaults to the name
150 of the `static_folder` folder.
151 :param static_folder: The folder with static files that is served at
152 ``static_url_path``. Relative to the application ``root_path``
153 or an absolute path. Defaults to ``'static'``.
154 :param static_host: the host to use when adding the static route.
155 Defaults to None. Required when using ``host_matching=True``
156 with a ``static_folder`` configured.
157 :param host_matching: set ``url_map.host_matching`` attribute.
158 Defaults to False.
159 :param subdomain_matching: consider the subdomain relative to
160 :data:`SERVER_NAME` when matching routes. Defaults to False.
161 :param template_folder: the folder that contains the templates that should
162 be used by the application. Defaults to
163 ``'templates'`` folder in the root path of the
164 application.
165 :param instance_path: An alternative instance path for the application.
166 By default the folder ``'instance'`` next to the
167 package or module is assumed to be the instance
168 path.
src/flask/app.py 266
240 import_name=import_name,
241 static_url_path=static_url_path,
242 static_folder=static_folder,
243 static_host=static_host,
244 host_matching=host_matching,
245 subdomain_matching=subdomain_matching,
246 template_folder=template_folder,
247 instance_path=instance_path,
248 instance_relative_config=instance_relative_config,
249 root_path=root_path,
250 )
251
252 #: The Click command group for registering CLI commands for this
253 #: object. The commands are available from the ``flask`` command
254 #: once the application has been discovered and blueprints have
255 #: been registered.
256 self.cli = cli.AppGroup()
257
258 # Set the name of the Click group in case someone wants to add
259 # the app's commands to another CLI tool.
260 self.cli.name = self.name
261
262 # Add a static route using the provided static_url_path, static_host,
263 # and static_folder if there is a configured static_folder.
264 # Note we do this without checking if static_folder exists.
265 # For one, it might be created while the server is running (e.g. during
266 # development). Also, Google App Engine stores static files somewhere
267 if self.has_static_folder:
268 assert (
269 bool(static_host) == host_matching
270 ), "Invalid static_host/host_matching combination"
271 # Use a weakref to avoid creating a reference cycle between the app
272 # and the view function (see #3761).
273 self_ref = weakref.ref(self)
274 self.add_url_rule(
275 f"{self.static_url_path}/<path:filename>",
276 endpoint="static",
277 host=static_host,
278 view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950
279 )
280
281 def get_send_file_max_age(self, filename: str | None) -> int | None:
282 """Used by :func:`send_file` to determine the ``max_age`` cache
283 value for a given file path if it wasn't passed.
284
285 By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from
286 the configuration of :data:`~flask.current_app`. This defaults
287 to ``None``, which tells the browser to use conditional requests
288 instead of a timed cache, which is usually preferable.
289
290 Note this is a duplicate of the same method in the Flask
291 class.
292
293 .. versionchanged:: 2.0
294 The default configuration is ``None`` instead of 12 hours.
295
296 .. versionadded:: 0.9
297 """
298 value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"]
299
300 if value is None:
301 return None
302
303 if isinstance(value, timedelta):
304 return int(value.total_seconds())
305
306 return value # type: ignore[no-any-return]
307
308 def send_static_file(self, filename: str) -> Response:
309 """The view function used to serve files from
310 :attr:`static_folder`. A route is automatically registered for
src/flask/app.py 268
382
383 return open(path, mode, encoding=encoding)
384
385 def create_jinja_environment(self) -> Environment:
386 """Create the Jinja environment based on :attr:`jinja_options`
387 and the various Jinja-related methods of the app. Changing
388 :attr:`jinja_options` after this will have no effect. Also adds
389 Flask-related globals and filters to the environment.
390
391 .. versionchanged:: 0.11
392 ``Environment.auto_reload`` set in accordance with
393 ``TEMPLATES_AUTO_RELOAD`` configuration option.
394
395 .. versionadded:: 0.5
396 """
397 options = dict(self.jinja_options)
398
399 if "autoescape" not in options:
400 options["autoescape"] = self.select_jinja_autoescape
401
402 if "auto_reload" not in options:
403 auto_reload = self.config["TEMPLATES_AUTO_RELOAD"]
404
405 if auto_reload is None:
406 auto_reload = self.debug
407
408 options["auto_reload"] = auto_reload
409
410 rv = self.jinja_environment(self, **options)
411 rv.globals.update(
412 url_for=self.url_for,
413 get_flashed_messages=get_flashed_messages,
414 config=self.config,
415 # request, session and g are normally added with the
416 # context processor for efficiency reasons but for imported
417 # templates we also want the proxies in there.
418 request=request,
419 session=session,
420 g=g,
421 )
422 rv.policies["json.dumps_function"] = self.json.dumps
423 return rv
424
425 def create_url_adapter(self, request: Request | None) -> MapAdapter | None:
426 """Creates a URL adapter for the given request. The URL adapter
427 is created at a point where the request context is not yet set
428 up so the request is passed explicitly.
429
430 .. versionchanged:: 3.1
431 If :data:`SERVER_NAME` is set, it does not restrict requests to
432 only that domain, for both ``subdomain_matching`` and
433 ``host_matching``.
434
435 .. versionchanged:: 1.0
436 :data:`SERVER_NAME` no longer implicitly enables subdomain
437 matching. Use :attr:`subdomain_matching` instead.
438
439 .. versionchanged:: 0.9
440 This can be called outside a request when the URL adapter is created
441 for an application context.
442
443 .. versionadded:: 0.6
444 """
445 if request is not None:
446 if (trusted_hosts := self.config["TRUSTED_HOSTS"]) is not None:
447 request.trusted_hosts = trusted_hosts
448
449 # Check trusted_hosts here until bind_to_environ does.
450 request.host = get_host(request.environ, request.trusted_hosts) # pyright: ignore
451 subdomain = None
452 server_name = self.config["SERVER_NAME"]
src/flask/app.py 270
453
454 if self.url_map.host_matching:
455 # Don't pass SERVER_NAME, otherwise it's used and the actual
456 # host is ignored, which breaks host matching.
457 server_name = None
458 elif not self.subdomain_matching:
459 # Werkzeug doesn't implement subdomain matching yet. Until then,
460 # disable it by forcing the current subdomain to the default, or
461 # the empty string.
462 subdomain = self.url_map.default_subdomain or ""
463
464 return self.url_map.bind_to_environ(
465 request.environ, server_name=server_name, subdomain=subdomain
466 )
467
468 # Need at least SERVER_NAME to match/build outside a request.
469 if self.config["SERVER_NAME"] is not None:
470 return self.url_map.bind(
471 self.config["SERVER_NAME"],
472 script_name=self.config["APPLICATION_ROOT"],
473 url_scheme=self.config["PREFERRED_URL_SCHEME"],
474 )
475
476 return None
477
478 def raise_routing_exception(self, request: Request) -> t.NoReturn:
479 """Intercept routing exceptions and possibly do something else.
480
481 In debug mode, intercept a routing redirect and replace it with
482 an error if the body will be discarded.
483
484 With modern Werkzeug this shouldn't occur, since it now uses a
485 308 status which tells the browser to resend the method and
486 body.
487
488 .. versionchanged:: 2.1
489 Don't intercept 307 and 308 redirects.
490
491 :meta private:
492 :internal:
493 """
494 if (
495 not self.debug
496 or not isinstance(request.routing_exception, RequestRedirect)
497 or request.routing_exception.code in {307, 308}
498 or request.method in {"GET", "HEAD", "OPTIONS"}
499 ):
500 raise request.routing_exception # type: ignore[misc]
501
502 from .debughelpers import FormDataRoutingRedirect
503
504 raise FormDataRoutingRedirect(request)
505
506 def update_template_context(self, context: dict[str, t.Any]) -> None:
507 """Update the template context with some commonly used variables.
508 This injects request, session, config and g into the template
509 context as well as everything template context processors want
510 to inject. Note that the as of Flask 0.6, the original values
511 in the context will not be overridden if a context processor
512 decides to return a value with the same key.
513
514 :param context: the context as a dictionary that is updated in place
515 to add extra variables.
516 """
517 names: t.Iterable[str | None] = (None,)
518
519 # A template may be rendered outside a request context.
520 if request:
521 names = chain(names, reversed(request.blueprints))
522
523 # The values passed to render_template take precedence. Keep a
src/flask/app.py 271
595 information.
596
597 .. versionchanged:: 1.0
598 If installed, python-dotenv will be used to load environment
599 variables from :file:`.env` and :file:`.flaskenv` files.
600
601 The :envvar:`FLASK_DEBUG` environment variable will override :attr:`debug`.
602
603 Threaded mode is enabled by default.
604
605 .. versionchanged:: 0.10
606 The default port is now picked from the ``SERVER_NAME``
607 variable.
608 """
609 # Ignore this call so that it doesn't start another server if
610 # the 'flask run' command is used.
611 if os.environ.get("FLASK_RUN_FROM_CLI") == "true":
612 if not is_running_from_reloader():
613 click.secho(
614 " * Ignoring a call to 'app.run()' that would block"
615 " the current 'flask' CLI command.\n"
616 " Only call 'app.run()' in an 'if __name__ =="
617 ' "__main__"\' guard.',
618 fg="red",
619 )
620
621 return
622
623 if get_load_dotenv(load_dotenv):
624 cli.load_dotenv()
625
626 # if set, env var overrides existing value
627 if "FLASK_DEBUG" in os.environ:
628 self.debug = get_debug_flag()
629
630 # debug passed to method overrides all other sources
631 if debug is not None:
632 self.debug = bool(debug)
633
634 server_name = self.config.get("SERVER_NAME")
635 sn_host = sn_port = None
636
637 if server_name:
638 sn_host, _, sn_port = server_name.partition(":")
639
640 if not host:
641 if sn_host:
642 host = sn_host
643 else:
644 host = "127.0.0.1"
645
646 if port or port == 0:
647 port = int(port)
648 elif sn_port:
649 port = int(sn_port)
650 else:
651 port = 5000
652
653 options.setdefault("use_reloader", self.debug)
654 options.setdefault("use_debugger", self.debug)
655 options.setdefault("threaded", True)
656
657 cli.show_server_banner(self.debug, self.name)
658
659 from werkzeug.serving import run_simple
660
661 try:
662 run_simple(t.cast(str, host), port, self, **options)
663 finally:
664 # reset the first request information if the development server
665 # reset normally. This makes it possible to restart the server
src/flask/app.py 273
808
809 return self.ensure_sync(handler)(e) # type: ignore[no-any-return]
810
811 def handle_exception(self, e: Exception) -> Response:
812 """Handle an exception that did not have an error handler
813 associated with it, or that was raised from an error handler.
814 This always causes a 500 ``InternalServerError``.
815
816 Always sends the :data:`got_request_exception` signal.
817
818 If :data:`PROPAGATE_EXCEPTIONS` is ``True``, such as in debug
819 mode, the error will be re-raised so that the debugger can
820 display it. Otherwise, the original exception is logged, and
821 an :exc:`~werkzeug.exceptions.InternalServerError` is returned.
822
823 If an error handler is registered for ``InternalServerError`` or
824 ``500``, it will be used. For consistency, the handler will
825 always receive the ``InternalServerError``. The original
826 unhandled exception is available as ``e.original_exception``.
827
828 .. versionchanged:: 1.1.0
829 Always passes the ``InternalServerError`` instance to the
830 handler, setting ``original_exception`` to the unhandled
831 error.
832
833 .. versionchanged:: 1.1.0
834 ``after_request`` functions and other finalization is done
835 even for the default 500 response when there is no handler.
836
837 .. versionadded:: 0.3
838 """
839 exc_info = sys.exc_info()
840 got_request_exception.send(self, _async_wrapper=self.ensure_sync, exception=e)
841 propagate = self.config["PROPAGATE_EXCEPTIONS"]
842
843 if propagate is None:
844 propagate = self.testing or self.debug
845
846 if propagate:
847 # Re-raise if called with an active exception, otherwise
848 # raise the passed in exception.
849 if exc_info[1] is e:
850 raise
851
852 raise e
853
854 self.log_exception(exc_info)
855 server_error: InternalServerError | ft.ResponseReturnValue
856 server_error = InternalServerError(original_exception=e)
857 handler = self._find_error_handler(server_error, request.blueprints)
858
859 if handler is not None:
860 server_error = self.ensure_sync(handler)(server_error)
861
862 return self.finalize_request(server_error, from_error_handler=True)
863
864 def log_exception(
865 self,
866 exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]),
867 ) -> None:
868 """Logs an exception. This is called by :meth:`handle_exception`
869 if debugging is disabled and right before the handler is called.
870 The default implementation logs the exception as error on the
871 :attr:`logger`.
872
873 .. versionadded:: 0.8
874 """
875 self.logger.error(
876 f"Exception on {request.path} [{request.method}]", exc_info=exc_info
877 )
878
src/flask/app.py 276
950 )
951 return response
952
953 def make_default_options_response(self) -> Response:
954 """This method is called to create the default ``OPTIONS`` response.
955 This can be changed through subclassing to change the default
956 behavior of ``OPTIONS`` responses.
957
958 .. versionadded:: 0.7
959 """
960 adapter = request_ctx.url_adapter
961 methods = adapter.allowed_methods() # type: ignore[union-attr]
962 rv = self.response_class()
963 rv.allow.update(methods)
964 return rv
965
966 def ensure_sync(self, func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
967 """Ensure that the function is synchronous for WSGI workers.
968 Plain ``def`` functions are returned as-is. ``async def``
969 functions are wrapped to run and wait for the response.
970
971 Override this method to change how the app runs async views.
972
973 .. versionadded:: 2.0
974 """
975 if iscoroutinefunction(func):
976 return self.async_to_sync(func)
977
978 return func
979
980 def async_to_sync(
981 self, func: t.Callable[..., t.Coroutine[t.Any, t.Any, t.Any]]
982 ) -> t.Callable[..., t.Any]:
983 """Return a sync function that will run the coroutine function.
984
985 .. code-block:: python
986
987 result = app.async_to_sync(func)(*args, **kwargs)
988
989 Override this method to change how the app converts async code
990 to be synchronously callable.
991
992 .. versionadded:: 2.0
993 """
994 try:
995 from asgiref.sync import async_to_sync as asgiref_async_to_sync
996 except ImportError:
997 raise RuntimeError(
998 "Install Flask with the 'async' extra in order to use async views."
999 ) from None
1000
1001 return asgiref_async_to_sync(func)
1002
1003 def url_for(
1004 self,
1005 /,
1006 endpoint: str,
1007 *,
1008 _anchor: str | None = None,
1009 _method: str | None = None,
1010 _scheme: str | None = None,
1011 _external: bool | None = None,
1012 **values: t.Any,
1013 ) -> str:
1014 """Generate a URL to the given endpoint with the given values.
1015
1016 This is called by :func:`flask.url_for`, and can be called
1017 directly as well.
1018
1019 An *endpoint* is the name of a URL rule, usually added with
1020 :meth:`@app.route() <route>`, and usually the same name as the
src/flask/app.py 278
1163
1164 :attr:`response_class`
1165 The object is returned unchanged.
1166
1167 other :class:`~werkzeug.wrappers.Response` class
1168 The object is coerced to :attr:`response_class`.
1169
1170 :func:`callable`
1171 The function is called as a WSGI application. The result is
1172 used to create a response object.
1173
1174 .. versionchanged:: 2.2
1175 A generator will be converted to a streaming response.
1176 A list will be converted to a JSON response.
1177
1178 .. versionchanged:: 1.1
1179 A dict will be converted to a JSON response.
1180
1181 .. versionchanged:: 0.9
1182 Previously a tuple was interpreted as the arguments for the
1183 response object.
1184 """
1185
1186 status: int | None = None
1187 headers: HeadersValue | None = None
1188
1189 # unpack tuple returns
1190 if isinstance(rv, tuple):
1191 len_rv = len(rv)
1192
1193 # a 3-tuple is unpacked directly
1194 if len_rv == 3:
1195 rv, status, headers = rv # type: ignore[misc]
1196 # decide if a 2-tuple has status or headers
1197 elif len_rv == 2:
1198 if isinstance(rv[1], (Headers, dict, tuple, list)):
1199 rv, headers = rv # pyright: ignore
1200 else:
1201 rv, status = rv # type: ignore[assignment,misc]
1202 # other sized tuples are not allowed
1203 else:
1204 raise TypeError(
1205 "The view function did not return a valid response tuple."
1206 " The tuple must have the form (body, status, headers),"
1207 " (body, status), or (body, headers)."
1208 )
1209
1210 # the body must not be None
1211 if rv is None:
1212 raise TypeError(
1213 f"The view function for {request.endpoint!r} did not"
1214 " return a valid response. The function either returned"
1215 " None or ended without a return statement."
1216 )
1217
1218 # make sure the body is an instance of the response class
1219 if not isinstance(rv, self.response_class):
1220 if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, cabc.Iterator):
1221 # let the response class set the status and headers instead of
1222 # waiting to do it manually, so that the class can handle any
1223 # special logic
1224 rv = self.response_class(
1225 rv,
1226 status=status,
1227 headers=headers, # type: ignore[arg-type]
1228 )
1229 status = headers = None
1230 elif isinstance(rv, (dict, list)):
1231 rv = self.json.response(rv)
1232 elif isinstance(rv, BaseResponse) or callable(rv):
1233 # evaluate a WSGI callable, or coerce a different response
src/flask/app.py 281
src/flask/sansio/README.md
1 # Sansio
2
3 This folder contains code that can be used by alternative Flask
4 implementations, for example Quart. The code therefore cannot do any
5 IO, nor be part of a likely IO path. Finally this code cannot use the
6 Flask globals.
src/flask/sansio/blueprints.py
1 from __future__ import annotations
2
3 import os
4 import typing as t
5 from collections import defaultdict
6 from functools import update_wrapper
7
8 from .. import typing as ft
9 from .scaffold import _endpoint_from_view_func
10 from .scaffold import _sentinel
11 from .scaffold import Scaffold
12 from .scaffold import setupmethod
13
14 if t.TYPE_CHECKING: # pragma: no cover
15 from .app import App
16
17 DeferredSetupFunction = t.Callable[["BlueprintSetupState"], None]
18 T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any])
19 T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable)
20 T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable)
21 T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable)
22 T_template_context_processor = t.TypeVar(
23 "T_template_context_processor", bound=ft.TemplateContextProcessorCallable
24 )
25 T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable)
26 T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable)
27 T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable)
28 T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable)
29 T_url_value_preprocessor = t.TypeVar(
30 "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable
31 )
32
33
34 class BlueprintSetupState:
35 """Temporary holder object for registering a blueprint with the
36 application. An instance of this class is created by the
37 :meth:`~flask.Blueprint.make_setup_state` method and later passed
src/flask/sansio/blueprints.py 286
109
110 self.app.add_url_rule(
111 rule,
112 f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."),
113 view_func,
114 defaults=defaults,
115 **options,
116 )
117
118
119 class Blueprint(Scaffold):
120 """Represents a blueprint, a collection of routes and other
121 app-related functions that can be registered on a real application
122 later.
123
124 A blueprint is an object that allows defining application functions
125 without requiring an application object ahead of time. It uses the
126 same decorators as :class:`~flask.Flask`, but defers the need for an
127 application by recording them for later registration.
128
129 Decorating a function with a blueprint creates a deferred function
130 that is called with :class:`~flask.blueprints.BlueprintSetupState`
131 when the blueprint is registered on an application.
132
133 See :doc:`/blueprints` for more information.
134
135 :param name: The name of the blueprint. Will be prepended to each
136 endpoint name.
137 :param import_name: The name of the blueprint package, usually
138 ``__name__``. This helps locate the ``root_path`` for the
139 blueprint.
140 :param static_folder: A folder with static files that should be
141 served by the blueprint's static route. The path is relative to
142 the blueprint's root path. Blueprint static files are disabled
143 by default.
144 :param static_url_path: The url to serve static files from.
145 Defaults to ``static_folder``. If the blueprint does not have
146 a ``url_prefix``, the app's static route will take precedence,
147 and the blueprint's static files won't be accessible.
148 :param template_folder: A folder with templates that should be added
149 to the app's template search path. The path is relative to the
150 blueprint's root path. Blueprint templates are disabled by
151 default. Blueprint templates have a lower precedence than those
152 in the app's templates folder.
153 :param url_prefix: A path to prepend to all of the blueprint's URLs,
154 to make them distinct from the rest of the app's routes.
155 :param subdomain: A subdomain that blueprint routes will match on by
156 default.
157 :param url_defaults: A dict of default values that blueprint routes
158 will receive by default.
159 :param root_path: By default, the blueprint will automatically set
160 this based on ``import_name``. In certain situations this
161 automatic detection can fail, so the path can be specified
162 manually instead.
163
164 .. versionchanged:: 1.1.0
165 Blueprints have a ``cli`` group to register nested CLI commands.
166 The ``cli_group`` parameter controls the name of the group under
167 the ``flask`` command.
168
169 .. versionadded:: 0.7
170 """
171
172 _got_registered_once = False
173
174 def __init__(
175 self,
176 name: str,
177 import_name: str,
178 static_folder: str | os.PathLike[str] | None = None,
179 static_url_path: str | None = None,
src/flask/sansio/blueprints.py 288
251 Subclasses can override this to return a subclass of the setup state.
252 """
253 return BlueprintSetupState(self, app, options, first_registration)
254
255 @setupmethod
256 def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None:
257 """Register a :class:`~flask.Blueprint` on this blueprint. Keyword
258 arguments passed to this method will override the defaults set
259 on the blueprint.
260
261 .. versionchanged:: 2.0.1
262 The ``name`` option can be used to change the (pre-dotted)
263 name the blueprint is registered with. This allows the same
264 blueprint to be registered multiple times with unique names
265 for ``url_for``.
266
267 .. versionadded:: 2.0
268 """
269 if blueprint is self:
270 raise ValueError("Cannot register a blueprint on itself")
271 self._blueprints.append((blueprint, options))
272
273 def register(self, app: App, options: dict[str, t.Any]) -> None:
274 """Called by :meth:`Flask.register_blueprint` to register all
275 views and callbacks registered on the blueprint with the
276 application. Creates a :class:`.BlueprintSetupState` and calls
277 each :meth:`record` callback with it.
278
279 :param app: The application this blueprint is being registered
280 with.
281 :param options: Keyword arguments forwarded from
282 :meth:`~Flask.register_blueprint`.
283
284 .. versionchanged:: 2.3
285 Nested blueprints now correctly apply subdomains.
286
287 .. versionchanged:: 2.1
288 Registering the same blueprint with the same name multiple
289 times is an error.
290
291 .. versionchanged:: 2.0.1
292 Nested blueprints are registered with their dotted name.
293 This allows different blueprints with the same name to be
294 nested at different locations.
295
296 .. versionchanged:: 2.0.1
297 The ``name`` option can be used to change the (pre-dotted)
298 name the blueprint is registered with. This allows the same
299 blueprint to be registered multiple times with unique names
300 for ``url_for``.
301 """
302 name_prefix = options.get("name_prefix", "")
303 self_name = options.get("name", self.name)
304 name = f"{name_prefix}.{self_name}".lstrip(".")
305
306 if name in app.blueprints:
307 bp_desc = "this" if app.blueprints[name] is self else "a different"
308 existing_at = f" '{name}'" if self_name != name else ""
309
310 raise ValueError(
311 f"The name '{self_name}' is already registered for"
312 f" {bp_desc} blueprint{existing_at}. Use 'name=' to"
313 f" provide a unique name."
314 )
315
316 first_bp_registration = not any(bp is self for bp in app.blueprints.values())
317 first_name_registration = name not in app.blueprints
318
319 app.blueprints[name] = self
320 self._got_registered_once = True
321 state = self.make_setup_state(app, options, first_bp_registration)
src/flask/sansio/blueprints.py 290
322
323 if self.has_static_folder:
324 state.add_url_rule(
325 f"{self.static_url_path}/<path:filename>",
326 view_func=self.send_static_file, # type: ignore[attr-defined]
327 endpoint="static",
328 )
329
330 # Merge blueprint data into parent.
331 if first_bp_registration or first_name_registration:
332 self._merge_blueprint_funcs(app, name)
333
334 for deferred in self.deferred_functions:
335 deferred(state)
336
337 cli_resolved_group = options.get("cli_group", self.cli_group)
338
339 if self.cli.commands:
340 if cli_resolved_group is None:
341 app.cli.commands.update(self.cli.commands)
342 elif cli_resolved_group is _sentinel:
343 self.cli.name = name
344 app.cli.add_command(self.cli)
345 else:
346 self.cli.name = cli_resolved_group
347 app.cli.add_command(self.cli)
348
349 for blueprint, bp_options in self._blueprints:
350 bp_options = bp_options.copy()
351 bp_url_prefix = bp_options.get("url_prefix")
352 bp_subdomain = bp_options.get("subdomain")
353
354 if bp_subdomain is None:
355 bp_subdomain = blueprint.subdomain
356
357 if state.subdomain is not None and bp_subdomain is not None:
358 bp_options["subdomain"] = bp_subdomain + "." + state.subdomain
359 elif bp_subdomain is not None:
360 bp_options["subdomain"] = bp_subdomain
361 elif state.subdomain is not None:
362 bp_options["subdomain"] = state.subdomain
363
364 if bp_url_prefix is None:
365 bp_url_prefix = blueprint.url_prefix
366
367 if state.url_prefix is not None and bp_url_prefix is not None:
368 bp_options["url_prefix"] = (
369 state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/")
370 )
371 elif bp_url_prefix is not None:
372 bp_options["url_prefix"] = bp_url_prefix
373 elif state.url_prefix is not None:
374 bp_options["url_prefix"] = state.url_prefix
375
376 bp_options["name_prefix"] = name
377 blueprint.register(app, bp_options)
378
379 def _merge_blueprint_funcs(self, app: App, name: str) -> None:
380 def extend(
381 bp_dict: dict[ft.AppOrBlueprintKey, list[t.Any]],
382 parent_dict: dict[ft.AppOrBlueprintKey, list[t.Any]],
383 ) -> None:
384 for key, values in bp_dict.items():
385 key = name if key is None else f"{name}.{key}"
386 parent_dict[key].extend(values)
387
388 for key, value in self.error_handler_spec.items():
389 key = name if key is None else f"{name}.{key}"
390 value = defaultdict(
391 dict,
392 {
src/flask/sansio/blueprints.py 291
606
607 self.record_once(from_blueprint)
608 return f
609
610 return decorator
611
612 @setupmethod
613 def app_url_value_preprocessor(
614 self, f: T_url_value_preprocessor
615 ) -> T_url_value_preprocessor:
616 """Like :meth:`url_value_preprocessor`, but for every request, not only those
617 handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`.
618 """
619 self.record_once(
620 lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f)
621 )
622 return f
623
624 @setupmethod
625 def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults:
626 """Like :meth:`url_defaults`, but for every request, not only those handled by
627 the blueprint. Equivalent to :meth:`.Flask.url_defaults`.
628 """
629 self.record_once(
630 lambda s: s.app.url_default_functions.setdefault(None, []).append(f)
631 )
632 return f
src/flask/sansio/scaffold.py
1 from __future__ import annotations
2
3 import importlib.util
4 import os
5 import pathlib
6 import sys
7 import typing as t
8 from collections import defaultdict
9 from functools import update_wrapper
10
11 from jinja2 import BaseLoader
12 from jinja2 import FileSystemLoader
13 from werkzeug.exceptions import default_exceptions
14 from werkzeug.exceptions import HTTPException
15 from werkzeug.utils import cached_property
16
17 from .. import typing as ft
18 from ..helpers import get_root_path
19 from ..templating import _default_template_ctx_processor
20
21 if t.TYPE_CHECKING: # pragma: no cover
22 from click import Group
23
24 # a singleton sentinel value for parameter defaults
25 _sentinel = object()
26
27 F = t.TypeVar("F", bound=t.Callable[..., t.Any])
28 T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any])
29 T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable)
30 T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable)
31 T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable)
32 T_template_context_processor = t.TypeVar(
33 "T_template_context_processor", bound=ft.TemplateContextProcessorCallable
34 )
35 T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable)
36 T_url_value_preprocessor = t.TypeVar(
37 "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable
38 )
39 T_route = t.TypeVar("T_route", bound=ft.RouteCallable)
40
src/flask/sansio/scaffold.py 295
41
42 def setupmethod(f: F) -> F:
43 f_name = f.__name__
44
45 def wrapper_func(self: Scaffold, *args: t.Any, **kwargs: t.Any) -> t.Any:
46 self._check_setup_finished(f_name)
47 return f(self, *args, **kwargs)
48
49 return t.cast(F, update_wrapper(wrapper_func, f))
50
51
52 class Scaffold:
53 """Common behavior shared between :class:`~flask.Flask` and
54 :class:`~flask.blueprints.Blueprint`.
55
56 :param import_name: The import name of the module where this object
57 is defined. Usually :attr:`__name__` should be used.
58 :param static_folder: Path to a folder of static files to serve.
59 If this is set, a static route will be added.
60 :param static_url_path: URL prefix for the static route.
61 :param template_folder: Path to a folder containing template files.
62 for rendering. If this is set, a Jinja loader will be added.
63 :param root_path: The path that static, template, and resource files
64 are relative to. Typically not set, it is discovered based on
65 the ``import_name``.
66
67 .. versionadded:: 2.0
68 """
69
70 cli: Group
71 name: str
72 _static_folder: str | None = None
73 _static_url_path: str | None = None
74
75 def __init__(
76 self,
77 import_name: str,
78 static_folder: str | os.PathLike[str] | None = None,
79 static_url_path: str | None = None,
80 template_folder: str | os.PathLike[str] | None = None,
81 root_path: str | None = None,
82 ):
83 #: The name of the package or module that this object belongs
84 #: to. Do not change this once it is set by the constructor.
85 self.import_name = import_name
86
87 self.static_folder = static_folder # type: ignore
88 self.static_url_path = static_url_path
89
90 #: The path to the templates folder, relative to
91 #: :attr:`root_path`, to add to the template loader. ``None`` if
92 #: templates should not be added.
93 self.template_folder = template_folder
94
95 if root_path is None:
96 root_path = get_root_path(self.import_name)
97
98 #: Absolute path to the package on the filesystem. Used to look
99 #: up resources contained in the package.
100 self.root_path = root_path
101
102 #: A dictionary mapping endpoint names to view functions.
103 #:
104 #: To register a view function, use the :meth:`route` decorator.
105 #:
106 #: This data structure is internal. It should not be modified
107 #: directly and its format may change at any time.
108 self.view_functions: dict[str, ft.RouteCallable] = {}
109
110 #: A data structure of registered error handlers, in the format
111 #: ``{scope: {code: {class: handler}}}``. The ``scope`` key is
src/flask/sansio/scaffold.py 296
254 """
255 if self._static_url_path is not None:
256 return self._static_url_path
257
258 if self.static_folder is not None:
259 basename = os.path.basename(self.static_folder)
260 return f"/{basename}".rstrip("/")
261
262 return None
263
264 @static_url_path.setter
265 def static_url_path(self, value: str | None) -> None:
266 if value is not None:
267 value = value.rstrip("/")
268
269 self._static_url_path = value
270
271 @cached_property
272 def jinja_loader(self) -> BaseLoader | None:
273 """The Jinja loader for this object's templates. By default this
274 is a class :class:`jinja2.loaders.FileSystemLoader` to
275 :attr:`template_folder` if it is set.
276
277 .. versionadded:: 0.5
278 """
279 if self.template_folder is not None:
280 return FileSystemLoader(os.path.join(self.root_path, self.template_folder))
281 else:
282 return None
283
284 def _method_route(
285 self,
286 method: str,
287 rule: str,
288 options: dict[str, t.Any],
289 ) -> t.Callable[[T_route], T_route]:
290 if "methods" in options:
291 raise TypeError("Use the 'route' decorator to use the 'methods' argument.")
292
293 return self.route(rule, methods=[method], **options)
294
295 @setupmethod
296 def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
297 """Shortcut for :meth:`route` with ``methods=["GET"]``.
298
299 .. versionadded:: 2.0
300 """
301 return self._method_route("GET", rule, options)
302
303 @setupmethod
304 def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
305 """Shortcut for :meth:`route` with ``methods=["POST"]``.
306
307 .. versionadded:: 2.0
308 """
309 return self._method_route("POST", rule, options)
310
311 @setupmethod
312 def put(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
313 """Shortcut for :meth:`route` with ``methods=["PUT"]``.
314
315 .. versionadded:: 2.0
316 """
317 return self._method_route("PUT", rule, options)
318
319 @setupmethod
320 def delete(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
321 """Shortcut for :meth:`route` with ``methods=["DELETE"]``.
322
323 .. versionadded:: 2.0
324 """
src/flask/sansio/scaffold.py 299
467
468 @app.before_request
469 def load_user():
470 if "user_id" in session:
471 g.user = db.session.get(session["user_id"])
472
473 The function will be called without any arguments. If it returns
474 a non-``None`` value, the value is handled as if it was the
475 return value from the view, and further request handling is
476 stopped.
477
478 This is available on both app and blueprint objects. When used on an app, this
479 executes before every request. When used on a blueprint, this executes before
480 every request that the blueprint handles. To register with a blueprint and
481 execute before every request, use :meth:`.Blueprint.before_app_request`.
482 """
483 self.before_request_funcs.setdefault(None, []).append(f)
484 return f
485
486 @setupmethod
487 def after_request(self, f: T_after_request) -> T_after_request:
488 """Register a function to run after each request to this object.
489
490 The function is called with the response object, and must return
491 a response object. This allows the functions to modify or
492 replace the response before it is sent.
493
494 If a function raises an exception, any remaining
495 ``after_request`` functions will not be called. Therefore, this
496 should not be used for actions that must execute, such as to
497 close resources. Use :meth:`teardown_request` for that.
498
499 This is available on both app and blueprint objects. When used on an app, this
500 executes after every request. When used on a blueprint, this executes after
501 every request that the blueprint handles. To register with a blueprint and
502 execute after every request, use :meth:`.Blueprint.after_app_request`.
503 """
504 self.after_request_funcs.setdefault(None, []).append(f)
505 return f
506
507 @setupmethod
508 def teardown_request(self, f: T_teardown) -> T_teardown:
509 """Register a function to be called when the request context is
510 popped. Typically this happens at the end of each request, but
511 contexts may be pushed manually as well during testing.
512
513 .. code-block:: python
514
515 with app.test_request_context():
516 ...
517
518 When the ``with`` block exits (or ``ctx.pop()`` is called), the
519 teardown functions are called just before the request context is
520 made inactive.
521
522 When a teardown function was called because of an unhandled
523 exception it will be passed an error object. If an
524 :meth:`errorhandler` is registered, it will handle the exception
525 and the teardown will not receive it.
526
527 Teardown functions must avoid raising exceptions. If they
528 execute code that might fail they must surround that code with a
529 ``try``/``except`` block and log any errors.
530
531 The return values of teardown functions are ignored.
532
533 This is available on both app and blueprint objects. When used on an app, this
534 executes after every request. When used on a blueprint, this executes after
535 every request that the blueprint handles. To register with a blueprint and
536 execute after every request, use :meth:`.Blueprint.teardown_app_request`.
537 """
src/flask/sansio/scaffold.py 302
609
610 You can also register handlers for arbitrary exceptions::
611
612 @app.errorhandler(DatabaseError)
613 def special_exception_handler(error):
614 return 'Database connection failed', 500
615
616 This is available on both app and blueprint objects. When used on an app, this
617 can handle errors from every request. When used on a blueprint, this can handle
618 errors from requests that the blueprint handles. To register with a blueprint
619 and affect every request, use :meth:`.Blueprint.app_errorhandler`.
620
621 .. versionadded:: 0.7
622 Use :meth:`register_error_handler` instead of modifying
623 :attr:`error_handler_spec` directly, for application wide error
624 handlers.
625
626 .. versionadded:: 0.7
627 One can now additionally also register custom exception types
628 that do not necessarily have to be a subclass of the
629 :class:`~werkzeug.exceptions.HTTPException` class.
630
631 :param code_or_exception: the code as integer for the handler, or
632 an arbitrary exception
633 """
634
635 def decorator(f: T_error_handler) -> T_error_handler:
636 self.register_error_handler(code_or_exception, f)
637 return f
638
639 return decorator
640
641 @setupmethod
642 def register_error_handler(
643 self,
644 code_or_exception: type[Exception] | int,
645 f: ft.ErrorHandlerCallable,
646 ) -> None:
647 """Alternative error attach function to the :meth:`errorhandler`
648 decorator that is more straightforward to use for non decorator
649 usage.
650
651 .. versionadded:: 0.7
652 """
653 exc_class, code = self._get_exc_class_and_code(code_or_exception)
654 self.error_handler_spec[None][code][exc_class] = f
655
656 @staticmethod
657 def _get_exc_class_and_code(
658 exc_class_or_code: type[Exception] | int,
659 ) -> tuple[type[Exception], int | None]:
660 """Get the exception class being handled. For HTTP status codes
661 or ``HTTPException`` subclasses, return both the exception and
662 status code.
663
664 :param exc_class_or_code: Any exception class, or an HTTP status
665 code as an integer.
666 """
667 exc_class: type[Exception]
668
669 if isinstance(exc_class_or_code, int):
670 try:
671 exc_class = default_exceptions[exc_class_or_code]
672 except KeyError:
673 raise ValueError(
674 f"'{exc_class_or_code}' is not a recognized HTTP"
675 " error code. Use a subclass of HTTPException with"
676 " that code instead."
677 ) from None
678 else:
679 exc_class = exc_class_or_code
src/flask/sansio/scaffold.py 304
680
681 if isinstance(exc_class, Exception):
682 raise TypeError(
683 f"{exc_class!r} is an instance, not a class. Handlers"
684 " can only be registered for Exception classes or HTTP"
685 " error codes."
686 )
687
688 if not issubclass(exc_class, Exception):
689 raise ValueError(
690 f"'{exc_class.__name__}' is not a subclass of Exception."
691 " Handlers can only be registered for Exception classes"
692 " or HTTP error codes."
693 )
694
695 if issubclass(exc_class, HTTPException):
696 return exc_class, exc_class.code
697 else:
698 return exc_class, None
699
700
701 def _endpoint_from_view_func(view_func: ft.RouteCallable) -> str:
702 """Internal helper that returns the default endpoint for a given
703 function. This always is the function name.
704 """
705 assert view_func is not None, "expected view func if endpoint is not provided."
706 return view_func.__name__
707
708
709 def _find_package_path(import_name: str) -> str:
710 """Find the path that contains the package or module."""
711 root_mod_name, _, _ = import_name.partition(".")
712
713 try:
714 root_spec = importlib.util.find_spec(root_mod_name)
715
716 if root_spec is None:
717 raise ValueError("not found")
718 except (ImportError, ValueError):
719 # ImportError: the machinery told us it does not exist
720 # ValueError:
721 # - the module name was invalid
722 # - the module name is __main__
723 # - we raised `ValueError` due to `root_spec` being `None`
724 return os.getcwd()
725
726 if root_spec.submodule_search_locations:
727 if root_spec.origin is None or root_spec.origin == "namespace":
728 # namespace package
729 package_spec = importlib.util.find_spec(import_name)
730
731 if package_spec is not None and package_spec.submodule_search_locations:
732 # Pick the path in the namespace that contains the submodule.
733 package_path = pathlib.Path(
734 os.path.commonpath(package_spec.submodule_search_locations)
735 )
736 search_location = next(
737 location
738 for location in root_spec.submodule_search_locations
739 if package_path.is_relative_to(location)
740 )
741 else:
742 # Pick the first path.
743 search_location = root_spec.submodule_search_locations[0]
744
745 return os.path.dirname(search_location)
746 else:
747 # package with __init__.py
748 return os.path.dirname(os.path.dirname(root_spec.origin))
749 else:
750 # module
305
src/flask/sansio/app.py
1 from __future__ import annotations
2
3 import logging
4 import os
5 import sys
6 import typing as t
7 from datetime import timedelta
8 from itertools import chain
9
10 from werkzeug.exceptions import Aborter
11 from werkzeug.exceptions import BadRequest
12 from werkzeug.exceptions import BadRequestKeyError
13 from werkzeug.routing import BuildError
14 from werkzeug.routing import Map
15 from werkzeug.routing import Rule
16 from werkzeug.sansio.response import Response
17 from werkzeug.utils import cached_property
18 from werkzeug.utils import redirect as _wz_redirect
19
20 from .. import typing as ft
21 from ..config import Config
22 from ..config import ConfigAttribute
23 from ..ctx import _AppCtxGlobals
24 from ..helpers import _split_blueprint_path
25 from ..helpers import get_debug_flag
src/flask/sansio/app.py 306
97 to how resources are looked up. However it will make debugging more
98 painful. Certain extensions can make assumptions based on the
99 import name of your application. For example the Flask-SQLAlchemy
100 extension will look for the code in your application that triggered
101 an SQL query in debug mode. If the import name is not properly set
102 up, that debugging information is lost. (For example it would only
103 pick up SQL queries in `yourapplication.app` and not
104 `yourapplication.views.frontend`)
105
106 .. versionadded:: 0.7
107 The `static_url_path`, `static_folder`, and `template_folder`
108 parameters were added.
109
110 .. versionadded:: 0.8
111 The `instance_path` and `instance_relative_config` parameters were
112 added.
113
114 .. versionadded:: 0.11
115 The `root_path` parameter was added.
116
117 .. versionadded:: 1.0
118 The ``host_matching`` and ``static_host`` parameters were added.
119
120 .. versionadded:: 1.0
121 The ``subdomain_matching`` parameter was added. Subdomain
122 matching needs to be enabled manually now. Setting
123 :data:`SERVER_NAME` does not implicitly enable it.
124
125 :param import_name: the name of the application package
126 :param static_url_path: can be used to specify a different path for the
127 static files on the web. Defaults to the name
128 of the `static_folder` folder.
129 :param static_folder: The folder with static files that is served at
130 ``static_url_path``. Relative to the application ``root_path``
131 or an absolute path. Defaults to ``'static'``.
132 :param static_host: the host to use when adding the static route.
133 Defaults to None. Required when using ``host_matching=True``
134 with a ``static_folder`` configured.
135 :param host_matching: set ``url_map.host_matching`` attribute.
136 Defaults to False.
137 :param subdomain_matching: consider the subdomain relative to
138 :data:`SERVER_NAME` when matching routes. Defaults to False.
139 :param template_folder: the folder that contains the templates that should
140 be used by the application. Defaults to
141 ``'templates'`` folder in the root path of the
142 application.
143 :param instance_path: An alternative instance path for the application.
144 By default the folder ``'instance'`` next to the
145 package or module is assumed to be the instance
146 path.
147 :param instance_relative_config: if set to ``True`` relative filenames
148 for loading the config are assumed to
149 be relative to the instance path instead
150 of the application root.
151 :param root_path: The path to the root of the application files.
152 This should only be set manually when it can't be detected
153 automatically, such as for namespace packages.
154 """
155
156 #: The class of the object assigned to :attr:`aborter`, created by
157 #: :meth:`create_aborter`. That object is called by
158 #: :func:`flask.abort` to raise HTTP errors, and can be
159 #: called directly as well.
160 #:
161 #: Defaults to :class:`werkzeug.exceptions.Aborter`.
162 #:
163 #: .. versionadded:: 2.2
164 aborter_class = Aborter
165
166 #: The class that is used for the Jinja environment.
167 #:
src/flask/sansio/app.py 308
310
311 #: Holds the path to the instance folder.
312 #:
313 #: .. versionadded:: 0.8
314 self.instance_path = instance_path
315
316 #: The configuration dictionary as :class:`Config`. This behaves
317 #: exactly like a regular dictionary but supports additional methods
318 #: to load a config from files.
319 self.config = self.make_config(instance_relative_config)
320
321 #: An instance of :attr:`aborter_class` created by
322 #: :meth:`make_aborter`. This is called by :func:`flask.abort`
323 #: to raise HTTP errors, and can be called directly as well.
324 #:
325 #: .. versionadded:: 2.2
326 #: Moved from ``flask.abort``, which calls this object.
327 self.aborter = self.make_aborter()
328
329 self.json: JSONProvider = self.json_provider_class(self)
330 """Provides access to JSON methods. Functions in ``flask.json``
331 will call methods on this provider when the application context
332 is active. Used for handling JSON requests and responses.
333
334 An instance of :attr:`json_provider_class`. Can be customized by
335 changing that attribute on a subclass, or by assigning to this
336 attribute afterwards.
337
338 The default, :class:`~flask.json.provider.DefaultJSONProvider`,
339 uses Python's built-in :mod:`json` library. A different provider
340 can use a different JSON library.
341
342 .. versionadded:: 2.2
343 """
344
345 #: A list of functions that are called by
346 #: :meth:`handle_url_build_error` when :meth:`.url_for` raises a
347 #: :exc:`~werkzeug.routing.BuildError`. Each function is called
348 #: with ``error``, ``endpoint`` and ``values``. If a function
349 #: returns ``None`` or raises a ``BuildError``, it is skipped.
350 #: Otherwise, its return value is returned by ``url_for``.
351 #:
352 #: .. versionadded:: 0.9
353 self.url_build_error_handlers: list[
354 t.Callable[[Exception, str, dict[str, t.Any]], str]
355 ] = []
356
357 #: A list of functions that are called when the application context
358 #: is destroyed. Since the application context is also torn down
359 #: if the request ends this is the place to store code that disconnects
360 #: from databases.
361 #:
362 #: .. versionadded:: 0.9
363 self.teardown_appcontext_funcs: list[ft.TeardownCallable] = []
364
365 #: A list of shell context processor functions that should be run
366 #: when a shell context is created.
367 #:
368 #: .. versionadded:: 0.11
369 self.shell_context_processors: list[ft.ShellContextProcessorCallable] = []
370
371 #: Maps registered blueprint names to blueprint objects. The
372 #: dict retains the order the blueprints were registered in.
373 #: Blueprints can be registered multiple times, this dict does
374 #: not track how often they were attached.
375 #:
376 #: .. versionadded:: 0.7
377 self.blueprints: dict[str, Blueprint] = {}
378
379 #: a place where extensions can store application specific state. For
380 #: example this is where an extension could store database engines and
src/flask/sansio/app.py 311
452
453 .. versionchanged:: 1.1.0
454 The logger takes the same name as :attr:`name` rather than
455 hard-coding ``"flask.app"``.
456
457 .. versionchanged:: 1.0.0
458 Behavior was simplified. The logger is always named
459 ``"flask.app"``. The level is only set during configuration,
460 it doesn't check ``app.debug`` each time. Only one format is
461 used, not different ones depending on ``app.debug``. No
462 handlers are removed, and a handler is only added if no
463 handlers are already configured.
464
465 .. versionadded:: 0.3
466 """
467 return create_logger(self)
468
469 @cached_property
470 def jinja_env(self) -> Environment:
471 """The Jinja environment used to load templates.
472
473 The environment is created the first time this property is
474 accessed. Changing :attr:`jinja_options` after that will have no
475 effect.
476 """
477 return self.create_jinja_environment()
478
479 def create_jinja_environment(self) -> Environment:
480 raise NotImplementedError()
481
482 def make_config(self, instance_relative: bool = False) -> Config:
483 """Used to create the config attribute by the Flask constructor.
484 The `instance_relative` parameter is passed in from the constructor
485 of Flask (there named `instance_relative_config`) and indicates if
486 the config should be relative to the instance path or the root path
487 of the application.
488
489 .. versionadded:: 0.8
490 """
491 root_path = self.root_path
492 if instance_relative:
493 root_path = self.instance_path
494 defaults = dict(self.default_config)
495 defaults["DEBUG"] = get_debug_flag()
496 return self.config_class(root_path, defaults)
497
498 def make_aborter(self) -> Aborter:
499 """Create the object to assign to :attr:`aborter`. That object
500 is called by :func:`flask.abort` to raise HTTP errors, and can
501 be called directly as well.
502
503 By default, this creates an instance of :attr:`aborter_class`,
504 which defaults to :class:`werkzeug.exceptions.Aborter`.
505
506 .. versionadded:: 2.2
507 """
508 return self.aborter_class()
509
510 def auto_find_instance_path(self) -> str:
511 """Tries to locate the instance path if it was not provided to the
512 constructor of the application class. It will basically calculate
513 the path to a folder named ``instance`` next to your main file or
514 the package.
515
516 .. versionadded:: 0.8
517 """
518 prefix, package_path = find_package(self.import_name)
519 if prefix is None:
520 return os.path.join(package_path, "instance")
521 return os.path.join(prefix, "var", f"{self.name}-instance")
522
src/flask/sansio/app.py 313
594 """
595 blueprint.register(self, options)
596
597 def iter_blueprints(self) -> t.ValuesView[Blueprint]:
598 """Iterates over all blueprints by the order they were registered.
599
600 .. versionadded:: 0.11
601 """
602 return self.blueprints.values()
603
604 @setupmethod
605 def add_url_rule(
606 self,
607 rule: str,
608 endpoint: str | None = None,
609 view_func: ft.RouteCallable | None = None,
610 provide_automatic_options: bool | None = None,
611 **options: t.Any,
612 ) -> None:
613 if endpoint is None:
614 endpoint = _endpoint_from_view_func(view_func) # type: ignore
615 options["endpoint"] = endpoint
616 methods = options.pop("methods", None)
617
618 # if the methods are not given and the view_func object knows its
619 # methods we can use that instead. If neither exists, we go with
620 # a tuple of only ``GET`` as default.
621 if methods is None:
622 methods = getattr(view_func, "methods", None) or ("GET",)
623 if isinstance(methods, str):
624 raise TypeError(
625 "Allowed methods must be a list of strings, for"
626 ' example: @app.route(..., methods=["POST"])'
627 )
628 methods = {item.upper() for item in methods}
629
630 # Methods that should always be added
631 required_methods: set[str] = set(getattr(view_func, "required_methods", ()))
632
633 # starting with Flask 0.8 the view_func object can disable and
634 # force-enable the automatic options handling.
635 if provide_automatic_options is None:
636 provide_automatic_options = getattr(
637 view_func, "provide_automatic_options", None
638 )
639
640 if provide_automatic_options is None:
641 if "OPTIONS" not in methods and self.config["PROVIDE_AUTOMATIC_OPTIONS"]:
642 provide_automatic_options = True
643 required_methods.add("OPTIONS")
644 else:
645 provide_automatic_options = False
646
647 # Add the required methods now.
648 methods |= required_methods
649
650 rule_obj = self.url_rule_class(rule, methods=methods, **options)
651 rule_obj.provide_automatic_options = provide_automatic_options # type: ignore[attr-defined]
652
653 self.url_map.add(rule_obj)
654 if view_func is not None:
655 old_func = self.view_functions.get(endpoint)
656 if old_func is not None and old_func != view_func:
657 raise AssertionError(
658 "View function mapping is overwriting an existing"
659 f" endpoint function: {endpoint}"
660 )
661 self.view_functions[endpoint] = view_func
662
663 @setupmethod
664 def template_filter(
src/flask/sansio/app.py 315
878 if trap_bad_request:
879 return isinstance(e, BadRequest)
880
881 return False
882
883 def should_ignore_error(self, error: BaseException | None) -> bool:
884 """This is called to figure out if an error should be ignored
885 or not as far as the teardown system is concerned. If this
886 function returns ``True`` then the teardown handlers will not be
887 passed the error.
888
889 .. versionadded:: 0.10
890 """
891 return False
892
893 def redirect(self, location: str, code: int = 302) -> BaseResponse:
894 """Create a redirect response object.
895
896 This is called by :func:`flask.redirect`, and can be called
897 directly as well.
898
899 :param location: The URL to redirect to.
900 :param code: The status code for the redirect.
901
902 .. versionadded:: 2.2
903 Moved from ``flask.redirect``, which calls this method.
904 """
905 return _wz_redirect(
906 location,
907 code=code,
908 Response=self.response_class, # type: ignore[arg-type]
909 )
910
911 def inject_url_defaults(self, endpoint: str, values: dict[str, t.Any]) -> None:
912 """Injects the URL defaults for the given endpoint directly into
913 the values dictionary passed. This is used internally and
914 automatically called on URL building.
915
916 .. versionadded:: 0.7
917 """
918 names: t.Iterable[str | None] = (None,)
919
920 # url_for may be called outside a request context, parse the
921 # passed endpoint instead of using request.blueprints.
922 if "." in endpoint:
923 names = chain(
924 names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0]))
925 )
926
927 for name in names:
928 if name in self.url_default_functions:
929 for func in self.url_default_functions[name]:
930 func(endpoint, values)
931
932 def handle_url_build_error(
933 self, error: BuildError, endpoint: str, values: dict[str, t.Any]
934 ) -> str:
935 """Called by :meth:`.url_for` if a
936 :exc:`~werkzeug.routing.BuildError` was raised. If this returns
937 a value, it will be returned by ``url_for``, otherwise the error
938 will be re-raised.
939
940 Each function in :attr:`url_build_error_handlers` is called with
941 ``error``, ``endpoint`` and ``values``. If a function returns
942 ``None`` or raises a ``BuildError``, it is skipped. Otherwise,
943 its return value is returned by ``url_for``.
944
945 :param error: The active ``BuildError`` being handled.
946 :param endpoint: The endpoint being built.
947 :param values: The keyword arguments passed to ``url_for``.
948 """
src/flask/sansio/app.py 319