You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

Flask

Package Overview
Dependencies
Maintainers
0
Versions
64
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

Flask - pypi Package Compare versions

Comparing version
3.0.3
to
3.1.0
+28
examples/javascript/LICENSE.txt
Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz
https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz
https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz
https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz
https://github.com/pallets/click/archive/refs/heads/main.tar.gz
https://github.com/pallets-eco/blinker/archive/refs/heads/main.tar.gz
werkzeug==3.1.0
jinja2==3.1.2
markupsafe==2.1.1
itsdangerous==2.2.0
click==8.1.3
blinker==1.9.0
#
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
# pip-compile tests-min.in
#
blinker==1.9.0
# via -r tests-min.in
click==8.1.3
# via -r tests-min.in
itsdangerous==2.2.0
# via -r tests-min.in
jinja2==3.1.2
# via -r tests-min.in
markupsafe==2.1.1
# via
# -r tests-min.in
# jinja2
# werkzeug
werkzeug==3.1.0
# via -r tests-min.in
from __future__ import annotations
from flask import Flask
from flask import Request
from flask import request
from flask.testing import FlaskClient
def test_max_content_length(app: Flask, client: FlaskClient) -> None:
app.config["MAX_CONTENT_LENGTH"] = 50
@app.post("/")
def index():
request.form["myfile"]
AssertionError()
@app.errorhandler(413)
def catcher(error):
return "42"
rv = client.post("/", data={"myfile": "foo" * 50})
assert rv.data == b"42"
def test_limit_config(app: Flask):
app.config["MAX_CONTENT_LENGTH"] = 100
app.config["MAX_FORM_MEMORY_SIZE"] = 50
app.config["MAX_FORM_PARTS"] = 3
r = Request({})
# no app context, use Werkzeug defaults
assert r.max_content_length is None
assert r.max_form_memory_size == 500_000
assert r.max_form_parts == 1_000
# in app context, use config
with app.app_context():
assert r.max_content_length == 100
assert r.max_form_memory_size == 50
assert r.max_form_parts == 3
# regardless of app context, use override
r.max_content_length = 90
r.max_form_memory_size = 30
r.max_form_parts = 4
assert r.max_content_length == 90
assert r.max_form_memory_size == 30
assert r.max_form_parts == 4
with app.app_context():
assert r.max_content_length == 90
assert r.max_form_memory_size == 30
assert r.max_form_parts == 4
def test_trusted_hosts_config(app: Flask) -> None:
app.config["TRUSTED_HOSTS"] = ["example.test", ".other.test"]
@app.get("/")
def index() -> str:
return ""
client = app.test_client()
r = client.get(base_url="http://example.test")
assert r.status_code == 200
r = client.get(base_url="http://a.other.test")
assert r.status_code == 200
r = client.get(base_url="http://bad.test")
assert r.status_code == 400
from __future__ import annotations
from flask import Flask
from flask import Response
app = Flask(__name__)
@app.after_request
def after_sync(response: Response) -> Response:
return Response()
@app.after_request
async def after_async(response: Response) -> Response:
return Response()
@app.before_request
def before_sync() -> None: ...
@app.before_request
async def before_async() -> None: ...
@app.teardown_appcontext
def teardown_sync(exc: BaseException | None) -> None: ...
@app.teardown_appcontext
async def teardown_async(exc: BaseException | None) -> None: ...
from __future__ import annotations
from http import HTTPStatus
from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import NotFound
from flask import Flask
app = Flask(__name__)
@app.errorhandler(400)
@app.errorhandler(HTTPStatus.BAD_REQUEST)
@app.errorhandler(BadRequest)
def handle_400(e: BadRequest) -> str:
return ""
@app.errorhandler(ValueError)
def handle_custom(e: ValueError) -> str:
return ""
@app.errorhandler(ValueError)
def handle_accept_base(e: Exception) -> str:
return ""
@app.errorhandler(BadRequest)
@app.errorhandler(404)
def handle_multiple(e: BadRequest | NotFound) -> str:
return ""
from __future__ import annotations
import typing as t
from http import HTTPStatus
from flask import Flask
from flask import jsonify
from flask import stream_template
from flask.templating import render_template
from flask.views import View
from flask.wrappers import Response
app = Flask(__name__)
@app.route("/str")
def hello_str() -> str:
return "<p>Hello, World!</p>"
@app.route("/bytes")
def hello_bytes() -> bytes:
return b"<p>Hello, World!</p>"
@app.route("/json")
def hello_json() -> Response:
return jsonify("Hello, World!")
@app.route("/json/dict")
def hello_json_dict() -> dict[str, t.Any]:
return {"response": "Hello, World!"}
@app.route("/json/dict")
def hello_json_list() -> list[t.Any]:
return [{"message": "Hello"}, {"message": "World"}]
class StatusJSON(t.TypedDict):
status: str
@app.route("/typed-dict")
def typed_dict() -> StatusJSON:
return {"status": "ok"}
@app.route("/generator")
def hello_generator() -> t.Generator[str, None, None]:
def show() -> t.Generator[str, None, None]:
for x in range(100):
yield f"data:{x}\n\n"
return show()
@app.route("/generator-expression")
def hello_generator_expression() -> t.Iterator[bytes]:
return (f"data:{x}\n\n".encode() for x in range(100))
@app.route("/iterator")
def hello_iterator() -> t.Iterator[str]:
return iter([f"data:{x}\n\n" for x in range(100)])
@app.route("/status")
@app.route("/status/<int:code>")
def tuple_status(code: int = 200) -> tuple[str, int]:
return "hello", code
@app.route("/status-enum")
def tuple_status_enum() -> tuple[str, int]:
return "hello", HTTPStatus.OK
@app.route("/headers")
def tuple_headers() -> tuple[str, dict[str, str]]:
return "Hello, World!", {"Content-Type": "text/plain"}
@app.route("/template")
@app.route("/template/<name>")
def return_template(name: str | None = None) -> str:
return render_template("index.html", name=name)
@app.route("/template")
def return_template_stream() -> t.Iterator[str]:
return stream_template("index.html", name="Hello")
@app.route("/async")
async def async_route() -> str:
return "Hello"
class RenderTemplateView(View):
def __init__(self: RenderTemplateView, template_name: str) -> None:
self.template_name = template_name
def dispatch_request(self: RenderTemplateView) -> str:
return render_template(self.template_name)
app.add_url_rule(
"/about",
view_func=RenderTemplateView.as_view("about_page", template_name="about.html"),
)
+0
-6

@@ -26,8 +26,2 @@ .. _async_await:

.. admonition:: Using ``async`` on Windows on Python 3.8
Python 3.8 has a bug related to asyncio on Windows. If you encounter
something like ``ValueError: set_wakeup_fd only works in main thread``,
please upgrade to Python 3.9.
.. admonition:: Using ``async`` with greenlet

@@ -34,0 +28,0 @@

+122
-7

@@ -128,2 +128,18 @@ Configuration Handling

.. py:data:: SECRET_KEY_FALLBACKS
A list of old secret keys that can still be used for unsigning, most recent
first. This allows a project to implement key rotation without invalidating
active sessions or other recently-signed secrets.
Keys should be removed after an appropriate period of time, as checking each
additional key adds some overhead.
Flask's built-in secure cookie session supports this. Extensions that use
:data:`SECRET_KEY` may not support this yet.
Default: ``None``
.. versionadded:: 3.1
.. py:data:: SESSION_COOKIE_NAME

@@ -146,2 +162,8 @@

.. warning::
If this is changed after the browser created a cookie is created with
one setting, it may result in another being created. Browsers may send
send both in an undefined order. In that case, you may want to change
:data:`SESSION_COOKIE_NAME` as well or otherwise invalidate old sessions.
.. versionchanged:: 2.3

@@ -172,2 +194,19 @@ Not set by default, does not fall back to ``SERVER_NAME``.

.. py:data:: SESSION_COOKIE_PARTITIONED
Browsers will send cookies based on the top-level document's domain, rather
than only the domain of the document setting the cookie. This prevents third
party cookies set in iframes from "leaking" between separate sites.
Browsers are beginning to disallow non-partitioned third party cookies, so
you need to mark your cookies partitioned if you expect them to work in such
embedded situations.
Enabling this implicitly enables :data:`SESSION_COOKIE_SECURE` as well, as
it is only valid when served over HTTPS.
Default: ``False``
.. versionadded:: 3.1
.. py:data:: SESSION_COOKIE_SAMESITE

@@ -225,12 +264,36 @@

.. py:data:: TRUSTED_HOSTS
Validate :attr:`.Request.host` and other attributes that use it against
these trusted values. Raise a :exc:`~werkzeug.exceptions.SecurityError` if
the host is invalid, which results in a 400 error. If it is ``None``, all
hosts are valid. Each value is either an exact match, or can start with
a dot ``.`` to match any subdomain.
Validation is done during routing against this value. ``before_request`` and
``after_request`` callbacks will still be called.
Default: ``None``
.. versionadded:: 3.1
.. py:data:: SERVER_NAME
Inform the application what host and port it is bound to. Required
for subdomain route matching support.
Inform the application what host and port it is bound to.
If set, ``url_for`` can generate external URLs with only an application
context instead of a request context.
Must be set if ``subdomain_matching`` is enabled, to be able to extract the
subdomain from the request.
Must be set for ``url_for`` to generate external URLs outside of a
request context.
Default: ``None``
.. versionchanged:: 3.1
Does not restrict requests to only this domain, for both
``subdomain_matching`` and ``host_matching``.
.. versionchanged:: 1.0
Does not implicitly enable ``subdomain_matching``.
.. versionchanged:: 2.3

@@ -260,8 +323,50 @@ Does not affect ``SESSION_COOKIE_DOMAIN``.

Don't read more than this many bytes from the incoming request data. If not
set and the request does not specify a ``CONTENT_LENGTH``, no data will be
read for security.
The maximum number of bytes that will be read during this request. If
this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge`
error is raised. If it is set to ``None``, no limit is enforced at the
Flask application level. However, if it is ``None`` and the request has no
``Content-Length`` header and the WSGI server does not indicate that it
terminates the stream, then no data is read to avoid an infinite stream.
Each request defaults to this config. It can be set on a specific
:attr:`.Request.max_content_length` to apply the limit to that specific
view. This should be set appropriately based on an application's or view's
specific needs.
Default: ``None``
.. versionadded:: 0.6
.. py:data:: MAX_FORM_MEMORY_SIZE
The maximum size in bytes any non-file form field may be in a
``multipart/form-data`` body. If this limit is exceeded, a 413
:exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it is
set to ``None``, no limit is enforced at the Flask application level.
Each request defaults to this config. It can be set on a specific
:attr:`.Request.max_form_memory_parts` to apply the limit to that specific
view. This should be set appropriately based on an application's or view's
specific needs.
Default: ``500_000``
.. versionadded:: 3.1
.. py:data:: MAX_FORM_PARTS
The maximum number of fields that may be present in a
``multipart/form-data`` body. If this limit is exceeded, a 413
:exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it
is set to ``None``, no limit is enforced at the Flask application level.
Each request defaults to this config. It can be set on a specific
:attr:`.Request.max_form_parts` to apply the limit to that specific view.
This should be set appropriately based on an application's or view's
specific needs.
Default: ``1_000``
.. versionadded:: 3.1
.. py:data:: TEMPLATES_AUTO_RELOAD

@@ -288,2 +393,8 @@

.. py:data:: PROVIDE_AUTOMATIC_OPTIONS
Set to ``False`` to disable the automatic addition of OPTIONS
responses. This can be overridden per route by altering the
``provide_automatic_options`` attribute.
.. versionadded:: 0.4

@@ -340,3 +451,7 @@ ``LOGGER_NAME``

.. versionadded:: 3.10
Added :data:`PROVIDE_AUTOMATIC_OPTIONS` to control the default
addition of autogenerated OPTIONS responses.
Configuring from Python Files

@@ -343,0 +458,0 @@ -----------------------------

@@ -23,3 +23,3 @@ ASGI

and then serving the ``asgi_app`` with the ASGI server, e.g. using
`Hypercorn <https://gitlab.com/pgjones/hypercorn>`_,
`Hypercorn <https://github.com/pgjones/hypercorn>`_,

@@ -26,0 +26,0 @@ .. sourcecode:: text

@@ -71,3 +71,3 @@ Waitress

You can bind to all external IPs on a non-privileged port by not
specifying the ``--host`` option. Don't do this when using a revers
specifying the ``--host`` option. Don't do this when using a reverse
proxy setup, otherwise it will be possible to bypass the proxy.

@@ -74,0 +74,0 @@

@@ -297,3 +297,4 @@ Flask Extension Development

9. Indicate the versions of Python supported using ``python_requires=">=version"``.
Flask itself supports Python >=3.8 as of April 2023, but this will update over time.
Flask itself supports Python >=3.9 as of October 2024, and this will update
over time.

@@ -300,0 +301,0 @@ .. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask

@@ -9,3 +9,7 @@ .. rst-class:: hide-header

Welcome to Flask's documentation. Get started with :doc:`installation`
Welcome to Flask's documentation. Flask is a lightweight WSGI web application framework.
It is designed to make getting started quick and easy, with the ability to scale up to
complex applications.
Get started with :doc:`installation`
and then get an overview with the :doc:`quickstart`. There is also a

@@ -12,0 +16,0 @@ more detailed :doc:`tutorial/index` that shows how to create a small but

@@ -8,3 +8,3 @@ Installation

We recommend using the latest version of Python. Flask supports Python 3.8 and newer.
We recommend using the latest version of Python. Flask supports Python 3.9 and newer.

@@ -11,0 +11,0 @@

@@ -378,3 +378,3 @@ Quickstart

def hello(name=None):
return render_template('hello.html', name=name)
return render_template('hello.html', person=name)

@@ -408,4 +408,4 @@ Flask will look for templates in the :file:`templates` folder. So if your

<title>Hello from Flask</title>
{% if name %}
<h1>Hello {{ name }}!</h1>
{% if person %}
<h1>Hello {{ person }}!</h1>
{% else %}

@@ -424,3 +424,3 @@ <h1>Hello, World!</h1>

Automatic escaping is enabled, so if ``name`` contains HTML it will be escaped
Automatic escaping is enabled, so if ``person`` contains HTML it will be escaped
automatically. If you can trust a variable and you know that it will be

@@ -427,0 +427,0 @@ safe HTML (for example because it came from a module that converts wiki

@@ -195,3 +195,3 @@ Testing Flask Applications

def test_logout_redirect(client):
response = client.get("/logout")
response = client.get("/logout", follow_redirects=True)
# Check that there was one redirect response.

@@ -198,0 +198,0 @@ assert len(response.history) == 1

@@ -40,2 +40,3 @@ .. currentmodule:: flask

import sqlite3
from datetime import datetime

@@ -136,2 +137,7 @@ import click

sqlite3.register_converter(
"timestamp", lambda v: datetime.fromisoformat(v.decode())
)
:meth:`open_resource() <Flask.open_resource>` opens a file relative to

@@ -147,3 +153,7 @@ the ``flaskr`` package, which is useful since you won't necessarily know

The call to :func:`sqlite3.register_converter` tells Python how to
interpret timestamp values in the database. We convert the value to a
:class:`datetime.datetime`.
Register with the Application

@@ -150,0 +160,0 @@ -----------------------------

Security Considerations
=======================
Web applications usually face all kinds of security problems and it's very
hard to get everything right. Flask tries to solve a few of these things
for you, but there are a couple more you have to take care of yourself.
Web applications face many types of potential security problems, and it can be
hard to get everything right, or even to know what "right" is in general. Flask
tries to solve a few of these things by default, but there are other parts you
may have to take care of yourself. Many of these solutions are tradeoffs, and
will depend on each application's specific needs and threat model. Many hosting
platforms may take care of certain types of problems without the need for the
Flask application to handle them.
Resource Use
------------
A common category of attacks is "Denial of Service" (DoS or DDoS). This is a
very broad category, and different variants target different layers in a
deployed application. In general, something is done to increase how much
processing time or memory is used to handle each request, to the point where
there are not enough resources to handle legitimate requests.
Flask provides a few configuration options to handle resource use. They can
also be set on individual requests to customize only that request. The
documentation for each goes into more detail.
- :data:`MAX_CONTENT_LENGTH` or :attr:`.Request.max_content_length` controls
how much data will be read from a request. It is not set by default,
although it will still block truly unlimited streams unless the WSGI server
indicates support.
- :data:`MAX_FORM_MEMORY_SIZE` or :attr:`.Request.max_form_memory_size`
controls how large any non-file ``multipart/form-data`` field can be. It is
set to 500kB by default.
- :data:`MAX_FORM_PARTS` or :attr:`.Request.max_form_parts` controls how many
``multipart/form-data`` fields can be parsed. It is set to 1000 by default.
Combined with the default `max_form_memory_size`, this means that a form
will occupy at most 500MB of memory.
Regardless of these settings, you should also review what settings are available
from your operating system, container deployment (Docker etc), WSGI server, HTTP
server, and hosting platform. They typically have ways to set process resource
limits, timeouts, and other checks regardless of how Flask is configured.
.. _security-xss:

@@ -9,0 +43,0 @@

@@ -6,4 +6,4 @@ [project]

readme = "README.md"
requires-python = ">=3.8"
dependencies = ["flask>=2.2.2", "celery[redis]>=5.2.7"]
classifiers = ["Private :: Do Not Upload"]
dependencies = ["flask", "celery[redis]"]

@@ -10,0 +10,0 @@ [build-system]

@@ -6,8 +6,9 @@ [project]

readme = "README.rst"
license = {file = "LICENSE.rst"}
license = {file = "LICENSE.txt"}
maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}]
classifiers = ["Private :: Do Not Upload"]
dependencies = ["flask"]
[project.urls]
Documentation = "https://flask.palletsprojects.com/patterns/jquery/"
Documentation = "https://flask.palletsprojects.com/patterns/javascript/"

@@ -14,0 +15,0 @@ [project.optional-dependencies]

@@ -18,3 +18,3 @@ JavaScript Ajax Example

.. _Flask docs: https://flask.palletsprojects.com/patterns/jquery/
.. _Flask docs: https://flask.palletsprojects.com/patterns/javascript/

@@ -21,0 +21,0 @@

@@ -8,3 +8,3 @@ import pytest

(
("/", "xhr.html"),
("/", "fetch.html"),
("/plain", "xhr.html"),

@@ -11,0 +11,0 @@ ("/fetch", "fetch.html"),

import sqlite3
from datetime import datetime

@@ -47,2 +48,5 @@ import click

sqlite3.register_converter("timestamp", lambda v: datetime.fromisoformat(v.decode()))
def init_app(app):

@@ -49,0 +53,0 @@ """Register database functions with the Flask app. This is called by

@@ -6,4 +6,5 @@ [project]

readme = "README.rst"
license = {text = "BSD-3-Clause"}
license = {file = "LICENSE.txt"}
maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}]
classifiers = ["Private :: Do Not Upload"]
dependencies = [

@@ -10,0 +11,0 @@ "flask",

@@ -1,7 +0,7 @@

Metadata-Version: 2.1
Metadata-Version: 2.3
Name: Flask
Version: 3.0.3
Version: 3.1.0
Summary: A simple framework for building complex web applications.
Maintainer-email: Pallets <contact@palletsprojects.com>
Requires-Python: >=3.8
Requires-Python: >=3.9
Description-Content-Type: text/markdown

@@ -20,8 +20,8 @@ Classifier: Development Status :: 5 - Production/Stable

Classifier: Typing :: Typed
Requires-Dist: Werkzeug>=3.0.0
Requires-Dist: Werkzeug>=3.1
Requires-Dist: Jinja2>=3.1.2
Requires-Dist: itsdangerous>=2.1.2
Requires-Dist: itsdangerous>=2.2
Requires-Dist: click>=8.1.3
Requires-Dist: blinker>=1.6.2
Requires-Dist: importlib-metadata>=3.6.0; python_version < '3.10'
Requires-Dist: blinker>=1.9
Requires-Dist: importlib-metadata>=3.6; python_version < '3.10'
Requires-Dist: asgiref>=3.2 ; extra == "async"

@@ -55,14 +55,2 @@ Requires-Dist: python-dotenv ; extra == "dotenv"

## Installing
Install and update from [PyPI][] using an installer such as [pip][]:
```
$ pip install -U Flask
```
[PyPI]: https://pypi.org/project/Flask/
[pip]: https://pip.pypa.io/en/stable/getting-started/
## A Simple Example

@@ -87,10 +75,2 @@

## Contributing
For guidance on setting up a development environment and how to make a
contribution to Flask, see the [contributing guidelines][].
[contributing guidelines]: https://github.com/pallets/flask/blob/main/CONTRIBUTING.rst
## Donate

@@ -97,0 +77,0 @@

[project]
name = "Flask"
version = "3.0.3"
version = "3.1.0"
description = "A simple framework for building complex web applications."

@@ -22,10 +22,10 @@ readme = "README.md"

]
requires-python = ">=3.8"
requires-python = ">=3.9"
dependencies = [
"Werkzeug>=3.0.0",
"Werkzeug>=3.1",
"Jinja2>=3.1.2",
"itsdangerous>=2.1.2",
"itsdangerous>=2.2",
"click>=8.1.3",
"blinker>=1.6.2",
"importlib-metadata>=3.6.0; python_version < '3.10'",
"blinker>=1.9",
"importlib-metadata>=3.6; python_version < '3.10'",
]

@@ -82,4 +82,4 @@

[tool.mypy]
python_version = "3.8"
files = ["src/flask", "tests/typing"]
python_version = "3.9"
files = ["src/flask", "tests/type_check"]
show_error_codes = true

@@ -99,4 +99,4 @@ pretty = true

[tool.pyright]
pythonVersion = "3.8"
include = ["src/flask", "tests"]
pythonVersion = "3.9"
include = ["src/flask", "tests/type_check"]
typeCheckingMode = "basic"

@@ -119,3 +119,2 @@

]
ignore-init-module-imports = true

@@ -125,1 +124,6 @@ [tool.ruff.lint.isort]

order-by-type = false
[tool.gha-update]
tag-only = [
"slsa-framework/slsa-github-generator",
]

@@ -19,14 +19,2 @@ # Flask

## Installing
Install and update from [PyPI][] using an installer such as [pip][]:
```
$ pip install -U Flask
```
[PyPI]: https://pypi.org/project/Flask/
[pip]: https://pip.pypa.io/en/stable/getting-started/
## A Simple Example

@@ -51,10 +39,2 @@

## Contributing
For guidance on setting up a development environment and how to make a
contribution to Flask, see the [contributing guidelines][].
[contributing guidelines]: https://github.com/pallets/flask/blob/main/CONTRIBUTING.rst
## Donate

@@ -61,0 +41,0 @@

#
# This file is autogenerated by pip-compile with Python 3.12
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:

@@ -7,7 +7,7 @@ #

#
build==1.2.1
build==1.2.2.post1
# via -r build.in
packaging==24.0
packaging==24.2
# via build
pyproject-hooks==1.0.0
pyproject-hooks==1.2.0
# via build
#
# This file is autogenerated by pip-compile with Python 3.12
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:

@@ -7,23 +7,23 @@ #

#
alabaster==0.7.16
alabaster==1.0.0
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
asgiref==3.8.1
# via
# -r tests.txt
# -r typing.txt
babel==2.14.0
# -r /Users/david/Projects/flask/requirements/tests.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
babel==2.16.0
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
cachetools==5.3.3
cachetools==5.5.0
# via tox
certifi==2024.2.2
certifi==2024.8.30
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# requests
cffi==1.16.0
cffi==1.17.1
# via
# -r typing.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
# cryptography

@@ -34,60 +34,60 @@ cfgv==3.4.0

# via tox
charset-normalizer==3.3.2
charset-normalizer==3.4.0
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# requests
colorama==0.4.6
# via tox
cryptography==42.0.5
# via -r typing.txt
distlib==0.3.8
cryptography==43.0.3
# via -r /Users/david/Projects/flask/requirements/typing.txt
distlib==0.3.9
# via virtualenv
docutils==0.20.1
docutils==0.21.2
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
# sphinx-tabs
filelock==3.13.3
filelock==3.16.1
# via
# tox
# virtualenv
identify==2.5.35
identify==2.6.2
# via pre-commit
idna==3.6
idna==3.10
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# requests
imagesize==1.4.1
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
iniconfig==2.0.0
# via
# -r tests.txt
# -r typing.txt
# -r /Users/david/Projects/flask/requirements/tests.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
# pytest
jinja2==3.1.3
jinja2==3.1.4
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
markupsafe==2.1.5
markupsafe==3.0.2
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# jinja2
mypy==1.9.0
# via -r typing.txt
mypy==1.13.0
# via -r /Users/david/Projects/flask/requirements/typing.txt
mypy-extensions==1.0.0
# via
# -r typing.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
# mypy
nodeenv==1.8.0
nodeenv==1.9.1
# via
# -r typing.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
# pre-commit
# pyright
packaging==24.0
packaging==24.2
# via
# -r docs.txt
# -r tests.txt
# -r typing.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# -r /Users/david/Projects/flask/requirements/tests.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
# pallets-sphinx-themes

@@ -98,101 +98,104 @@ # pyproject-api

# tox
pallets-sphinx-themes==2.1.1
# via -r docs.txt
platformdirs==4.2.0
pallets-sphinx-themes==2.3.0
# via -r /Users/david/Projects/flask/requirements/docs.txt
platformdirs==4.3.6
# via
# tox
# virtualenv
pluggy==1.4.0
pluggy==1.5.0
# via
# -r tests.txt
# -r typing.txt
# -r /Users/david/Projects/flask/requirements/tests.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
# pytest
# tox
pre-commit==3.7.0
pre-commit==4.0.1
# via -r dev.in
pycparser==2.22
# via
# -r typing.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
# cffi
pygments==2.17.2
pygments==2.18.0
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
# sphinx-tabs
pyproject-api==1.6.1
pyproject-api==1.8.0
# via tox
pyright==1.1.357
# via -r typing.txt
pytest==8.1.1
pyright==1.1.389
# via -r /Users/david/Projects/flask/requirements/typing.txt
pytest==8.3.3
# via
# -r tests.txt
# -r typing.txt
# -r /Users/david/Projects/flask/requirements/tests.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
python-dotenv==1.0.1
# via
# -r tests.txt
# -r typing.txt
pyyaml==6.0.1
# -r /Users/david/Projects/flask/requirements/tests.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
pyyaml==6.0.2
# via pre-commit
requests==2.31.0
requests==2.32.3
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
snowballstemmer==2.2.0
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
sphinx==7.2.6
sphinx==8.1.3
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# pallets-sphinx-themes
# sphinx-notfound-page
# sphinx-tabs
# sphinxcontrib-log-cabinet
sphinx-tabs==3.4.5
# via -r docs.txt
sphinxcontrib-applehelp==1.0.8
sphinx-notfound-page==1.0.4
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# pallets-sphinx-themes
sphinx-tabs==3.4.7
# via -r /Users/david/Projects/flask/requirements/docs.txt
sphinxcontrib-applehelp==2.0.0
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
sphinxcontrib-devhelp==1.0.6
sphinxcontrib-devhelp==2.0.0
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
sphinxcontrib-htmlhelp==2.0.5
sphinxcontrib-htmlhelp==2.1.0
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
sphinxcontrib-jsmath==1.0.1
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
sphinxcontrib-log-cabinet==1.0.1
# via -r docs.txt
sphinxcontrib-qthelp==1.0.7
# via -r /Users/david/Projects/flask/requirements/docs.txt
sphinxcontrib-qthelp==2.0.0
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
sphinxcontrib-serializinghtml==1.1.10
sphinxcontrib-serializinghtml==2.0.0
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
tox==4.14.2
tox==4.23.2
# via -r dev.in
types-contextvars==2.4.7.3
# via -r typing.txt
# via -r /Users/david/Projects/flask/requirements/typing.txt
types-dataclasses==0.6.6
# via -r typing.txt
typing-extensions==4.11.0
# via -r /Users/david/Projects/flask/requirements/typing.txt
typing-extensions==4.12.2
# via
# -r typing.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
# mypy
urllib3==2.2.1
# pyright
urllib3==2.2.3
# via
# -r docs.txt
# -r /Users/david/Projects/flask/requirements/docs.txt
# requests
virtualenv==20.25.1
virtualenv==20.27.1
# via
# pre-commit
# tox
# The following packages are considered to be unsafe in a requirements file:
# setuptools
#
# This file is autogenerated by pip-compile with Python 3.12
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:

@@ -7,49 +7,52 @@ #

#
alabaster==0.7.16
alabaster==1.0.0
# via sphinx
babel==2.14.0
babel==2.16.0
# via sphinx
certifi==2024.2.2
certifi==2024.8.30
# via requests
charset-normalizer==3.3.2
charset-normalizer==3.4.0
# via requests
docutils==0.20.1
docutils==0.21.2
# via
# sphinx
# sphinx-tabs
idna==3.6
idna==3.10
# via requests
imagesize==1.4.1
# via sphinx
jinja2==3.1.3
jinja2==3.1.4
# via sphinx
markupsafe==2.1.5
markupsafe==3.0.2
# via jinja2
packaging==24.0
packaging==24.2
# via
# pallets-sphinx-themes
# sphinx
pallets-sphinx-themes==2.1.1
pallets-sphinx-themes==2.3.0
# via -r docs.in
pygments==2.17.2
pygments==2.18.0
# via
# sphinx
# sphinx-tabs
requests==2.31.0
requests==2.32.3
# via sphinx
snowballstemmer==2.2.0
# via sphinx
sphinx==7.2.6
sphinx==8.1.3
# via
# -r docs.in
# pallets-sphinx-themes
# sphinx-notfound-page
# sphinx-tabs
# sphinxcontrib-log-cabinet
sphinx-tabs==3.4.5
sphinx-notfound-page==1.0.4
# via pallets-sphinx-themes
sphinx-tabs==3.4.7
# via -r docs.in
sphinxcontrib-applehelp==1.0.8
sphinxcontrib-applehelp==2.0.0
# via sphinx
sphinxcontrib-devhelp==1.0.6
sphinxcontrib-devhelp==2.0.0
# via sphinx
sphinxcontrib-htmlhelp==2.0.5
sphinxcontrib-htmlhelp==2.1.0
# via sphinx

@@ -60,7 +63,7 @@ sphinxcontrib-jsmath==1.0.1

# via -r docs.in
sphinxcontrib-qthelp==1.0.7
sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==1.1.10
sphinxcontrib-serializinghtml==2.0.0
# via sphinx
urllib3==2.2.1
urllib3==2.2.3
# via requests
#
# This file is autogenerated by pip-compile with Python 3.12
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:

@@ -11,9 +11,9 @@ #

# via pytest
packaging==24.0
packaging==24.2
# via pytest
pluggy==1.4.0
pluggy==1.5.0
# via pytest
pytest==8.1.1
pytest==8.3.3
# via -r tests.in
python-dotenv==1.0.1
# via -r tests.in
#
# This file is autogenerated by pip-compile with Python 3.12
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:

@@ -9,23 +9,23 @@ #

# via -r typing.in
cffi==1.16.0
cffi==1.17.1
# via cryptography
cryptography==42.0.5
cryptography==43.0.3
# via -r typing.in
iniconfig==2.0.0
# via pytest
mypy==1.9.0
mypy==1.13.0
# via -r typing.in
mypy-extensions==1.0.0
# via mypy
nodeenv==1.8.0
nodeenv==1.9.1
# via pyright
packaging==24.0
packaging==24.2
# via pytest
pluggy==1.4.0
pluggy==1.5.0
# via pytest
pycparser==2.22
# via cffi
pyright==1.1.357
pyright==1.1.389
# via -r typing.in
pytest==8.1.1
pytest==8.3.3
# via -r typing.in

@@ -38,6 +38,5 @@ python-dotenv==1.0.1

# via -r typing.in
typing-extensions==4.11.0
# via mypy
# The following packages are considered to be unsafe in a requirements file:
# setuptools
typing-extensions==4.12.2
# via
# mypy
# pyright

@@ -27,2 +27,3 @@ from __future__ import annotations

from werkzeug.wrappers import Response as BaseResponse
from werkzeug.wsgi import get_host

@@ -63,2 +64,3 @@ from . import cli

from .testing import FlaskCliRunner
from .typing import HeadersValue

@@ -184,4 +186,6 @@ T_shell_context_processor = t.TypeVar(

"SECRET_KEY": None,
"SECRET_KEY_FALLBACKS": None,
"PERMANENT_SESSION_LIFETIME": timedelta(days=31),
"USE_X_SENDFILE": False,
"TRUSTED_HOSTS": None,
"SERVER_NAME": None,

@@ -194,5 +198,8 @@ "APPLICATION_ROOT": "/",

"SESSION_COOKIE_SECURE": False,
"SESSION_COOKIE_PARTITIONED": False,
"SESSION_COOKIE_SAMESITE": None,
"SESSION_REFRESH_EACH_REQUEST": True,
"MAX_CONTENT_LENGTH": None,
"MAX_FORM_MEMORY_SIZE": 500_000,
"MAX_FORM_PARTS": 1_000,
"SEND_FILE_MAX_AGE_DEFAULT": None,

@@ -205,2 +212,3 @@ "TRAP_BAD_REQUEST_ERRORS": None,

"MAX_COOKIE_SIZE": 4093,
"PROVIDE_AUTOMATIC_OPTIONS": True,
}

@@ -327,5 +335,6 @@ )

def open_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]:
"""Open a resource file relative to :attr:`root_path` for
reading.
def open_resource(
self, resource: str, mode: str = "rb", encoding: str | None = None
) -> t.IO[t.AnyStr]:
"""Open a resource file relative to :attr:`root_path` for reading.

@@ -341,10 +350,10 @@ For example, if the file ``schema.sql`` is next to the file

:param resource: Path to the resource relative to
:attr:`root_path`.
:param mode: Open the file in this mode. Only reading is
supported, valid values are "r" (or "rt") and "rb".
:param resource: Path to the resource relative to :attr:`root_path`.
:param mode: Open the file in this mode. Only reading is supported,
valid values are ``"r"`` (or ``"rt"``) and ``"rb"``.
:param encoding: Open the file with this encoding when opening in text
mode. This is ignored when opening in binary mode.
Note this is a duplicate of the same method in the Flask
class.
.. versionchanged:: 3.1
Added the ``encoding`` parameter.
"""

@@ -354,16 +363,31 @@ if mode not in {"r", "rt", "rb"}:

return open(os.path.join(self.root_path, resource), mode)
path = os.path.join(self.root_path, resource)
def open_instance_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]:
"""Opens a resource from the application's instance folder
(:attr:`instance_path`). Otherwise works like
:meth:`open_resource`. Instance resources can also be opened for
writing.
if mode == "rb":
return open(path, mode) # pyright: ignore
:param resource: the name of the resource. To access resources within
subfolders use forward slashes as separator.
:param mode: resource file opening mode, default is 'rb'.
return open(path, mode, encoding=encoding)
def open_instance_resource(
self, resource: str, mode: str = "rb", encoding: str | None = "utf-8"
) -> t.IO[t.AnyStr]:
"""Open a resource file relative to the application's instance folder
:attr:`instance_path`. Unlike :meth:`open_resource`, files in the
instance folder can be opened for writing.
:param resource: Path to the resource relative to :attr:`instance_path`.
:param mode: Open the file in this mode.
:param encoding: Open the file with this encoding when opening in text
mode. This is ignored when opening in binary mode.
.. versionchanged:: 3.1
Added the ``encoding`` parameter.
"""
return open(os.path.join(self.instance_path, resource), mode)
path = os.path.join(self.instance_path, resource)
if "b" in mode:
return open(path, mode)
return open(path, mode, encoding=encoding)
def create_jinja_environment(self) -> Environment:

@@ -414,28 +438,41 @@ """Create the Jinja environment based on :attr:`jinja_options`

.. versionadded:: 0.6
.. versionchanged:: 3.1
If :data:`SERVER_NAME` is set, it does not restrict requests to
only that domain, for both ``subdomain_matching`` and
``host_matching``.
.. versionchanged:: 0.9
This can now also be called without a request object when the
URL adapter is created for the application context.
.. versionchanged:: 1.0
:data:`SERVER_NAME` no longer implicitly enables subdomain
matching. Use :attr:`subdomain_matching` instead.
.. versionchanged:: 0.9
This can be called outside a request when the URL adapter is created
for an application context.
.. versionadded:: 0.6
"""
if request is not None:
# If subdomain matching is disabled (the default), use the
# default subdomain in all cases. This should be the default
# in Werkzeug but it currently does not have that feature.
if not self.subdomain_matching:
subdomain = self.url_map.default_subdomain or None
else:
subdomain = None
if (trusted_hosts := self.config["TRUSTED_HOSTS"]) is not None:
request.trusted_hosts = trusted_hosts
# Check trusted_hosts here until bind_to_environ does.
request.host = get_host(request.environ, request.trusted_hosts) # pyright: ignore
subdomain = None
server_name = self.config["SERVER_NAME"]
if self.url_map.host_matching:
# Don't pass SERVER_NAME, otherwise it's used and the actual
# host is ignored, which breaks host matching.
server_name = None
elif not self.subdomain_matching:
# Werkzeug doesn't implement subdomain matching yet. Until then,
# disable it by forcing the current subdomain to the default, or
# the empty string.
subdomain = self.url_map.default_subdomain or ""
return self.url_map.bind_to_environ(
request.environ,
server_name=self.config["SERVER_NAME"],
subdomain=subdomain,
request.environ, server_name=server_name, subdomain=subdomain
)
# We need at the very least the server name to be set for this
# to work.
# Need at least SERVER_NAME to match/build outside a request.
if self.config["SERVER_NAME"] is not None:

@@ -1158,3 +1195,4 @@ return self.url_map.bind(

status = headers = None
status: int | None = None
headers: HeadersValue | None = None

@@ -1171,3 +1209,3 @@ # unpack tuple returns

if isinstance(rv[1], (Headers, dict, tuple, list)):
rv, headers = rv
rv, headers = rv # pyright: ignore
else:

@@ -1240,3 +1278,3 @@ rv, status = rv # type: ignore[assignment,misc]

if headers:
rv.headers.update(headers) # type: ignore[arg-type]
rv.headers.update(headers)

@@ -1243,0 +1281,0 @@ return rv

@@ -104,23 +104,17 @@ from __future__ import annotations

def open_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]:
"""Open a resource file relative to :attr:`root_path` for
reading.
def open_resource(
self, resource: str, mode: str = "rb", encoding: str | None = "utf-8"
) -> t.IO[t.AnyStr]:
"""Open a resource file relative to :attr:`root_path` for reading. The
blueprint-relative equivalent of the app's :meth:`~.Flask.open_resource`
method.
For example, if the file ``schema.sql`` is next to the file
``app.py`` where the ``Flask`` app is defined, it can be opened
with:
:param resource: Path to the resource relative to :attr:`root_path`.
:param mode: Open the file in this mode. Only reading is supported,
valid values are ``"r"`` (or ``"rt"``) and ``"rb"``.
:param encoding: Open the file with this encoding when opening in text
mode. This is ignored when opening in binary mode.
.. code-block:: python
with app.open_resource("schema.sql") as f:
conn.executescript(f.read())
:param resource: Path to the resource relative to
:attr:`root_path`.
:param mode: Open the file in this mode. Only reading is
supported, valid values are "r" (or "rt") and "rb".
Note this is a duplicate of the same method in the Flask
class.
.. versionchanged:: 3.1
Added the ``encoding`` parameter.
"""

@@ -130,2 +124,7 @@ if mode not in {"r", "rt", "rb"}:

return open(os.path.join(self.root_path, resource), mode)
path = os.path.join(self.root_path, resource)
if mode == "rb":
return open(path, mode) # pyright: ignore
return open(path, mode, encoding=encoding)

@@ -300,2 +300,5 @@ from __future__ import annotations

onwards as click object.
.. versionchanged:: 3.1
Added the ``load_dotenv_defaults`` parameter and attribute.
"""

@@ -308,2 +311,3 @@

set_debug_flag: bool = True,
load_dotenv_defaults: bool = True,
) -> None:

@@ -319,2 +323,12 @@ #: Optionally the import path for the Flask application.

self.set_debug_flag = set_debug_flag
self.load_dotenv_defaults = get_load_dotenv(load_dotenv_defaults)
"""Whether default ``.flaskenv`` and ``.env`` files should be loaded.
``ScriptInfo`` doesn't load anything, this is for reference when doing
the load elsewhere during processing.
.. versionadded:: 3.1
"""
self._loaded_app: Flask | None = None

@@ -329,5 +343,5 @@

return self._loaded_app
app: Flask | None = None
if self.create_app is not None:
app: Flask | None = self.create_app()
app = self.create_app()
else:

@@ -486,19 +500,18 @@ if self.app_import_path:

) -> str | None:
if value is None:
return None
import importlib
try:
importlib.import_module("dotenv")
import dotenv # noqa: F401
except ImportError:
raise click.BadParameter(
"python-dotenv must be installed to load an env file.",
ctx=ctx,
param=param,
) from None
# Only show an error if a value was passed, otherwise we still want to
# call load_dotenv and show a message without exiting.
if value is not None:
raise click.BadParameter(
"python-dotenv must be installed to load an env file.",
ctx=ctx,
param=param,
) from None
# Don't check FLASK_SKIP_DOTENV, that only disables automatically
# loading .env and .flaskenv files.
load_dotenv(value)
# Load if a value was passed, or we want to load default files, or both.
if value is not None or ctx.obj.load_dotenv_defaults:
load_dotenv(value, load_defaults=ctx.obj.load_dotenv_defaults)
return value

@@ -512,3 +525,7 @@

type=click.Path(exists=True, dir_okay=False),
help="Load environment variables from this file. python-dotenv must be installed.",
help=(
"Load environment variables from this file, taking precedence over"
" those set by '.env' and '.flaskenv'. Variables set directly in the"
" environment take highest precedence. python-dotenv must be installed."
),
is_eager=True,

@@ -537,2 +554,5 @@ expose_value=False,

.. versionchanged:: 3.1
``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files.
.. versionchanged:: 2.2

@@ -559,3 +579,3 @@ Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options.

) -> None:
params = list(extra.pop("params", None) or ())
params: list[click.Parameter] = list(extra.pop("params", None) or ())
# Processing is done with option callbacks instead of a group

@@ -598,3 +618,3 @@ # callback. This allows users to make a custom group callback

# so use the backport for consistency.
import importlib_metadata as metadata
import importlib_metadata as metadata # pyright: ignore

@@ -666,10 +686,7 @@ for ep in metadata.entry_points(group="flask.commands"):

# Attempt to load .env and .flask env files. The --env-file
# option can cause another file to be loaded.
if get_load_dotenv(self.load_dotenv):
load_dotenv()
if "obj" not in extra and "obj" not in self.context_settings:
extra["obj"] = ScriptInfo(
create_app=self.create_app, set_debug_flag=self.set_debug_flag
create_app=self.create_app,
set_debug_flag=self.set_debug_flag,
load_dotenv_defaults=self.load_dotenv,
)

@@ -697,8 +714,10 @@

def load_dotenv(path: str | os.PathLike[str] | None = None) -> bool:
"""Load "dotenv" files in order of precedence to set environment variables.
def load_dotenv(
path: str | os.PathLike[str] | None = None, load_defaults: bool = True
) -> bool:
"""Load "dotenv" files to set environment variables. A given path takes
precedence over ``.env``, which takes precedence over ``.flaskenv``. After
loading and combining these files, values are only set if the key is not
already set in ``os.environ``.
If an env var is already set it is not overwritten, so earlier files in the
list are preferred over later files.
This is a no-op if `python-dotenv`_ is not installed.

@@ -708,5 +727,11 @@

:param path: Load the file at this location instead of searching.
:return: ``True`` if a file was loaded.
:param path: Load the file at this location.
:param load_defaults: Search for and load the default ``.flaskenv`` and
``.env`` files.
:return: ``True`` if at least one env var was loaded.
.. versionchanged:: 3.1
Added the ``load_defaults`` parameter. A given path takes precedence
over default files.
.. versionchanged:: 2.0

@@ -730,4 +755,4 @@ The current directory is not changed to the location of the

click.secho(
" * Tip: There are .env or .flaskenv files present."
' Do "pip install python-dotenv" to use them.',
" * Tip: There are .env files present. Install python-dotenv"
" to use them.",
fg="yellow",

@@ -739,22 +764,21 @@ err=True,

# Always return after attempting to load a given path, don't load
# the default files.
if path is not None:
if os.path.isfile(path):
return dotenv.load_dotenv(path, encoding="utf-8")
data: dict[str, str | None] = {}
return False
if load_defaults:
for default_name in (".flaskenv", ".env"):
if not (default_path := dotenv.find_dotenv(default_name, usecwd=True)):
continue
loaded = False
data |= dotenv.dotenv_values(default_path, encoding="utf-8")
for name in (".env", ".flaskenv"):
path = dotenv.find_dotenv(name, usecwd=True)
if path is not None and os.path.isfile(path):
data |= dotenv.dotenv_values(path, encoding="utf-8")
if not path:
for key, value in data.items():
if key in os.environ or value is None:
continue
dotenv.load_dotenv(path, encoding="utf-8")
loaded = True
os.environ[key] = value
return loaded # True if at least one file was located and loaded.
return bool(data) # True if at least one env var was loaded.

@@ -951,3 +975,3 @@

try:
app: WSGIApplication = info.load_app()
app: WSGIApplication = info.load_app() # pyright: ignore
except Exception as e:

@@ -954,0 +978,0 @@ if is_running_from_reloader():

@@ -153,3 +153,2 @@ from __future__ import annotations

prefix = f"{prefix}_"
len_prefix = len(prefix)

@@ -161,2 +160,3 @@ for key in sorted(os.environ):

value = os.environ[key]
key = key.removeprefix(prefix)

@@ -169,5 +169,2 @@ try:

# Change to key.removeprefix(prefix) on Python >= 3.9.
key = key[len_prefix:]
if "__" not in key:

@@ -174,0 +171,0 @@ # A non-nested key, set directly.

@@ -8,3 +8,3 @@ from __future__ import annotations

from datetime import datetime
from functools import lru_cache
from functools import cache
from functools import update_wrapper

@@ -51,5 +51,17 @@

@t.overload
def stream_with_context(
generator_or_function: t.Iterator[t.AnyStr],
) -> t.Iterator[t.AnyStr]: ...
@t.overload
def stream_with_context(
generator_or_function: t.Callable[..., t.Iterator[t.AnyStr]],
) -> t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: ...
def stream_with_context(
generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]],
) -> t.Iterator[t.AnyStr]:
) -> t.Iterator[t.AnyStr] | t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]:
"""Request contexts disappear when the response is started on the server.

@@ -540,3 +552,4 @@ This is done for efficiency reasons and to make it less likely to encounter

:param directory: The directory that ``path`` must be located under,
relative to the current application's root path.
relative to the current application's root path. This *must not*
be a value provided by the client, otherwise it becomes insecure.
:param path: The path to the file to send, relative to

@@ -593,3 +606,3 @@ ``directory``.

if hasattr(loader, "get_filename"):
filepath = loader.get_filename(import_name)
filepath = loader.get_filename(import_name) # pyright: ignore
else:

@@ -618,3 +631,3 @@ # Fall back to imports.

@lru_cache(maxsize=None)
@cache
def _split_blueprint_path(name: str) -> list[str]:

@@ -621,0 +634,0 @@ out: list[str] = [name]

@@ -116,3 +116,3 @@ from __future__ import annotations

if dataclasses and dataclasses.is_dataclass(o):
return dataclasses.asdict(o)
return dataclasses.asdict(o) # type: ignore[arg-type]

@@ -119,0 +119,0 @@ if hasattr(o, "__html__"):

@@ -294,3 +294,3 @@ from __future__ import annotations

root_path: str | None = None,
):
) -> None:
super().__init__(

@@ -632,3 +632,3 @@ import_name=import_name,

# Methods that should always be added
required_methods = set(getattr(view_func, "required_methods", ()))
required_methods: set[str] = set(getattr(view_func, "required_methods", ()))

@@ -643,3 +643,3 @@ # starting with Flask 0.8 the view_func object can disable and

if provide_automatic_options is None:
if "OPTIONS" not in methods:
if "OPTIONS" not in methods and self.config["PROVIDE_AUTOMATIC_OPTIONS"]:
provide_automatic_options = True

@@ -646,0 +646,0 @@ required_methods.add("OPTIONS")

@@ -709,11 +709,2 @@ from __future__ import annotations

def _path_is_relative_to(path: pathlib.PurePath, base: str) -> bool:
# Path.is_relative_to doesn't exist until Python 3.9
try:
path.relative_to(base)
return True
except ValueError:
return False
def _find_package_path(import_name: str) -> str:

@@ -749,3 +740,3 @@ """Find the path that contains the package or module."""

for location in root_spec.submodule_search_locations
if _path_is_relative_to(package_path, location)
if package_path.is_relative_to(location)
)

@@ -782,3 +773,3 @@ else:

# installed to the system
if _path_is_relative_to(pathlib.PurePath(package_path), py_prefix):
if pathlib.PurePath(package_path).is_relative_to(py_prefix):
return py_prefix, package_path

@@ -785,0 +776,0 @@

from __future__ import annotations
import collections.abc as c
import hashlib

@@ -23,4 +24,3 @@ import typing as t

# TODO generic when Python > 3.8
class SessionMixin(MutableMapping): # type: ignore[type-arg]
class SessionMixin(MutableMapping[str, t.Any]):
"""Expands a basic dictionary with session attributes."""

@@ -53,4 +53,3 @@

# TODO generic when Python > 3.8
class SecureCookieSession(CallbackDict, SessionMixin): # type: ignore[type-arg]
class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
"""Base class for sessions based on signed cookies.

@@ -77,3 +76,6 @@

def __init__(self, initial: t.Any = None) -> None:
def __init__(
self,
initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None,
) -> None:
def on_update(self: te.Self) -> None:

@@ -230,2 +232,10 @@ self.modified = True

def get_cookie_partitioned(self, app: Flask) -> bool:
"""Returns True if the cookie should be partitioned. By default, uses
the value of :data:`SESSION_COOKIE_PARTITIONED`.
.. versionadded:: 3.1
"""
return app.config["SESSION_COOKIE_PARTITIONED"] # type: ignore[no-any-return]
def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None:

@@ -314,10 +324,16 @@ """A helper method that returns an expiration date for the session

return None
signer_kwargs = dict(
key_derivation=self.key_derivation, digest_method=self.digest_method
)
keys: list[str | bytes] = [app.secret_key]
if fallbacks := app.config["SECRET_KEY_FALLBACKS"]:
keys.extend(fallbacks)
return URLSafeTimedSerializer(
app.secret_key,
keys, # type: ignore[arg-type]
salt=self.salt,
serializer=self.serializer,
signer_kwargs=signer_kwargs,
signer_kwargs={
"key_derivation": self.key_derivation,
"digest_method": self.digest_method,
},
)

@@ -346,2 +362,3 @@

secure = self.get_cookie_secure(app)
partitioned = self.get_cookie_partitioned(app)
samesite = self.get_cookie_samesite(app)

@@ -363,2 +380,3 @@ httponly = self.get_cookie_httponly(app)

secure=secure,
partitioned=partitioned,
samesite=samesite,

@@ -375,6 +393,6 @@ httponly=httponly,

expires = self.get_expiration_time(app, session)
val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore
val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore[union-attr]
response.set_cookie(
name,
val, # type: ignore
val,
expires=expires,

@@ -385,4 +403,5 @@ httponly=httponly,

secure=secure,
partitioned=partitioned,
samesite=samesite,
)
response.vary.add("Cookie")

@@ -82,4 +82,3 @@ from __future__ import annotations

if url.query:
sep = b"?" if isinstance(url.query, bytes) else "?"
path += sep + url.query
path = f"{path}?{url.query}"

@@ -86,0 +85,0 @@ self.app = app

@@ -15,3 +15,3 @@ from __future__ import annotations

bytes,
t.List[t.Any],
list[t.Any],
# Only dict is actually accepted, but Mapping allows for TypedDict.

@@ -25,3 +25,3 @@ t.Mapping[str, t.Any],

# This should be a Union, but mypy doesn't pass unless it's a TypeVar.
HeaderValue = t.Union[str, t.List[str], t.Tuple[str, ...]]
HeaderValue = t.Union[str, list[str], tuple[str, ...]]

@@ -32,3 +32,3 @@ # the possible types for HTTP headers

t.Mapping[str, HeaderValue],
t.Sequence[t.Tuple[str, HeaderValue]],
t.Sequence[tuple[str, HeaderValue]],
]

@@ -39,5 +39,5 @@

ResponseValue,
t.Tuple[ResponseValue, HeadersValue],
t.Tuple[ResponseValue, int],
t.Tuple[ResponseValue, int, HeadersValue],
tuple[ResponseValue, HeadersValue],
tuple[ResponseValue, int],
tuple[ResponseValue, int, HeadersValue],
"WSGIApplication",

@@ -63,3 +63,3 @@ ]

]
ShellContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]]
ShellContextProcessorCallable = t.Callable[[], dict[str, t.Any]]
TeardownCallable = t.Union[

@@ -70,4 +70,4 @@ t.Callable[[t.Optional[BaseException]], None],

TemplateContextProcessorCallable = t.Union[
t.Callable[[], t.Dict[str, t.Any]],
t.Callable[[], t.Awaitable[t.Dict[str, t.Any]]],
t.Callable[[], dict[str, t.Any]],
t.Callable[[], t.Awaitable[dict[str, t.Any]]],
]

@@ -77,5 +77,5 @@ TemplateFilterCallable = t.Callable[..., t.Any]

TemplateTestCallable = t.Callable[..., bool]
URLDefaultCallable = t.Callable[[str, t.Dict[str, t.Any]], None]
URLDefaultCallable = t.Callable[[str, dict[str, t.Any]], None]
URLValuePreprocessorCallable = t.Callable[
[t.Optional[str], t.Optional[t.Dict[str, t.Any]]], None
[t.Optional[str], t.Optional[dict[str, t.Any]]], None
]

@@ -82,0 +82,0 @@

@@ -64,3 +64,3 @@ from __future__ import annotations

#: .. versionadded:: 0.8
decorators: t.ClassVar[list[t.Callable[[F], F]]] = []
decorators: t.ClassVar[list[t.Callable[..., t.Any]]] = []

@@ -114,3 +114,3 @@ #: Create a new instance of this view class for every request by

else:
self = cls(*class_args, **class_kwargs)
self = cls(*class_args, **class_kwargs) # pyright: ignore

@@ -117,0 +117,0 @@ def view(**kwargs: t.Any) -> ft.ResponseReturnValue:

@@ -55,11 +55,94 @@ from __future__ import annotations

_max_content_length: int | None = None
_max_form_memory_size: int | None = None
_max_form_parts: int | None = None
@property
def max_content_length(self) -> int | None: # type: ignore[override]
"""Read-only view of the ``MAX_CONTENT_LENGTH`` config key."""
if current_app:
return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return]
else:
return None
def max_content_length(self) -> int | None:
"""The maximum number of bytes that will be read during this request. If
this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge`
error is raised. If it is set to ``None``, no limit is enforced at the
Flask application level. However, if it is ``None`` and the request has
no ``Content-Length`` header and the WSGI server does not indicate that
it terminates the stream, then no data is read to avoid an infinite
stream.
Each request defaults to the :data:`MAX_CONTENT_LENGTH` config, which
defaults to ``None``. It can be set on a specific ``request`` to apply
the limit to that specific view. This should be set appropriately based
on an application's or view's specific needs.
.. versionchanged:: 3.1
This can be set per-request.
.. versionchanged:: 0.6
This is configurable through Flask config.
"""
if self._max_content_length is not None:
return self._max_content_length
if not current_app:
return super().max_content_length
return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return]
@max_content_length.setter
def max_content_length(self, value: int | None) -> None:
self._max_content_length = value
@property
def max_form_memory_size(self) -> int | None:
"""The maximum size in bytes any non-file form field may be in a
``multipart/form-data`` body. If this limit is exceeded, a 413
:exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it
is set to ``None``, no limit is enforced at the Flask application level.
Each request defaults to the :data:`MAX_FORM_MEMORY_SIZE` config, which
defaults to ``500_000``. It can be set on a specific ``request`` to
apply the limit to that specific view. This should be set appropriately
based on an application's or view's specific needs.
.. versionchanged:: 3.1
This is configurable through Flask config.
"""
if self._max_form_memory_size is not None:
return self._max_form_memory_size
if not current_app:
return super().max_form_memory_size
return current_app.config["MAX_FORM_MEMORY_SIZE"] # type: ignore[no-any-return]
@max_form_memory_size.setter
def max_form_memory_size(self, value: int | None) -> None:
self._max_form_memory_size = value
@property # type: ignore[override]
def max_form_parts(self) -> int | None:
"""The maximum number of fields that may be present in a
``multipart/form-data`` body. If this limit is exceeded, a 413
:exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it
is set to ``None``, no limit is enforced at the Flask application level.
Each request defaults to the :data:`MAX_FORM_PARTS` config, which
defaults to ``1_000``. It can be set on a specific ``request`` to apply
the limit to that specific view. This should be set appropriately based
on an application's or view's specific needs.
.. versionchanged:: 3.1
This is configurable through Flask config.
"""
if self._max_form_parts is not None:
return self._max_form_parts
if not current_app:
return super().max_form_parts
return current_app.config["MAX_FORM_PARTS"] # type: ignore[no-any-return]
@max_form_parts.setter
def max_form_parts(self, value: int | None) -> None:
self._max_form_parts = value
@property
def endpoint(self) -> str | None:

@@ -75,3 +158,3 @@ """The endpoint that matched the request URL.

if self.url_rule is not None:
return self.url_rule.endpoint
return self.url_rule.endpoint # type: ignore[no-any-return]

@@ -134,7 +217,7 @@ return None

return super().on_json_loading_failed(e)
except BadRequest as e:
except BadRequest as ebr:
if current_app and current_app.debug:
raise
raise BadRequest() from e
raise BadRequest() from ebr

@@ -141,0 +224,0 @@

import gc
import re
import typing as t
import uuid
import warnings
import weakref
from contextlib import nullcontext
from datetime import datetime

@@ -296,2 +298,3 @@ from datetime import timezone

SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_PARTITIONED=True,
SESSION_COOKIE_SAMESITE="Lax",

@@ -319,2 +322,3 @@ SESSION_COOKIE_PATH="/",

assert "samesite" in cookie
assert "partitioned" in cookie

@@ -329,2 +333,3 @@ rv = client.get("/clear", "http://www.example.com:8080/test/")

assert "samesite" in cookie
assert "partitioned" in cookie

@@ -372,2 +377,23 @@

def test_session_secret_key_fallbacks(app, client) -> None:
@app.post("/")
def set_session() -> str:
flask.session["a"] = 1
return ""
@app.get("/")
def get_session() -> dict[str, t.Any]:
return dict(flask.session)
# Set session with initial secret key
client.post()
assert client.get().json == {"a": 1}
# Change secret key, session can't be loaded and appears empty
app.secret_key = "new test key"
assert client.get().json == {}
# Add initial secret key as fallback, session can be loaded
app.config["SECRET_KEY_FALLBACKS"] = ["test key"]
assert client.get().json == {"a": 1}
def test_session_expiration(app, client):

@@ -1465,2 +1491,44 @@ permanent = True

@pytest.mark.parametrize(
("subdomain_matching", "host_matching", "expect_base", "expect_abc", "expect_xyz"),
[
(False, False, "default", "default", "default"),
(True, False, "default", "abc", "<invalid>"),
(False, True, "default", "abc", "default"),
],
)
def test_server_name_matching(
subdomain_matching: bool,
host_matching: bool,
expect_base: str,
expect_abc: str,
expect_xyz: str,
) -> None:
app = flask.Flask(
__name__,
subdomain_matching=subdomain_matching,
host_matching=host_matching,
static_host="example.test" if host_matching else None,
)
app.config["SERVER_NAME"] = "example.test"
@app.route("/", defaults={"name": "default"}, host="<name>")
@app.route("/", subdomain="<name>", host="<name>.example.test")
def index(name: str) -> str:
return name
client = app.test_client()
r = client.get(base_url="http://example.test")
assert r.text == expect_base
r = client.get(base_url="http://abc.example.test")
assert r.text == expect_abc
with pytest.warns() if subdomain_matching else nullcontext():
r = client.get(base_url="http://xyz.other.test")
assert r.text == expect_xyz
def test_server_name_subdomain():

@@ -1546,23 +1614,2 @@ app = flask.Flask(__name__, subdomain_matching=True)

def test_max_content_length(app, client):
app.config["MAX_CONTENT_LENGTH"] = 64
@app.before_request
def always_first():
flask.request.form["myfile"]
AssertionError()
@app.route("/accept", methods=["POST"])
def accept_file():
flask.request.form["myfile"]
AssertionError()
@app.errorhandler(413)
def catcher(error):
return "42"
rv = client.post("/accept", data={"myfile": "foo" * 100})
assert rv.data == b"42"
def test_url_processors(app, client):

@@ -1569,0 +1616,0 @@ @app.url_defaults

@@ -954,3 +954,6 @@ import pytest

def test_nesting_subdomains(app, client) -> None:
subdomain = "api"
app.subdomain_matching = True
app.config["SERVER_NAME"] = "example.test"
client.allow_subdomain_redirects = True
parent = flask.Blueprint("parent", __name__)

@@ -964,10 +967,5 @@ child = flask.Blueprint("child", __name__)

parent.register_blueprint(child)
app.register_blueprint(parent, subdomain=subdomain)
app.register_blueprint(parent, subdomain="api")
client.allow_subdomain_redirects = True
domain_name = "domain.tld"
app.config["SERVER_NAME"] = domain_name
response = client.get("/child/", base_url="http://api." + domain_name)
response = client.get("/child/", base_url="http://api.example.test")
assert response.status_code == 200

@@ -977,6 +975,8 @@

def test_child_and_parent_subdomain(app, client) -> None:
child_subdomain = "api"
parent_subdomain = "parent"
app.subdomain_matching = True
app.config["SERVER_NAME"] = "example.test"
client.allow_subdomain_redirects = True
parent = flask.Blueprint("parent", __name__)
child = flask.Blueprint("child", __name__, subdomain=child_subdomain)
child = flask.Blueprint("child", __name__, subdomain="api")

@@ -988,16 +988,8 @@ @child.route("/")

parent.register_blueprint(child)
app.register_blueprint(parent, subdomain=parent_subdomain)
app.register_blueprint(parent, subdomain="parent")
client.allow_subdomain_redirects = True
domain_name = "domain.tld"
app.config["SERVER_NAME"] = domain_name
response = client.get(
"/", base_url=f"http://{child_subdomain}.{parent_subdomain}.{domain_name}"
)
response = client.get("/", base_url="http://api.parent.example.test")
assert response.status_code == 200
response = client.get("/", base_url=f"http://{parent_subdomain}.{domain_name}")
response = client.get("/", base_url="http://parent.example.test")
assert response.status_code == 404

@@ -1004,0 +996,0 @@

@@ -401,3 +401,8 @@ # This file was part of Flask-CLI and was modified under the terms of

runner = CliRunner(mix_stderr=False)
try:
runner = CliRunner(mix_stderr=False)
except (DeprecationWarning, TypeError):
# Click >= 8.2
runner = CliRunner()
result = runner.invoke(cli, ["missing"])

@@ -412,3 +417,8 @@ assert result.exit_code == 2

runner = CliRunner(mix_stderr=False)
try:
runner = CliRunner(mix_stderr=False)
except (DeprecationWarning, TypeError):
# Click >= 8.2
runner = CliRunner()
result = runner.invoke(cli, ["--help"])

@@ -425,3 +435,9 @@ assert result.exit_code == 0

cli = FlaskGroup(create_app=create_app)
runner = CliRunner(mix_stderr=False)
try:
runner = CliRunner(mix_stderr=False)
except (DeprecationWarning, TypeError):
# Click >= 8.2
runner = CliRunner()
result = runner.invoke(cli, ["--help"])

@@ -543,3 +559,3 @@ assert result.exit_code == 0

# Non existent file should not load
assert not load_dotenv("non-existent-file")
assert not load_dotenv("non-existent-file", load_defaults=False)

@@ -546,0 +562,0 @@

@@ -337,14 +337,25 @@ import io

@pytest.mark.parametrize("mode", ("r", "rb", "rt"))
def test_open_resource(self, mode):
app = flask.Flask(__name__)
with app.open_resource("static/index.html", mode) as f:
assert "<h1>Hello World!</h1>" in str(f.read())
@pytest.mark.parametrize("mode", ("r", "rb", "rt"))
def test_open_resource(mode):
app = flask.Flask(__name__)
@pytest.mark.parametrize("mode", ("w", "x", "a", "r+"))
def test_open_resource_exceptions(self, mode):
app = flask.Flask(__name__)
with app.open_resource("static/index.html", mode) as f:
assert "<h1>Hello World!</h1>" in str(f.read())
with pytest.raises(ValueError):
app.open_resource("static/index.html", mode)
@pytest.mark.parametrize("mode", ("w", "x", "a", "r+"))
def test_open_resource_exceptions(mode):
app = flask.Flask(__name__)
with pytest.raises(ValueError):
app.open_resource("static/index.html", mode)
@pytest.mark.parametrize("encoding", ("utf-8", "utf-16-le"))
def test_open_resource_with_encoding(tmp_path, encoding):
app = flask.Flask(__name__, root_path=os.fspath(tmp_path))
(tmp_path / "test").write_text("test", encoding=encoding)
with app.open_resource("test", mode="rt", encoding=encoding) as f:
assert f.read() == "test"
+29
-16
[tox]
envlist =
py3{12,11,10,9,8}
py3{13,12,11,10,9}
pypy310
py312-min
py38-dev
py313-min
py39-dev
style

@@ -20,4 +20,4 @@ typing

-r requirements/tests.txt
min: -r requirements-skip/tests-min.txt
dev: -r requirements-skip/tests-dev.txt
min: -r requirements/tests-min.txt
dev: -r requirements/tests-dev.txt
commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs}

@@ -32,20 +32,33 @@

deps = -r requirements/typing.txt
commands = mypy
commands =
mypy
pyright
[testenv:docs]
deps = -r requirements/docs.txt
commands = sphinx-build -W -b dirhtml docs docs/_build/dirhtml
commands = sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml
[testenv:update-actions]
labels = update
deps = gha-update
skip_install = true
commands = gha-update
[testenv:update-pre_commit]
labels = update
deps = pre-commit
skip_install = true
commands = pre-commit autoupdate -j4
[testenv:update-requirements]
deps =
pip-tools
pre-commit
labels = update
deps = pip-tools
skip_install = true
change_dir = requirements
commands =
pre-commit autoupdate -j4
pip-compile -U build.in
pip-compile -U docs.in
pip-compile -U tests.in
pip-compile -U typing.in
pip-compile -U dev.in
pip-compile build.in -q {posargs:-U}
pip-compile docs.in -q {posargs:-U}
pip-compile tests.in -q {posargs:-U}
pip-compile tests-min.in -q
pip-compile typing.in -q {posargs:-U}
pip-compile dev.in -q {posargs:-U}
Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import annotations
from flask import Flask
from flask import Response
app = Flask(__name__)
@app.after_request
def after_sync(response: Response) -> Response:
return Response()
@app.after_request
async def after_async(response: Response) -> Response:
return Response()
@app.before_request
def before_sync() -> None: ...
@app.before_request
async def before_async() -> None: ...
@app.teardown_appcontext
def teardown_sync(exc: BaseException | None) -> None: ...
@app.teardown_appcontext
async def teardown_async(exc: BaseException | None) -> None: ...
from __future__ import annotations
from http import HTTPStatus
from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import NotFound
from flask import Flask
app = Flask(__name__)
@app.errorhandler(400)
@app.errorhandler(HTTPStatus.BAD_REQUEST)
@app.errorhandler(BadRequest)
def handle_400(e: BadRequest) -> str:
return ""
@app.errorhandler(ValueError)
def handle_custom(e: ValueError) -> str:
return ""
@app.errorhandler(ValueError)
def handle_accept_base(e: Exception) -> str:
return ""
@app.errorhandler(BadRequest)
@app.errorhandler(404)
def handle_multiple(e: BadRequest | NotFound) -> str:
return ""
from __future__ import annotations
import typing as t
from http import HTTPStatus
from flask import Flask
from flask import jsonify
from flask import stream_template
from flask.templating import render_template
from flask.views import View
from flask.wrappers import Response
app = Flask(__name__)
@app.route("/str")
def hello_str() -> str:
return "<p>Hello, World!</p>"
@app.route("/bytes")
def hello_bytes() -> bytes:
return b"<p>Hello, World!</p>"
@app.route("/json")
def hello_json() -> Response:
return jsonify("Hello, World!")
@app.route("/json/dict")
def hello_json_dict() -> dict[str, t.Any]:
return {"response": "Hello, World!"}
@app.route("/json/dict")
def hello_json_list() -> list[t.Any]:
return [{"message": "Hello"}, {"message": "World"}]
class StatusJSON(t.TypedDict):
status: str
@app.route("/typed-dict")
def typed_dict() -> StatusJSON:
return {"status": "ok"}
@app.route("/generator")
def hello_generator() -> t.Generator[str, None, None]:
def show() -> t.Generator[str, None, None]:
for x in range(100):
yield f"data:{x}\n\n"
return show()
@app.route("/generator-expression")
def hello_generator_expression() -> t.Iterator[bytes]:
return (f"data:{x}\n\n".encode() for x in range(100))
@app.route("/iterator")
def hello_iterator() -> t.Iterator[str]:
return iter([f"data:{x}\n\n" for x in range(100)])
@app.route("/status")
@app.route("/status/<int:code>")
def tuple_status(code: int = 200) -> tuple[str, int]:
return "hello", code
@app.route("/status-enum")
def tuple_status_enum() -> tuple[str, int]:
return "hello", HTTPStatus.OK
@app.route("/headers")
def tuple_headers() -> tuple[str, dict[str, str]]:
return "Hello, World!", {"Content-Type": "text/plain"}
@app.route("/template")
@app.route("/template/<name>")
def return_template(name: str | None = None) -> str:
return render_template("index.html", name=name)
@app.route("/template")
def return_template_stream() -> t.Iterator[str]:
return stream_template("index.html", name="Hello")
@app.route("/async")
async def async_route() -> str:
return "Hello"
class RenderTemplateView(View):
def __init__(self: RenderTemplateView, template_name: str) -> None:
self.template_name = template_name
def dispatch_request(self: RenderTemplateView) -> str:
return render_template(self.template_name)
app.add_url_rule(
"/about",
view_func=RenderTemplateView.as_view("about_page", template_name="about.html"),
)

Sorry, the diff of this file is too big to display