Flask
Advanced tools
| 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"), | ||
| ) |
@@ -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 |
+5
-1
@@ -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 |
+1
-1
@@ -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", |
+7
-27
@@ -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 @@ |
+15
-11
| [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", | ||
| ] |
+0
-20
@@ -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 |
+91
-88
| # | ||
| # 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 |
+24
-21
| # | ||
| # 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 |
+13
-14
| # | ||
| # 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 |
+78
-40
@@ -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 |
+19
-20
@@ -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) |
+72
-48
@@ -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. |
+18
-5
@@ -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 @@ |
+31
-12
| 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 |
+11
-11
@@ -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 @@ |
+68
-21
| 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 |
+14
-22
@@ -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 @@ |
+20
-4
@@ -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 @@ |
+21
-10
@@ -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
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
1532952
1.47%235
1.73%13870
1.75%