From b109b092a95036de875246f05cdc377a2c7faab8 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 28 Mar 2022 12:17:25 -0700 Subject: [PATCH 001/229] start version 2.2.0 --- CHANGES.rst | 6 ++++++ src/flask/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4d229203fc..07e6924009 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,11 @@ .. currentmodule:: flask +Version 2.2.0 +------------- + +Unreleased + + Version 2.1.0 ------------- diff --git a/src/flask/__init__.py b/src/flask/__init__.py index e721eddda9..f4b8c75c55 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -42,4 +42,4 @@ from .templating import render_template as render_template from .templating import render_template_string as render_template_string -__version__ = "2.1.0" +__version__ = "2.2.0.dev0" From 9f4f559f5946306b190a0195baf2700851a9eb36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Apr 2022 16:04:08 +0000 Subject: [PATCH 002/229] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 733676b48d..7ad21db9ce 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -36,7 +36,7 @@ jobs: - {name: 'Pallets Development Versions', python: '3.7', os: ubuntu-latest, tox: py-dev} - {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} From 8ddbad9ccdc176b9d57a4aff0076c1c58c455318 Mon Sep 17 00:00:00 2001 From: DailyDreaming Date: Mon, 2 May 2022 07:46:09 -0700 Subject: [PATCH 003/229] Fix linting error. Suppress mypy. Suppress mypy error. Suppress mypy error. --- src/flask/cli.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/flask/cli.py b/src/flask/cli.py index 36c4f1b6dc..efcc0f99bb 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -9,6 +9,8 @@ from operator import attrgetter from threading import Lock from threading import Thread +from typing import Any +from typing import TYPE_CHECKING import click from werkzeug.utils import import_string @@ -36,7 +38,12 @@ # We technically have importlib.metadata on 3.8+, # but the API changed in 3.10, so use the backport # for consistency. - import importlib_metadata as metadata # type: ignore + if TYPE_CHECKING: + metadata: Any + else: + # we do this to avoid a version dependent mypy error + # because importlib_metadata is not installed in python3.10+ + import importlib_metadata as metadata class NoAppException(click.UsageError): From 9158d3b0b8c282e6d8580b76fb5098002d4ec0d4 Mon Sep 17 00:00:00 2001 From: Maria Andrea Vignau Date: Mon, 2 May 2022 11:58:24 -0600 Subject: [PATCH 004/229] remove tab directive --- CONTRIBUTING.rst | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a3c8b8512e..1ee593e200 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -98,21 +98,20 @@ First time setup - Create a virtualenv. - .. tabs:: - .. group-tab:: Linux/macOS + - Linux/macOS - .. code-block:: text + .. code-block:: text - $ python3 -m venv env - $ . env/bin/activate + $ python3 -m venv env + $ . env/bin/activate - .. group-tab:: Windows + - Windows - .. code-block:: text + .. code-block:: text - > py -3 -m venv env - > env\Scripts\activate + > py -3 -m venv env + > env\Scripts\activate - Upgrade pip and setuptools. From 1e5dd430223369d13ea94ffffe22bca53a98e730 Mon Sep 17 00:00:00 2001 From: Qingpeng Li Date: Fri, 29 Apr 2022 01:49:23 +0800 Subject: [PATCH 005/229] refactor error checks in register_error_handler Co-authored-by: David Lord --- CHANGES.rst | 3 +++ src/flask/scaffold.py | 43 +++++++++++++++++--------------- tests/test_basic.py | 7 ------ tests/test_user_error_handler.py | 26 +++++++++++-------- 4 files changed, 42 insertions(+), 37 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index aa2bddc193..285d632683 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Version 2.2.0 Unreleased +- Refactor ``register_error_handler`` to consolidate error checking. + Rewrite some error messages to be more consistent. :issue:`4559` + Version 2.1.2 ------------- diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index acf8708b55..f8819ccffe 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -697,22 +697,7 @@ def register_error_handler( .. versionadded:: 0.7 """ - if isinstance(code_or_exception, HTTPException): # old broken behavior - raise ValueError( - "Tried to register a handler for an exception instance" - f" {code_or_exception!r}. Handlers can only be" - " registered for exception classes or HTTP error codes." - ) - - try: - exc_class, code = self._get_exc_class_and_code(code_or_exception) - except KeyError: - raise KeyError( - f"'{code_or_exception}' is not a recognized HTTP error" - " code. Use a subclass of HTTPException with that code" - " instead." - ) from None - + exc_class, code = self._get_exc_class_and_code(code_or_exception) self.error_handler_spec[None][code][exc_class] = f @staticmethod @@ -727,14 +712,32 @@ def _get_exc_class_and_code( code as an integer. """ exc_class: t.Type[Exception] + if isinstance(exc_class_or_code, int): - exc_class = default_exceptions[exc_class_or_code] + try: + exc_class = default_exceptions[exc_class_or_code] + except KeyError: + raise ValueError( + f"'{exc_class_or_code}' is not a recognized HTTP" + " error code. Use a subclass of HTTPException with" + " that code instead." + ) from None else: exc_class = exc_class_or_code - assert issubclass( - exc_class, Exception - ), "Custom exceptions must be subclasses of Exception." + if isinstance(exc_class, Exception): + raise TypeError( + f"{exc_class!r} is an instance, not a class. Handlers" + " can only be registered for Exception classes or HTTP" + " error codes." + ) + + if not issubclass(exc_class, Exception): + raise ValueError( + f"'{exc_class.__name__}' is not a subclass of Exception." + " Handlers can only be registered for Exception classes" + " or HTTP error codes." + ) if issubclass(exc_class, HTTPException): return exc_class, exc_class.code diff --git a/tests/test_basic.py b/tests/test_basic.py index 3dc3a0e9a7..c4be662134 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -899,13 +899,6 @@ def error2(): assert b"forbidden" == rv.data -def test_error_handler_unknown_code(app): - with pytest.raises(KeyError) as exc_info: - app.register_error_handler(999, lambda e: ("999", 999)) - - assert "Use a subclass" in exc_info.value.args[0] - - def test_error_handling_processing(app, client): app.testing = False diff --git a/tests/test_user_error_handler.py b/tests/test_user_error_handler.py index d9f94a3f64..79c5a73c6a 100644 --- a/tests/test_user_error_handler.py +++ b/tests/test_user_error_handler.py @@ -11,29 +11,35 @@ def test_error_handler_no_match(app, client): class CustomException(Exception): pass - class UnacceptableCustomException(BaseException): - pass - @app.errorhandler(CustomException) def custom_exception_handler(e): assert isinstance(e, CustomException) return "custom" - with pytest.raises( - AssertionError, match="Custom exceptions must be subclasses of Exception." - ): - app.register_error_handler(UnacceptableCustomException, None) + with pytest.raises(TypeError) as exc_info: + app.register_error_handler(CustomException(), None) + + assert "CustomException() is an instance, not a class." in str(exc_info.value) + + with pytest.raises(ValueError) as exc_info: + app.register_error_handler(list, None) + + assert "'list' is not a subclass of Exception." in str(exc_info.value) @app.errorhandler(500) def handle_500(e): assert isinstance(e, InternalServerError) - original = getattr(e, "original_exception", None) - if original is not None: - return f"wrapped {type(original).__name__}" + if e.original_exception is not None: + return f"wrapped {type(e.original_exception).__name__}" return "direct" + with pytest.raises(ValueError) as exc_info: + app.register_error_handler(999, None) + + assert "Use a subclass of HTTPException" in str(exc_info.value) + @app.route("/custom") def custom_test(): raise CustomException() From a74e266474f0981e9fc0d54d5dc430386823e4d7 Mon Sep 17 00:00:00 2001 From: Stanislav Bushuev Date: Mon, 2 May 2022 14:42:23 +0200 Subject: [PATCH 006/229] skip coverage for TYPE_CHECKING --- src/flask/app.py | 2 +- src/flask/blueprints.py | 2 +- src/flask/ctx.py | 2 +- src/flask/globals.py | 2 +- src/flask/helpers.py | 2 +- src/flask/json/__init__.py | 2 +- src/flask/logging.py | 2 +- src/flask/scaffold.py | 2 +- src/flask/sessions.py | 2 +- src/flask/templating.py | 2 +- src/flask/testing.py | 2 +- src/flask/typing.py | 2 +- src/flask/wrappers.py | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 348bc7f74f..ac82f72a06 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -67,7 +67,7 @@ from .wrappers import Request from .wrappers import Response -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover import typing_extensions as te from .blueprints import Blueprint from .testing import FlaskClient diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 5d3b4e224c..c801d1b4cc 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -17,7 +17,7 @@ from .typing import URLDefaultCallable from .typing import URLValuePreprocessorCallable -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover from .app import Flask from .typing import ErrorHandlerCallable diff --git a/src/flask/ctx.py b/src/flask/ctx.py index 3ed8fd3a80..bd25b9b823 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -11,7 +11,7 @@ from .signals import appcontext_pushed from .typing import AfterRequestCallable -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover from .app import Flask from .sessions import SessionMixin from .wrappers import Request diff --git a/src/flask/globals.py b/src/flask/globals.py index 6d91c75edd..7824204fe9 100644 --- a/src/flask/globals.py +++ b/src/flask/globals.py @@ -4,7 +4,7 @@ from werkzeug.local import LocalProxy from werkzeug.local import LocalStack -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover from .app import Flask from .ctx import _AppCtxGlobals from .sessions import SessionMixin diff --git a/src/flask/helpers.py b/src/flask/helpers.py index fe47500eaf..656dab7681 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -20,7 +20,7 @@ from .globals import session from .signals import message_flashed -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover from .wrappers import Response diff --git a/src/flask/json/__init__.py b/src/flask/json/__init__.py index edc9793df4..574e9e746a 100644 --- a/src/flask/json/__init__.py +++ b/src/flask/json/__init__.py @@ -11,7 +11,7 @@ from ..globals import current_app from ..globals import request -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover from ..app import Flask from ..wrappers import Response diff --git a/src/flask/logging.py b/src/flask/logging.py index 48a5b7ff4c..8981b82055 100644 --- a/src/flask/logging.py +++ b/src/flask/logging.py @@ -6,7 +6,7 @@ from .globals import request -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover from .app import Flask diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index f8819ccffe..10e77bbcb4 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -26,7 +26,7 @@ from .typing import URLDefaultCallable from .typing import URLValuePreprocessorCallable -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover from .wrappers import Response from .typing import ErrorHandlerCallable diff --git a/src/flask/sessions.py b/src/flask/sessions.py index 4e19270e0d..a14aecbac4 100644 --- a/src/flask/sessions.py +++ b/src/flask/sessions.py @@ -11,7 +11,7 @@ from .helpers import is_ip from .json.tag import TaggedJSONSerializer -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover import typing_extensions as te from .app import Flask from .wrappers import Request, Response diff --git a/src/flask/templating.py b/src/flask/templating.py index b39adb77ea..36a8645c85 100644 --- a/src/flask/templating.py +++ b/src/flask/templating.py @@ -10,7 +10,7 @@ from .signals import before_render_template from .signals import template_rendered -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover from .app import Flask from .scaffold import Scaffold diff --git a/src/flask/testing.py b/src/flask/testing.py index e07e50e7a2..df69d90303 100644 --- a/src/flask/testing.py +++ b/src/flask/testing.py @@ -14,7 +14,7 @@ from .json import dumps as json_dumps from .sessions import SessionMixin -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover from werkzeug.test import TestResponse from .app import Flask diff --git a/src/flask/typing.py b/src/flask/typing.py index a839a7e48b..761d77fc64 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -1,7 +1,7 @@ import typing as t -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover from _typeshed.wsgi import WSGIApplication # noqa: F401 from werkzeug.datastructures import Headers # noqa: F401 from werkzeug.wrappers.response import Response # noqa: F401 diff --git a/src/flask/wrappers.py b/src/flask/wrappers.py index 7153876b8d..4b855bfccc 100644 --- a/src/flask/wrappers.py +++ b/src/flask/wrappers.py @@ -8,7 +8,7 @@ from .globals import current_app from .helpers import _split_blueprint_path -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover from werkzeug.routing import Rule From 11195f10838d84df0c5ab7158a99bbb94e34fc5a Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 4 May 2022 06:03:31 -0700 Subject: [PATCH 007/229] remove outdated dotenv docs The CLI does not change the working directory when loading a dotenv file. --- docs/cli.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index 4b40307e98..3be3aaa64c 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -299,9 +299,7 @@ used for public variables, such as ``FLASK_APP``, while :file:`.env` should not be committed to your repository so that it can set private variables. Directories are scanned upwards from the directory you call ``flask`` -from to locate the files. The current working directory will be set to the -location of the file, with the assumption that that is the top level project -directory. +from to locate the files. The files are only loaded by the ``flask`` command or calling :meth:`~Flask.run`. If you would like to load these files when running in From 410a324ec43ca2793a0413d3cd32ca45311e3ff5 Mon Sep 17 00:00:00 2001 From: Luis Palacios <60991609+moondial-pal@users.noreply.github.com> Date: Mon, 9 May 2022 22:22:16 -0700 Subject: [PATCH 008/229] remove becomingbig.rst --- docs/becomingbig.rst | 100 ------------------------------------- docs/design.rst | 3 -- docs/foreword.rst | 8 +-- docs/index.rst | 1 - docs/patterns/packages.rst | 4 -- 5 files changed, 4 insertions(+), 112 deletions(-) delete mode 100644 docs/becomingbig.rst diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst deleted file mode 100644 index 5e7a88e030..0000000000 --- a/docs/becomingbig.rst +++ /dev/null @@ -1,100 +0,0 @@ -Becoming Big -============ - -Here are your options when growing your codebase or scaling your application. - -Read the Source. ----------------- - -Flask started in part to demonstrate how to build your own framework on top of -existing well-used tools Werkzeug (WSGI) and Jinja (templating), and as it -developed, it became useful to a wide audience. As you grow your codebase, -don't just use Flask -- understand it. Read the source. Flask's code is -written to be read; its documentation is published so you can use its internal -APIs. Flask sticks to documented APIs in upstream libraries, and documents its -internal utilities so that you can find the hook points needed for your -project. - -Hook. Extend. -------------- - -The :doc:`/api` docs are full of available overrides, hook points, and -:doc:`/signals`. You can provide custom classes for things like the -request and response objects. Dig deeper on the APIs you use, and look -for the customizations which are available out of the box in a Flask -release. Look for ways in which your project can be refactored into a -collection of utilities and Flask extensions. Explore the many -:doc:`/extensions` in the community, and look for patterns to build your -own extensions if you do not find the tools you need. - -Subclass. ---------- - -The :class:`~flask.Flask` class has many methods designed for subclassing. You -can quickly add or customize behavior by subclassing :class:`~flask.Flask` (see -the linked method docs) and using that subclass wherever you instantiate an -application class. This works well with :doc:`/patterns/appfactories`. -See :doc:`/patterns/subclassing` for an example. - -Wrap with middleware. ---------------------- - -The :doc:`/patterns/appdispatch` pattern shows in detail how to apply middleware. You -can introduce WSGI middleware to wrap your Flask instances and introduce fixes -and changes at the layer between your Flask application and your HTTP -server. Werkzeug includes several `middlewares -`_. - -Fork. ------ - -If none of the above options work, fork Flask. The majority of code of Flask -is within Werkzeug and Jinja2. These libraries do the majority of the work. -Flask is just the paste that glues those together. For every project there is -the point where the underlying framework gets in the way (due to assumptions -the original developers had). This is natural because if this would not be the -case, the framework would be a very complex system to begin with which causes a -steep learning curve and a lot of user frustration. - -This is not unique to Flask. Many people use patched and modified -versions of their framework to counter shortcomings. This idea is also -reflected in the license of Flask. You don't have to contribute any -changes back if you decide to modify the framework. - -The downside of forking is of course that Flask extensions will most -likely break because the new framework has a different import name. -Furthermore integrating upstream changes can be a complex process, -depending on the number of changes. Because of that, forking should be -the very last resort. - -Scale like a pro. ------------------ - -For many web applications the complexity of the code is less an issue than -the scaling for the number of users or data entries expected. Flask by -itself is only limited in terms of scaling by your application code, the -data store you want to use and the Python implementation and webserver you -are running on. - -Scaling well means for example that if you double the amount of servers -you get about twice the performance. Scaling bad means that if you add a -new server the application won't perform any better or would not even -support a second server. - -There is only one limiting factor regarding scaling in Flask which are -the context local proxies. They depend on context which in Flask is -defined as being either a thread, process or greenlet. If your server -uses some kind of concurrency that is not based on threads or greenlets, -Flask will no longer be able to support these global proxies. However the -majority of servers are using either threads, greenlets or separate -processes to achieve concurrency which are all methods well supported by -the underlying Werkzeug library. - -Discuss with the community. ---------------------------- - -The Flask developers keep the framework accessible to users with codebases big -and small. If you find an obstacle in your way, caused by Flask, don't hesitate -to contact the developers on the mailing list or Discord server. The best way for -the Flask and Flask extension developers to improve the tools for larger -applications is getting feedback from users. diff --git a/docs/design.rst b/docs/design.rst index 5d57063e20..a5ef2568cd 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -167,9 +167,6 @@ large applications harder to maintain. However Flask is just not designed for large applications or asynchronous servers. Flask wants to make it quick and easy to write a traditional web application. -Also see the :doc:`/becomingbig` section of the documentation for some -inspiration for larger applications based on Flask. - Async/await and ASGI support ---------------------------- diff --git a/docs/foreword.rst b/docs/foreword.rst index 6a6d17f974..28b272d716 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -47,7 +47,7 @@ SQLAlchemy or another database tool, introduce non-relational data persistence as appropriate, and take advantage of framework-agnostic tools built for WSGI, the Python web interface. -Flask includes many hooks to customize its behavior. Should you need more -customization, the Flask class is built for subclassing. If you are interested -in that, check out the :doc:`becomingbig` chapter. If you are curious about -the Flask design principles, head over to the section about :doc:`design`. +Flask includes many hooks to customize its behavior. Should you need +more customization, the Flask class is built for subclassing. If you are +curious about the Flask design principles, head over to the section +about :doc:`design`. diff --git a/docs/index.rst b/docs/index.rst index 6ff6252940..c0066e8c36 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -58,7 +58,6 @@ instructions for web development with Flask. shell patterns/index deploying/index - becomingbig async-await diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 7c3a34cf7c..a30ef3cbc3 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -173,10 +173,6 @@ You should then end up with something like that:: ensuring the module is imported and we are doing that at the bottom of the file. - There are still some problems with that approach but if you want to use - decorators there is no way around that. Check out the - :doc:`/becomingbig` section for some inspiration how to deal with that. - Working with Blueprints ----------------------- From 2381044d04cdb058a526c0e27163b4d891d851ed Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 10 May 2022 09:27:54 -0700 Subject: [PATCH 009/229] remove references to mailing list --- .gitignore | 1 - CONTRIBUTING.rst | 5 ++--- docs/extensiondev.rst | 14 +++++++------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index e50a290eba..e6713351c1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ dist/ build/ *.egg *.egg-info/ -_mailinglist .tox/ .cache/ .pytest_cache/ diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 1ee593e200..d5e3a3f776 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -14,11 +14,10 @@ own code: - The ``#questions`` channel on our Discord chat: https://discord.gg/pallets -- The mailing list flask@python.org for long term discussion or larger - issues. - Ask on `Stack Overflow`_. Search with Google first using: ``site:stackoverflow.com flask {search term, exception message, etc.}`` -- Ask on our `GitHub Discussions`_. +- Ask on our `GitHub Discussions`_ for long term discussion or larger + questions. .. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent .. _GitHub Discussions: https://github.com/pallets/flask/discussions diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index dbaf62cb1a..34b118ce5a 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -271,16 +271,16 @@ Learn from Others This documentation only touches the bare minimum for extension development. If you want to learn more, it's a very good idea to check out existing extensions -on the `PyPI`_. If you feel lost there is still the `mailinglist`_ and the -`Discord server`_ to get some ideas for nice looking APIs. Especially if you do +on `PyPI`_. If you feel lost there is `Discord Chat`_ or +`GitHub Discussions`_ to get some ideas for nice looking APIs. Especially if you do something nobody before you did, it might be a very good idea to get some more input. This not only generates useful feedback on what people might want from an extension, but also avoids having multiple developers working in isolation on pretty much the same problem. -Remember: good API design is hard, so introduce your project on the -mailing list, and let other developers give you a helping hand with -designing the API. +Remember: good API design is hard, so introduce your project on +`Discord Chat`_ or `GitHub Discussions`_, and let other developers give +you a helping hand with designing the API. The best Flask extensions are extensions that share common idioms for the API. And this can only work if collaboration happens early. @@ -327,6 +327,6 @@ ecosystem remain consistent and compatible. indicate supported versions. .. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask -.. _mailinglist: https://mail.python.org/mailman/listinfo/flask -.. _Discord server: https://discord.gg/pallets +.. _Discord Chat: https://discord.gg/pallets +.. _GitHub Discussions: https://github.com/pallets/flask/discussions .. _Official Pallets Themes: https://pypi.org/project/Pallets-Sphinx-Themes/ From fdab801fbbd9de5adbdb3320ca4a1cb116c892f5 Mon Sep 17 00:00:00 2001 From: Tim Hoagland Date: Mon, 2 May 2022 12:44:15 -0400 Subject: [PATCH 010/229] add redirect method to app --- CHANGES.rst | 3 +++ src/flask/__init__.py | 2 +- src/flask/app.py | 11 +++++++++++ src/flask/helpers.py | 24 ++++++++++++++++++++++++ tests/test_helpers.py | 16 ++++++++++++++++ 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 285d632683..711003555f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Version 2.2.0 Unreleased +- Add an ``app.redirect`` method, which ``flask.redirect`` will call. + This makes it possible for an app to override how redirects work. + :issue:`4569` - Refactor ``register_error_handler`` to consolidate error checking. Rewrite some error messages to be more consistent. :issue:`4559` diff --git a/src/flask/__init__.py b/src/flask/__init__.py index f4b8c75c55..f71a7d42b2 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -1,7 +1,6 @@ from markupsafe import escape from markupsafe import Markup from werkzeug.exceptions import abort as abort -from werkzeug.utils import redirect as redirect from . import json as json from .app import Flask as Flask @@ -23,6 +22,7 @@ from .helpers import get_flashed_messages as get_flashed_messages from .helpers import get_template_attribute as get_template_attribute from .helpers import make_response as make_response +from .helpers import redirect as redirect from .helpers import send_file as send_file from .helpers import send_from_directory as send_from_directory from .helpers import stream_with_context as stream_with_context diff --git a/src/flask/app.py b/src/flask/app.py index ac82f72a06..8ce14301ac 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -22,6 +22,7 @@ from werkzeug.routing import RequestRedirect from werkzeug.routing import RoutingException from werkzeug.routing import Rule +from werkzeug.utils import redirect as _wz_redirect from werkzeug.wrappers import Response as BaseResponse from . import cli @@ -1630,6 +1631,16 @@ def async_to_sync( return asgiref_async_to_sync(func) + def redirect(self, location: str, code: int = 302) -> BaseResponse: + """Create a redirect response object. + + :param location: the url of the redirect + :param code: http return code + + .. versionadded:: 2.2 + """ + return _wz_redirect(location, code=code, Response=self.response_class) + def make_response(self, rv: ResponseReturnValue) -> Response: """Convert the return value from a view function to an instance of :attr:`response_class`. diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 656dab7681..e3883ed601 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -12,6 +12,7 @@ import werkzeug.utils from werkzeug.routing import BuildError from werkzeug.urls import url_quote +from werkzeug.utils import redirect as _wz_redirect from .globals import _app_ctx_stack from .globals import _request_ctx_stack @@ -21,6 +22,7 @@ from .signals import message_flashed if t.TYPE_CHECKING: # pragma: no cover + from werkzeug.wrappers import Response as BaseResponse from .wrappers import Response @@ -340,6 +342,28 @@ def external_url_handler(error, endpoint, values): return rv +def redirect( + location: str, code: int = 302, Response: t.Optional[t.Type["BaseResponse"]] = None +) -> "BaseResponse": + """Create a redirect response object. + + If :data:`~flask.current_app` is available, it will use + :meth:`~flask.app.Flask.redirect`, otherwise it will use + :func:`werkzeug.utils.redirect`. + + :param location: The URL to redirect to. + :param code: The status code for the redirect. + :param Response: The response class to use. Not used when + ``current_app`` is active, which uses ``app.response_class``. + + .. versionadded:: 2.2 + """ + if current_app: + return current_app.redirect(location, code=code) + + return _wz_redirect(location, code=code, Response=Response) + + def get_template_attribute(template_name: str, attribute: str) -> t.Any: """Loads a macro (or variable) a template exports. This can be used to invoke a macro from within Python code. If you for example have a diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 58bf3c0b2d..63dbbc135a 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -158,6 +158,22 @@ def post(self): assert flask.url_for("myview", _method="POST") == "/myview/create" +def test_redirect_no_app(): + response = flask.redirect("https://localhost", 307) + assert response.location == "https://localhost" + assert response.status_code == 307 + + +def test_redirect_with_app(app): + def redirect(location, code=302): + raise ValueError + + app.redirect = redirect + + with app.app_context(), pytest.raises(ValueError): + flask.redirect("other") + + class TestNoImports: """Test Flasks are created without import. From eb5dd9f5ef255c578cbbe13c1cb4dd11389d5519 Mon Sep 17 00:00:00 2001 From: dzcode <9089037+dzcode@users.noreply.github.com> Date: Mon, 2 May 2022 10:16:12 -0600 Subject: [PATCH 011/229] add aborter object to app --- CHANGES.rst | 4 ++++ src/flask/__init__.py | 2 +- src/flask/app.py | 30 ++++++++++++++++++++++++++++++ src/flask/helpers.py | 27 +++++++++++++++++++++++++++ tests/test_helpers.py | 30 ++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 711003555f..153e964b5e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,10 @@ Version 2.2.0 Unreleased +- Add ``aborter_class`` and ``aborter`` attributes to the Flask app + object. ``flask.abort`` will call ``app.aborter``. This makes it + possible for an app to override how aborts work, including custom + status codes. :issue:`4567` - Add an ``app.redirect`` method, which ``flask.redirect`` will call. This makes it possible for an app to override how redirects work. :issue:`4569` diff --git a/src/flask/__init__.py b/src/flask/__init__.py index f71a7d42b2..bc93e0a34b 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -1,6 +1,5 @@ from markupsafe import escape from markupsafe import Markup -from werkzeug.exceptions import abort as abort from . import json as json from .app import Flask as Flask @@ -18,6 +17,7 @@ from .globals import g as g from .globals import request as request from .globals import session as session +from .helpers import abort as abort from .helpers import flash as flash from .helpers import get_flashed_messages as get_flashed_messages from .helpers import get_template_attribute as get_template_attribute diff --git a/src/flask/app.py b/src/flask/app.py index 8ce14301ac..219cde242a 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -12,6 +12,7 @@ from werkzeug.datastructures import Headers from werkzeug.datastructures import ImmutableDict +from werkzeug.exceptions import Aborter from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequestKeyError from werkzeug.exceptions import HTTPException @@ -201,6 +202,16 @@ class Flask(Scaffold): #: :class:`~flask.Response` for more information. response_class = Response + #: The class of the object assigned to :attr:`aborter`, created by + #: :meth:`create_aborter`. That object is called by + #: :func:`flask.abort` to raise HTTP errors, and can be + #: called directly as well. + #: + #: Defaults to :class:`werkzeug.exceptions.Aborter`. + #: + #: .. versionadded:: 2.2 + aborter_class = Aborter + #: The class that is used for the Jinja environment. #: #: .. versionadded:: 0.11 @@ -421,6 +432,13 @@ def __init__( #: to load a config from files. self.config = self.make_config(instance_relative_config) + #: An instance of :attr:`aborter_class` created by + #: :meth:`make_aborter`. This is called by :func:`flask.abort` + #: to raise HTTP errors, and can be called directly as well. + #: + #: .. versionadded:: 2.2 + self.aborter = self.make_aborter() + #: A list of functions that are called when :meth:`url_for` raises a #: :exc:`~werkzeug.routing.BuildError`. Each function registered here #: is called with `error`, `endpoint` and `values`. If a function @@ -628,6 +646,18 @@ def make_config(self, instance_relative: bool = False) -> Config: defaults["DEBUG"] = get_debug_flag() return self.config_class(root_path, defaults) + def make_aborter(self) -> Aborter: + """Create the object to assign to :attr:`aborter`. That object + is called by :func:`flask.abort` to raise HTTP errors, and can + be called directly as well. + + By default, this creates an instance of :attr:`aborter_class`, + which defaults to :class:`werkzeug.exceptions.Aborter`. + + .. versionadded:: 2.2 + """ + return self.aborter_class() + def auto_find_instance_path(self) -> str: """Tries to locate the instance path if it was not provided to the constructor of the application class. It will basically calculate diff --git a/src/flask/helpers.py b/src/flask/helpers.py index e3883ed601..070a4dafd5 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -10,6 +10,7 @@ from threading import RLock import werkzeug.utils +from werkzeug.exceptions import abort as _wz_abort from werkzeug.routing import BuildError from werkzeug.urls import url_quote from werkzeug.utils import redirect as _wz_redirect @@ -24,6 +25,7 @@ if t.TYPE_CHECKING: # pragma: no cover from werkzeug.wrappers import Response as BaseResponse from .wrappers import Response + import typing_extensions as te def get_env() -> str: @@ -364,6 +366,31 @@ def redirect( return _wz_redirect(location, code=code, Response=Response) +def abort( # type: ignore[misc] + code: t.Union[int, "BaseResponse"], *args: t.Any, **kwargs: t.Any +) -> "te.NoReturn": + """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given + status code. + + If :data:`~flask.current_app` is available, it will call its + :attr:`~flask.Flask.aborter` object, otherwise it will use + :func:`werkzeug.exceptions.abort`. + + :param code: The status code for the exception, which must be + registered in ``app.aborter``. + :param args: Passed to the exception. + :param kwargs: Passed to the exception. + + .. versionadded:: 2.2 + Calls ``current_app.aborter`` if available instead of always + using Werkzeug's default ``abort``. + """ + if current_app: + current_app.aborter(code, *args, **kwargs) + + _wz_abort(code, *args, **kwargs) + + def get_template_attribute(template_name: str, attribute: str) -> t.Any: """Loads a macro (or variable) a template exports. This can be used to invoke a macro from within Python code. If you for example have a diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 63dbbc135a..0893893f88 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -2,6 +2,7 @@ import os import pytest +import werkzeug.exceptions import flask from flask.helpers import get_debug_flag @@ -174,6 +175,35 @@ def redirect(location, code=302): flask.redirect("other") +def test_abort_no_app(): + with pytest.raises(werkzeug.exceptions.Unauthorized): + flask.abort(401) + + with pytest.raises(LookupError): + flask.abort(900) + + +def test_app_aborter_class(): + class MyAborter(werkzeug.exceptions.Aborter): + pass + + class MyFlask(flask.Flask): + aborter_class = MyAborter + + app = MyFlask(__name__) + assert isinstance(app.aborter, MyAborter) + + +def test_abort_with_app(app): + class My900Error(werkzeug.exceptions.HTTPException): + code = 900 + + app.aborter.mapping[900] = My900Error + + with app.app_context(), pytest.raises(My900Error): + flask.abort(900) + + class TestNoImports: """Test Flasks are created without import. From fac630379d31baaa1667894c2b5613304285a4bb Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 12 May 2022 16:30:01 -0700 Subject: [PATCH 012/229] update app.redirect docs --- CHANGES.rst | 15 ++++++++------- src/flask/app.py | 7 +++++-- src/flask/helpers.py | 6 ++++-- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 153e964b5e..3b841e1a38 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,13 +5,14 @@ Version 2.2.0 Unreleased -- Add ``aborter_class`` and ``aborter`` attributes to the Flask app - object. ``flask.abort`` will call ``app.aborter``. This makes it - possible for an app to override how aborts work, including custom - status codes. :issue:`4567` -- Add an ``app.redirect`` method, which ``flask.redirect`` will call. - This makes it possible for an app to override how redirects work. - :issue:`4569` +- Add new customization points to the ``Flask`` app object for many + previously global behaviors. + + - ``flask.abort`` will call ``app.aborter``. + ``Flask.aborter_class`` and ``Flask.make_aborter`` can be used + to customize this aborter. :issue:`4567` + - ``flask.redirect`` will call ``app.redirect``. :issue:`4569` + - Refactor ``register_error_handler`` to consolidate error checking. Rewrite some error messages to be more consistent. :issue:`4559` diff --git a/src/flask/app.py b/src/flask/app.py index 219cde242a..ad859b27f1 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1664,8 +1664,11 @@ def async_to_sync( def redirect(self, location: str, code: int = 302) -> BaseResponse: """Create a redirect response object. - :param location: the url of the redirect - :param code: http return code + This is called by :func:`flask.redirect`, and can be called + directly as well. + + :param location: The URL to redirect to. + :param code: The status code for the redirect. .. versionadded:: 2.2 """ diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 070a4dafd5..dec9b77126 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -349,8 +349,8 @@ def redirect( ) -> "BaseResponse": """Create a redirect response object. - If :data:`~flask.current_app` is available, it will use - :meth:`~flask.app.Flask.redirect`, otherwise it will use + If :data:`~flask.current_app` is available, it will use its + :meth:`~flask.Flask.redirect` method, otherwise it will use :func:`werkzeug.utils.redirect`. :param location: The URL to redirect to. @@ -359,6 +359,8 @@ def redirect( ``current_app`` is active, which uses ``app.response_class``. .. versionadded:: 2.2 + Calls ``current_app.redirect`` if available instead of always + using Werkzeug's default ``redirect``. """ if current_app: return current_app.redirect(location, code=code) From 92acd05d9bd33419ab885e4472b28a1d5cc15d2e Mon Sep 17 00:00:00 2001 From: Ivan Sushkov Date: Mon, 2 May 2022 12:29:55 -0600 Subject: [PATCH 013/229] add url_for method to app --- src/flask/app.py | 45 ++++++++++++++++++++++++++++++++++++++++++++ src/flask/helpers.py | 38 +------------------------------------ 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index ad859b27f1..de883a921c 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -23,6 +23,7 @@ from werkzeug.routing import RequestRedirect from werkzeug.routing import RoutingException from werkzeug.routing import Rule +from werkzeug.urls import url_quote from werkzeug.utils import redirect as _wz_redirect from werkzeug.wrappers import Response as BaseResponse @@ -1661,6 +1662,50 @@ def async_to_sync( return asgiref_async_to_sync(func) + def url_for( + self, + endpoint: str, + external: bool, + url_adapter, + **values: t.Any, + ) -> str: + + anchor = values.pop("_anchor", None) + method = values.pop("_method", None) + scheme = values.pop("_scheme", None) + self.inject_url_defaults(endpoint, values) + + # This is not the best way to deal with this but currently the + # underlying Werkzeug router does not support overriding the scheme on + # a per build call basis. + old_scheme = None + if scheme is not None: + if not external: + raise ValueError("When specifying _scheme, _external must be True") + old_scheme = url_adapter.url_scheme + url_adapter.url_scheme = scheme + + try: + try: + rv = url_adapter.build( + endpoint, values, method=method, force_external=external + ) + finally: + if old_scheme is not None: + url_adapter.url_scheme = old_scheme + except BuildError as error: + # We need to inject the values again so that the app callback can + # deal with that sort of stuff. + values["_external"] = external + values["_anchor"] = anchor + values["_method"] = method + values["_scheme"] = scheme + return self.handle_url_build_error(error, endpoint, values) + + if anchor is not None: + rv += f"#{url_quote(anchor)}" + return rv + def redirect(self, location: str, code: int = 302) -> BaseResponse: """Create a redirect response object. diff --git a/src/flask/helpers.py b/src/flask/helpers.py index dec9b77126..167fa13232 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -11,8 +11,6 @@ import werkzeug.utils from werkzeug.exceptions import abort as _wz_abort -from werkzeug.routing import BuildError -from werkzeug.urls import url_quote from werkzeug.utils import redirect as _wz_redirect from .globals import _app_ctx_stack @@ -307,41 +305,7 @@ def external_url_handler(error, endpoint, values): external = values.pop("_external", True) - anchor = values.pop("_anchor", None) - method = values.pop("_method", None) - scheme = values.pop("_scheme", None) - appctx.app.inject_url_defaults(endpoint, values) - - # This is not the best way to deal with this but currently the - # underlying Werkzeug router does not support overriding the scheme on - # a per build call basis. - old_scheme = None - if scheme is not None: - if not external: - raise ValueError("When specifying _scheme, _external must be True") - old_scheme = url_adapter.url_scheme - url_adapter.url_scheme = scheme - - try: - try: - rv = url_adapter.build( - endpoint, values, method=method, force_external=external - ) - finally: - if old_scheme is not None: - url_adapter.url_scheme = old_scheme - except BuildError as error: - # We need to inject the values again so that the app callback can - # deal with that sort of stuff. - values["_external"] = external - values["_anchor"] = anchor - values["_method"] = method - values["_scheme"] = scheme - return appctx.app.handle_url_build_error(error, endpoint, values) - - if anchor is not None: - rv += f"#{url_quote(anchor)}" - return rv + return current_app.url_for(endpoint, external, url_adapter, **values) def redirect( From 39f93632964ecabfcb9c1980e2add922098aad80 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 14 May 2022 12:43:38 -0700 Subject: [PATCH 014/229] finish moving url_for to app move entire implementation to app make special build args actual keyword-only args handle no app context in method mention other config in server_name error implicit external with scheme use adapter.build url_scheme argument rewrite documentation --- src/flask/app.py | 171 ++++++++++++++++++++++++++++++++---------- src/flask/helpers.py | 162 ++++++++++++--------------------------- tests/test_helpers.py | 12 ++- 3 files changed, 189 insertions(+), 156 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index de883a921c..4da84e3011 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -34,6 +34,7 @@ from .ctx import _AppCtxGlobals from .ctx import AppContext from .ctx import RequestContext +from .globals import _app_ctx_stack from .globals import _request_ctx_stack from .globals import g from .globals import request @@ -440,15 +441,16 @@ def __init__( #: .. versionadded:: 2.2 self.aborter = self.make_aborter() - #: A list of functions that are called when :meth:`url_for` raises a - #: :exc:`~werkzeug.routing.BuildError`. Each function registered here - #: is called with `error`, `endpoint` and `values`. If a function - #: returns ``None`` or raises a :exc:`BuildError` the next function is - #: tried. + #: A list of functions that are called by + #: :meth:`handle_url_build_error` when :meth:`.url_for` raises a + #: :exc:`~werkzeug.routing.BuildError`. Each function is called + #: with ``error``, ``endpoint`` and ``values``. If a function + #: returns ``None`` or raises a ``BuildError``, it is skipped. + #: Otherwise, its return value is returned by ``url_for``. #: #: .. versionadded:: 0.9 self.url_build_error_handlers: t.List[ - t.Callable[[Exception, str, dict], str] + t.Callable[[Exception, str, t.Dict[str, t.Any]], str] ] = [] #: A list of functions that will be called at the beginning of the @@ -1665,45 +1667,125 @@ def async_to_sync( def url_for( self, endpoint: str, - external: bool, - url_adapter, + *, + _anchor: t.Optional[str] = None, + _method: t.Optional[str] = None, + _scheme: t.Optional[str] = None, + _external: t.Optional[bool] = None, **values: t.Any, ) -> str: + """Generate a URL to the given endpoint with the given values. - anchor = values.pop("_anchor", None) - method = values.pop("_method", None) - scheme = values.pop("_scheme", None) - self.inject_url_defaults(endpoint, values) + This is called by :func:`flask.url_for`, and can be called + directly as well. - # This is not the best way to deal with this but currently the - # underlying Werkzeug router does not support overriding the scheme on - # a per build call basis. - old_scheme = None - if scheme is not None: - if not external: - raise ValueError("When specifying _scheme, _external must be True") - old_scheme = url_adapter.url_scheme - url_adapter.url_scheme = scheme + An *endpoint* is the name of a URL rule, usually added with + :meth:`@app.route() `, and usually the same name as the + view function. A route defined in a :class:`~flask.Blueprint` + will prepend the blueprint's name separated by a ``.`` to the + endpoint. + + In some cases, such as email messages, you want URLs to include + the scheme and domain, like ``https://example.com/hello``. When + not in an active request, URLs will be external by default, but + this requires setting :data:`SERVER_NAME` so Flask knows what + domain to use. :data:`APPLICATION_ROOT` and + :data:`PREFERRED_URL_SCHEME` should also be configured as + needed. This config is only used when not in an active request. + + Functions can be decorated with :meth:`url_defaults` to modify + keyword arguments before the URL is built. + + If building fails for some reason, such as an unknown endpoint + or incorrect values, the app's :meth:`handle_url_build_error` + method is called. If that returns a string, that is returned, + otherwise a :exc:`~werkzeug.routing.BuildError` is raised. + + :param endpoint: The endpoint name associated with the URL to + generate. If this starts with a ``.``, the current blueprint + name (if any) will be used. + :param _anchor: If given, append this as ``#anchor`` to the URL. + :param _method: If given, generate the URL associated with this + method for the endpoint. + :param _scheme: If given, the URL will have this scheme if it + is external. + :param _external: If given, prefer the URL to be internal + (False) or require it to be external (True). External URLs + include the scheme and domain. When not in an active + request, URLs are external by default. + :param values: Values to use for the variable parts of the URL + rule. Unknown keys are appended as query string arguments, + like ``?a=b&c=d``. - try: - try: - rv = url_adapter.build( - endpoint, values, method=method, force_external=external + .. versionadded:: 2.2 + Moved from ``flask.url_for``, which calls this method. + """ + req_ctx = _request_ctx_stack.top + + if req_ctx is not None: + url_adapter = req_ctx.url_adapter + blueprint_name = req_ctx.request.blueprint + + # If the endpoint starts with "." and the request matches a + # blueprint, the endpoint is relative to the blueprint. + if endpoint[:1] == ".": + if blueprint_name is not None: + endpoint = f"{blueprint_name}{endpoint}" + else: + endpoint = endpoint[1:] + + # When in a request, generate a URL without scheme and + # domain by default, unless a scheme is given. + if _external is None: + _external = _scheme is not None + else: + app_ctx = _app_ctx_stack.top + + # If called by helpers.url_for, an app context is active, + # use its url_adapter. Otherwise, app.url_for was called + # directly, build an adapter. + if app_ctx is not None: + url_adapter = app_ctx.url_adapter + else: + url_adapter = self.create_url_adapter(None) + + if url_adapter is None: + raise RuntimeError( + "Unable to build URLs outside an active request" + " without 'SERVER_NAME' configured. Also configure" + " 'APPLICATION_ROOT' and 'PREFERRED_URL_SCHEME' as" + " needed." ) - finally: - if old_scheme is not None: - url_adapter.url_scheme = old_scheme + + # When outside a request, generate a URL with scheme and + # domain by default. + if _external is None: + _external = True + + # It is an error to set _scheme when _external=False, in order + # to avoid accidental insecure URLs. + if _scheme is not None and not _external: + raise ValueError("When specifying '_scheme', '_external' must be True.") + + self.inject_url_defaults(endpoint, values) + + try: + rv = url_adapter.build( + endpoint, + values, + method=_method, + url_scheme=_scheme, + force_external=_external, + ) except BuildError as error: - # We need to inject the values again so that the app callback can - # deal with that sort of stuff. - values["_external"] = external - values["_anchor"] = anchor - values["_method"] = method - values["_scheme"] = scheme + values.update( + _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external + ) return self.handle_url_build_error(error, endpoint, values) - if anchor is not None: - rv += f"#{url_quote(anchor)}" + if _anchor is not None: + rv = f"{rv}#{url_quote(_anchor)}" + return rv def redirect(self, location: str, code: int = 302) -> BaseResponse: @@ -1905,10 +1987,21 @@ def inject_url_defaults(self, endpoint: str, values: dict) -> None: func(endpoint, values) def handle_url_build_error( - self, error: Exception, endpoint: str, values: dict + self, error: BuildError, endpoint: str, values: t.Dict[str, t.Any] ) -> str: - """Handle :class:`~werkzeug.routing.BuildError` on - :meth:`url_for`. + """Called by :meth:`.url_for` if a + :exc:`~werkzeug.routing.BuildError` was raised. If this returns + a value, it will be returned by ``url_for``, otherwise the error + will be re-raised. + + Each function in :attr:`url_build_error_handlers` is called with + ``error``, ``endpoint`` and ``values``. If a function returns + ``None`` or raises a ``BuildError``, it is skipped. Otherwise, + its return value is returned by ``url_for``. + + :param error: The active ``BuildError`` being handled. + :param endpoint: The endpoint being built. + :param values: The keyword arguments passed to ``url_for``. """ for handler in self.url_build_error_handlers: try: diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 167fa13232..6928b20316 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -13,7 +13,6 @@ from werkzeug.exceptions import abort as _wz_abort from werkzeug.utils import redirect as _wz_redirect -from .globals import _app_ctx_stack from .globals import _request_ctx_stack from .globals import current_app from .globals import request @@ -191,121 +190,58 @@ def index(): return current_app.make_response(args) # type: ignore -def url_for(endpoint: str, **values: t.Any) -> str: - """Generates a URL to the given endpoint with the method provided. - - Variable arguments that are unknown to the target endpoint are appended - to the generated URL as query arguments. If the value of a query argument - is ``None``, the whole pair is skipped. In case blueprints are active - you can shortcut references to the same blueprint by prefixing the - local endpoint with a dot (``.``). - - This will reference the index function local to the current blueprint:: - - url_for('.index') - - See :ref:`url-building`. - - Configuration values ``APPLICATION_ROOT`` and ``SERVER_NAME`` are only used when - generating URLs outside of a request context. - - To integrate applications, :class:`Flask` has a hook to intercept URL build - errors through :attr:`Flask.url_build_error_handlers`. The `url_for` - function results in a :exc:`~werkzeug.routing.BuildError` when the current - app does not have a URL for the given endpoint and values. When it does, the - :data:`~flask.current_app` calls its :attr:`~Flask.url_build_error_handlers` if - it is not ``None``, which can return a string to use as the result of - `url_for` (instead of `url_for`'s default to raise the - :exc:`~werkzeug.routing.BuildError` exception) or re-raise the exception. - An example:: - - def external_url_handler(error, endpoint, values): - "Looks up an external URL when `url_for` cannot build a URL." - # This is an example of hooking the build_error_handler. - # Here, lookup_url is some utility function you've built - # which looks up the endpoint in some external URL registry. - url = lookup_url(endpoint, **values) - if url is None: - # External lookup did not have a URL. - # Re-raise the BuildError, in context of original traceback. - exc_type, exc_value, tb = sys.exc_info() - if exc_value is error: - raise exc_type(exc_value).with_traceback(tb) - else: - raise error - # url_for will use this result, instead of raising BuildError. - return url - - app.url_build_error_handlers.append(external_url_handler) - - Here, `error` is the instance of :exc:`~werkzeug.routing.BuildError`, and - `endpoint` and `values` are the arguments passed into `url_for`. Note - that this is for building URLs outside the current application, and not for - handling 404 NotFound errors. - - .. versionadded:: 0.10 - The `_scheme` parameter was added. +def url_for( + endpoint: str, + *, + _anchor: t.Optional[str] = None, + _method: t.Optional[str] = None, + _scheme: t.Optional[str] = None, + _external: t.Optional[bool] = None, + **values: t.Any, +) -> str: + """Generate a URL to the given endpoint with the given values. + + This requires an active request or application context, and calls + :meth:`current_app.url_for() `. See that method + for full documentation. + + :param endpoint: The endpoint name associated with the URL to + generate. If this starts with a ``.``, the current blueprint + name (if any) will be used. + :param _anchor: If given, append this as ``#anchor`` to the URL. + :param _method: If given, generate the URL associated with this + method for the endpoint. + :param _scheme: If given, the URL will have this scheme if it is + external. + :param _external: If given, prefer the URL to be internal (False) or + require it to be external (True). External URLs include the + scheme and domain. When not in an active request, URLs are + external by default. + :param values: Values to use for the variable parts of the URL rule. + Unknown keys are appended as query string arguments, like + ``?a=b&c=d``. + + .. versionchanged:: 2.2 + Calls ``current_app.url_for``, allowing an app to override the + behavior. + + .. versionchanged:: 0.10 + The ``_scheme`` parameter was added. - .. versionadded:: 0.9 - The `_anchor` and `_method` parameters were added. + .. versionchanged:: 0.9 + The ``_anchor`` and ``_method`` parameters were added. - .. versionadded:: 0.9 - Calls :meth:`Flask.handle_build_error` on - :exc:`~werkzeug.routing.BuildError`. - - :param endpoint: the endpoint of the URL (name of the function) - :param values: the variable arguments of the URL rule - :param _external: if set to ``True``, an absolute URL is generated. Server - address can be changed via ``SERVER_NAME`` configuration variable which - falls back to the `Host` header, then to the IP and port of the request. - :param _scheme: a string specifying the desired URL scheme. The `_external` - parameter must be set to ``True`` or a :exc:`ValueError` is raised. The default - behavior uses the same scheme as the current request, or - :data:`PREFERRED_URL_SCHEME` if no request context is available. - This also can be set to an empty string to build protocol-relative - URLs. - :param _anchor: if provided this is added as anchor to the URL. - :param _method: if provided this explicitly specifies an HTTP method. + .. versionchanged:: 0.9 + Calls ``app.handle_url_build_error`` on build errors. """ - appctx = _app_ctx_stack.top - reqctx = _request_ctx_stack.top - - if appctx is None: - raise RuntimeError( - "Attempted to generate a URL without the application context being" - " pushed. This has to be executed when application context is" - " available." - ) - - # If request specific information is available we have some extra - # features that support "relative" URLs. - if reqctx is not None: - url_adapter = reqctx.url_adapter - blueprint_name = request.blueprint - - if endpoint[:1] == ".": - if blueprint_name is not None: - endpoint = f"{blueprint_name}{endpoint}" - else: - endpoint = endpoint[1:] - - external = values.pop("_external", False) - - # Otherwise go with the url adapter from the appctx and make - # the URLs external by default. - else: - url_adapter = appctx.url_adapter - - if url_adapter is None: - raise RuntimeError( - "Application was not able to create a URL adapter for request" - " independent URL generation. You might be able to fix this by" - " setting the SERVER_NAME config variable." - ) - - external = values.pop("_external", True) - - return current_app.url_for(endpoint, external, url_adapter, **values) + return current_app.url_for( + endpoint, + _anchor=_anchor, + _method=_method, + _scheme=_scheme, + _external=_external, + **values, + ) def redirect( diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 0893893f88..cc2daaf77e 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -119,11 +119,15 @@ def index(): ) def test_url_for_with_scheme_not_external(self, app, req_ctx): - @app.route("/") - def index(): - return "42" + app.add_url_rule("/", endpoint="index") - pytest.raises(ValueError, flask.url_for, "index", _scheme="https") + # Implicit external with scheme. + url = flask.url_for("index", _scheme="https") + assert url == "https://localhost/" + + # Error when external=False with scheme + with pytest.raises(ValueError): + flask.url_for("index", _scheme="https", _external=False) def test_url_for_with_alternating_schemes(self, app, req_ctx): @app.route("/") From 69e2300608497bba103fd05735c878c0d642dd68 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sun, 15 May 2022 08:48:07 -0700 Subject: [PATCH 015/229] use app.url_for as template global avoid extra call from helpers.url_for update changelog for method moves --- CHANGES.rst | 1 + src/flask/app.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3b841e1a38..91439096e5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,7 @@ Unreleased - Add new customization points to the ``Flask`` app object for many previously global behaviors. + - ``flask.url_for`` will call ``app.url_for``. :issue:`4568` - ``flask.abort`` will call ``app.aborter``. ``Flask.aborter_class`` and ``Flask.make_aborter`` can be used to customize this aborter. :issue:`4567` diff --git a/src/flask/app.py b/src/flask/app.py index 4da84e3011..e7a07addd7 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -45,7 +45,6 @@ from .helpers import get_flashed_messages from .helpers import get_load_dotenv from .helpers import locked_cached_property -from .helpers import url_for from .json import jsonify from .logging import create_logger from .scaffold import _endpoint_from_view_func @@ -439,6 +438,7 @@ def __init__( #: to raise HTTP errors, and can be called directly as well. #: #: .. versionadded:: 2.2 + #: Moved from ``flask.abort``, which calls this object. self.aborter = self.make_aborter() #: A list of functions that are called by @@ -727,7 +727,7 @@ def create_jinja_environment(self) -> Environment: rv = self.jinja_environment(self, **options) rv.globals.update( - url_for=url_for, + url_for=self.url_for, get_flashed_messages=get_flashed_messages, config=self.config, # request, session and g are normally added with the @@ -1798,6 +1798,7 @@ def redirect(self, location: str, code: int = 302) -> BaseResponse: :param code: The status code for the redirect. .. versionadded:: 2.2 + Moved from ``flask.redirect``, which calls this method. """ return _wz_redirect(location, code=code, Response=self.response_class) From eb36135cfe6a17350617e47b70b9ad383206eded Mon Sep 17 00:00:00 2001 From: Chris Hallacy Date: Mon, 2 May 2022 11:33:29 -0600 Subject: [PATCH 016/229] always warn on blueprint setupmethod after registration --- CHANGES.rst | 4 ++++ src/flask/blueprints.py | 8 ++++---- src/flask/scaffold.py | 4 +++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 91439096e5..d302347f30 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,6 +16,10 @@ Unreleased - Refactor ``register_error_handler`` to consolidate error checking. Rewrite some error messages to be more consistent. :issue:`4559` +- Use Blueprint decorators and functions intended for setup after + registering the blueprint will show a warning. In the next version, + this will become an error just like the application setup methods. + :issue:`4571` Version 2.1.2 diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index c801d1b4cc..e4a9a5ce80 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -162,7 +162,6 @@ class Blueprint(Scaffold): .. versionadded:: 0.7 """ - warn_on_modifications = False _got_registered_once = False #: Blueprint local JSON encoder class to use. Set to ``None`` to use @@ -209,7 +208,7 @@ def __init__( self._blueprints: t.List[t.Tuple["Blueprint", dict]] = [] def _is_setup_finished(self) -> bool: - return self.warn_on_modifications and self._got_registered_once + return self._got_registered_once def record(self, func: t.Callable) -> None: """Registers a function that is called when the blueprint is @@ -217,14 +216,15 @@ def record(self, func: t.Callable) -> None: state as argument as returned by the :meth:`make_setup_state` method. """ - if self._got_registered_once and self.warn_on_modifications: + if self._got_registered_once: + # TODO: Upgrade this to an error and unify it setupmethod in 2.3 from warnings import warn warn( Warning( "The blueprint was already registered once but is" " getting modified now. These changes will not show" - " up." + " up.\n This warning will be become an exception in 2.3." ) ) self.deferred_functions.append(func) diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 10e77bbcb4..b2dd46f354 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -27,8 +27,8 @@ from .typing import URLValuePreprocessorCallable if t.TYPE_CHECKING: # pragma: no cover - from .wrappers import Response from .typing import ErrorHandlerCallable + from .wrappers import Response # a singleton sentinel value for parameter defaults _sentinel = object() @@ -411,6 +411,7 @@ def patch(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """ return self._method_route("PATCH", rule, options) + @setupmethod def route(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """Decorate a view function to register it with the given URL rule and options. Calls :meth:`add_url_rule`, which has more @@ -510,6 +511,7 @@ def index(): """ raise NotImplementedError + @setupmethod def endpoint(self, endpoint: str) -> t.Callable: """Decorate a view function to register it for the given endpoint. Used if a rule is added without a ``view_func`` with From a406c297aafa28074d11ec6fd27c246c70418cb4 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 23 May 2022 08:49:30 -0700 Subject: [PATCH 017/229] apply setupmethod consistently --- src/flask/app.py | 13 ++++++++++-- src/flask/blueprints.py | 47 +++++++++++++++++++++++++++++------------ src/flask/scaffold.py | 23 +++++++------------- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index e7a07addd7..c29c2e8797 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -541,8 +541,17 @@ def __init__( # the app's commands to another CLI tool. self.cli.name = self.name - def _is_setup_finished(self) -> bool: - return self.debug and self._got_first_request + def _check_setup_finished(self, f_name: str) -> None: + if self._got_first_request: + raise AssertionError( + f"The setup method '{f_name}' can no longer be called" + " on the application. It has already handled its first" + " request, any changes will not be applied" + " consistently.\n" + "Make sure all imports, decorators, functions, etc." + " needed to set up the application are done before" + " running it." + ) @locked_cached_property def name(self) -> str: # type: ignore diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index e4a9a5ce80..a8856fe4eb 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -6,6 +6,7 @@ from .scaffold import _endpoint_from_view_func from .scaffold import _sentinel from .scaffold import Scaffold +from .scaffold import setupmethod from .typing import AfterRequestCallable from .typing import BeforeFirstRequestCallable from .typing import BeforeRequestCallable @@ -207,28 +208,33 @@ def __init__( self.cli_group = cli_group self._blueprints: t.List[t.Tuple["Blueprint", dict]] = [] - def _is_setup_finished(self) -> bool: - return self._got_registered_once + def _check_setup_finished(self, f_name: str) -> None: + if self._got_registered_once: + import warnings + + warnings.warn( + f"The setup method '{f_name}' can no longer be called on" + f" the blueprint '{self.name}'. It has already been" + " registered at least once, any changes will not be" + " applied consistently.\n" + "Make sure all imports, decorators, functions, etc." + " needed to set up the blueprint are done before" + " registering it.\n" + "This warning will become an exception in Flask 2.3.", + UserWarning, + stacklevel=3, + ) + @setupmethod def record(self, func: t.Callable) -> None: """Registers a function that is called when the blueprint is registered on the application. This function is called with the state as argument as returned by the :meth:`make_setup_state` method. """ - if self._got_registered_once: - # TODO: Upgrade this to an error and unify it setupmethod in 2.3 - from warnings import warn - - warn( - Warning( - "The blueprint was already registered once but is" - " getting modified now. These changes will not show" - " up.\n This warning will be become an exception in 2.3." - ) - ) self.deferred_functions.append(func) + @setupmethod def record_once(self, func: t.Callable) -> None: """Works like :meth:`record` but wraps the function in another function that will ensure the function is only called once. If the @@ -251,6 +257,7 @@ def make_setup_state( """ return BlueprintSetupState(self, app, options, first_registration) + @setupmethod def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: """Register a :class:`~flask.Blueprint` on this blueprint. Keyword arguments passed to this method will override the defaults set @@ -390,6 +397,7 @@ def extend(bp_dict, parent_dict): bp_options["name_prefix"] = name blueprint.register(app, bp_options) + @setupmethod def add_url_rule( self, rule: str, @@ -417,6 +425,7 @@ def add_url_rule( ) ) + @setupmethod def app_template_filter( self, name: t.Optional[str] = None ) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]: @@ -433,6 +442,7 @@ def decorator(f: TemplateFilterCallable) -> TemplateFilterCallable: return decorator + @setupmethod def add_app_template_filter( self, f: TemplateFilterCallable, name: t.Optional[str] = None ) -> None: @@ -449,6 +459,7 @@ def register_template(state: BlueprintSetupState) -> None: self.record_once(register_template) + @setupmethod def app_template_test( self, name: t.Optional[str] = None ) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]: @@ -467,6 +478,7 @@ def decorator(f: TemplateTestCallable) -> TemplateTestCallable: return decorator + @setupmethod def add_app_template_test( self, f: TemplateTestCallable, name: t.Optional[str] = None ) -> None: @@ -485,6 +497,7 @@ def register_template(state: BlueprintSetupState) -> None: self.record_once(register_template) + @setupmethod def app_template_global( self, name: t.Optional[str] = None ) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]: @@ -503,6 +516,7 @@ def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable: return decorator + @setupmethod def add_app_template_global( self, f: TemplateGlobalCallable, name: t.Optional[str] = None ) -> None: @@ -521,6 +535,7 @@ def register_template(state: BlueprintSetupState) -> None: self.record_once(register_template) + @setupmethod def before_app_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable: """Like :meth:`Flask.before_request`. Such a function is executed before each request, even if outside of a blueprint. @@ -530,6 +545,7 @@ def before_app_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable: ) return f + @setupmethod def before_app_first_request( self, f: BeforeFirstRequestCallable ) -> BeforeFirstRequestCallable: @@ -548,6 +564,7 @@ def after_app_request(self, f: AfterRequestCallable) -> AfterRequestCallable: ) return f + @setupmethod def teardown_app_request(self, f: TeardownCallable) -> TeardownCallable: """Like :meth:`Flask.teardown_request` but for a blueprint. Such a function is executed when tearing down each request, even if outside of @@ -558,6 +575,7 @@ def teardown_app_request(self, f: TeardownCallable) -> TeardownCallable: ) return f + @setupmethod def app_context_processor( self, f: TemplateContextProcessorCallable ) -> TemplateContextProcessorCallable: @@ -569,6 +587,7 @@ def app_context_processor( ) return f + @setupmethod def app_errorhandler(self, code: t.Union[t.Type[Exception], int]) -> t.Callable: """Like :meth:`Flask.errorhandler` but for a blueprint. This handler is used for all requests, even if outside of the blueprint. @@ -580,6 +599,7 @@ def decorator(f: "ErrorHandlerCallable") -> "ErrorHandlerCallable": return decorator + @setupmethod def app_url_value_preprocessor( self, f: URLValuePreprocessorCallable ) -> URLValuePreprocessorCallable: @@ -589,6 +609,7 @@ def app_url_value_preprocessor( ) return f + @setupmethod def app_url_defaults(self, f: URLDefaultCallable) -> URLDefaultCallable: """Same as :meth:`url_defaults` but application wide.""" self.record_once( diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index b2dd46f354..154426ca63 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -37,22 +37,10 @@ def setupmethod(f: F) -> F: - """Wraps a method so that it performs a check in debug mode if the - first request was already handled. - """ + f_name = f.__name__ def wrapper_func(self, *args: t.Any, **kwargs: t.Any) -> t.Any: - if self._is_setup_finished(): - raise AssertionError( - "A setup function was called after the first request " - "was handled. This usually indicates a bug in the" - " application where a module was not imported and" - " decorators or other functionality was called too" - " late.\nTo fix this make sure to import all your view" - " modules, database models, and everything related at a" - " central place before the application starts serving" - " requests." - ) + self._check_setup_finished(f_name) return f(self, *args, **kwargs) return t.cast(F, update_wrapper(wrapper_func, f)) @@ -239,7 +227,7 @@ def __init__( def __repr__(self) -> str: return f"<{type(self).__name__} {self.name!r}>" - def _is_setup_finished(self) -> bool: + def _check_setup_finished(self, f_name: str) -> None: raise NotImplementedError @property @@ -376,6 +364,7 @@ def _method_route( return self.route(rule, methods=[method], **options) + @setupmethod def get(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """Shortcut for :meth:`route` with ``methods=["GET"]``. @@ -383,6 +372,7 @@ def get(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """ return self._method_route("GET", rule, options) + @setupmethod def post(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """Shortcut for :meth:`route` with ``methods=["POST"]``. @@ -390,6 +380,7 @@ def post(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """ return self._method_route("POST", rule, options) + @setupmethod def put(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """Shortcut for :meth:`route` with ``methods=["PUT"]``. @@ -397,6 +388,7 @@ def put(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """ return self._method_route("PUT", rule, options) + @setupmethod def delete(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """Shortcut for :meth:`route` with ``methods=["DELETE"]``. @@ -404,6 +396,7 @@ def delete(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """ return self._method_route("DELETE", rule, options) + @setupmethod def patch(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: """Shortcut for :meth:`route` with ``methods=["PATCH"]``. From e044b00047da9bf94e7eb88049a17fe1dcf78f4e Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 23 May 2022 08:50:13 -0700 Subject: [PATCH 018/229] avoid triggering setupmethod late in tests --- tests/test_basic.py | 94 ++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 52 deletions(-) diff --git a/tests/test_basic.py b/tests/test_basic.py index c4be662134..fa9f902df2 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -329,6 +329,11 @@ def index(): flask.session["testing"] = 42 return "Hello World" + @app.route("/clear") + def clear(): + flask.session.pop("testing", None) + return "Goodbye World" + rv = client.get("/", "http://www.example.com:8080/test/") cookie = rv.headers["set-cookie"].lower() assert "domain=.example.com" in cookie @@ -337,11 +342,6 @@ def index(): assert "httponly" not in cookie assert "samesite" in cookie - @app.route("/clear") - def clear(): - flask.session.pop("testing", None) - return "Goodbye World" - rv = client.get("/clear", "http://www.example.com:8080/test/") cookie = rv.headers["set-cookie"].lower() assert "session=;" in cookie @@ -1031,7 +1031,14 @@ def raise_e3(): assert rv.data == b"E2" -def test_trapping_of_bad_request_key_errors(app, client): +@pytest.mark.parametrize( + ("debug", "trap", "expect_key", "expect_abort"), + [(False, None, True, True), (True, None, False, True), (False, True, False, False)], +) +def test_trap_bad_request_key_error(app, client, debug, trap, expect_key, expect_abort): + app.config["DEBUG"] = debug + app.config["TRAP_BAD_REQUEST_ERRORS"] = trap + @app.route("/key") def fail(): flask.request.form["missing_key"] @@ -1040,26 +1047,23 @@ def fail(): def allow_abort(): flask.abort(400) - rv = client.get("/key") - assert rv.status_code == 400 - assert b"missing_key" not in rv.data - rv = client.get("/abort") - assert rv.status_code == 400 + if expect_key: + rv = client.get("/key") + assert rv.status_code == 400 + assert b"missing_key" not in rv.data + else: + with pytest.raises(KeyError) as exc_info: + client.get("/key") - app.debug = True - with pytest.raises(KeyError) as e: - client.get("/key") - assert e.errisinstance(BadRequest) - assert "missing_key" in e.value.get_description() - rv = client.get("/abort") - assert rv.status_code == 400 + assert exc_info.errisinstance(BadRequest) + assert "missing_key" in exc_info.value.get_description() - app.debug = False - app.config["TRAP_BAD_REQUEST_ERRORS"] = True - with pytest.raises(KeyError): - client.get("/key") - with pytest.raises(BadRequest): - client.get("/abort") + if expect_abort: + rv = client.get("/abort") + assert rv.status_code == 400 + else: + with pytest.raises(BadRequest): + client.get("/abort") def test_trapping_of_all_http_exceptions(app, client): @@ -1661,7 +1665,7 @@ def index(): assert rv.data == b"Hello World!" -def test_debug_mode_complains_after_first_request(app, client): +def test_no_setup_after_first_request(app, client): app.debug = True @app.route("/") @@ -1671,19 +1675,10 @@ def index(): assert not app.got_first_request assert client.get("/").data == b"Awesome" - with pytest.raises(AssertionError) as e: + with pytest.raises(AssertionError) as exc_info: app.add_url_rule("/foo", endpoint="late") - assert "A setup function was called" in str(e.value) - - app.debug = False - - @app.route("/foo") - def working(): - return "Meh" - - assert client.get("/foo").data == b"Meh" - assert app.got_first_request + assert "setup method 'add_url_rule'" in str(exc_info.value) def test_before_first_request_functions(app, client): @@ -1720,28 +1715,23 @@ def get_and_assert(): def test_routing_redirect_debugging(monkeypatch, app, client): - @app.route("/foo/", methods=["GET", "POST"]) - def foo(): - return "success" + app.config["DEBUG"] = True - app.debug = False - rv = client.post("/foo", data={}, follow_redirects=True) - assert rv.data == b"success" + @app.route("/user/", methods=["GET", "POST"]) + def user(): + return flask.request.form["status"] - app.debug = True - - with client: - rv = client.post("/foo", data={}, follow_redirects=True) - assert rv.data == b"success" - rv = client.get("/foo", data={}, follow_redirects=True) - assert rv.data == b"success" + # default redirect code preserves form data + rv = client.post("/user", data={"status": "success"}, follow_redirects=True) + assert rv.data == b"success" + # 301 and 302 raise error monkeypatch.setattr(RequestRedirect, "code", 301) - with client, pytest.raises(AssertionError) as e: - client.post("/foo", data={}) + with client, pytest.raises(AssertionError) as exc_info: + client.post("/user", data={"status": "error"}, follow_redirects=True) - assert "canonical URL 'http://localhost/foo/'" in str(e.value) + assert "canonical URL 'http://localhost/user/'" in str(exc_info.value) def test_route_decorator_custom_endpoint(app, client): From 1232d698600e11dcb83bb5dc349ca785eae02d2f Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 23 May 2022 09:46:20 -0700 Subject: [PATCH 019/229] inline conditional imports for cli behaviors --- setup.cfg | 3 +++ src/flask/cli.py | 53 ++++++++++++++++++++--------------------------- tests/test_cli.py | 19 +++++++++++++---- 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/setup.cfg b/setup.cfg index 31a590a4e0..597eece14f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -116,3 +116,6 @@ ignore_missing_imports = True [mypy-cryptography.*] ignore_missing_imports = True + +[mypy-importlib_metadata] +ignore_missing_imports = True diff --git a/src/flask/cli.py b/src/flask/cli.py index efcc0f99bb..77c1e25a9c 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -9,8 +9,6 @@ from operator import attrgetter from threading import Lock from threading import Thread -from typing import Any -from typing import TYPE_CHECKING import click from werkzeug.utils import import_string @@ -20,31 +18,6 @@ from .helpers import get_env from .helpers import get_load_dotenv -try: - import dotenv -except ImportError: - dotenv = None - -try: - import ssl -except ImportError: - ssl = None # type: ignore - -if sys.version_info >= (3, 10): - from importlib import metadata -else: - # Use a backport on Python < 3.10. - # - # We technically have importlib.metadata on 3.8+, - # but the API changed in 3.10, so use the backport - # for consistency. - if TYPE_CHECKING: - metadata: Any - else: - # we do this to avoid a version dependent mypy error - # because importlib_metadata is not installed in python3.10+ - import importlib_metadata as metadata - class NoAppException(click.UsageError): """Raised if an application cannot be found or loaded.""" @@ -520,6 +493,14 @@ def _load_plugin_commands(self): if self._loaded_plugin_commands: return + if sys.version_info >= (3, 10): + from importlib import metadata + else: + # Use a backport on Python < 3.10. We technically have + # importlib.metadata on 3.8+, but the API changed in 3.10, + # so use the backport for consistency. + import importlib_metadata as metadata + for ep in metadata.entry_points(group="flask.commands"): self.add_command(ep.load(), ep.name) @@ -615,7 +596,9 @@ def load_dotenv(path=None): .. versionadded:: 1.0 """ - if dotenv is None: + try: + import dotenv + except ImportError: if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"): click.secho( " * Tip: There are .env or .flaskenv files present." @@ -691,12 +674,14 @@ def __init__(self): self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) def convert(self, value, param, ctx): - if ssl is None: + try: + import ssl + except ImportError: raise click.BadParameter( 'Using "--cert" requires Python to be compiled with SSL support.', ctx, param, - ) + ) from None try: return self.path_type(value, param, ctx) @@ -729,7 +714,13 @@ def _validate_key(ctx, param, value): """ cert = ctx.params.get("cert") is_adhoc = cert == "adhoc" - is_context = ssl and isinstance(cert, ssl.SSLContext) + + try: + import ssl + except ImportError: + is_context = False + else: + is_context = isinstance(cert, ssl.SSLContext) if value is not None: if is_adhoc: diff --git a/tests/test_cli.py b/tests/test_cli.py index 6e8b57f05a..c9dd5ade06 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -18,7 +18,6 @@ from flask import Flask from flask.cli import AppGroup from flask.cli import DispatchingApp -from flask.cli import dotenv from flask.cli import find_best_app from flask.cli import FlaskGroup from flask.cli import get_version @@ -492,7 +491,18 @@ def test_no_routes(self, invoke_no_routes): assert "No routes were registered." in result.output -need_dotenv = pytest.mark.skipif(dotenv is None, reason="dotenv is not installed") +def dotenv_not_available(): + try: + import dotenv # noqa: F401 + except ImportError: + return True + + return False + + +need_dotenv = pytest.mark.skipif( + dotenv_not_available(), reason="dotenv is not installed" +) @need_dotenv @@ -530,7 +540,7 @@ def test_dotenv_path(monkeypatch): def test_dotenv_optional(monkeypatch): - monkeypatch.setattr("flask.cli.dotenv", None) + monkeypatch.setitem(sys.modules, "dotenv", None) monkeypatch.chdir(test_path) load_dotenv() assert "FOO" not in os.environ @@ -602,7 +612,8 @@ def test_run_cert_import(monkeypatch): def test_run_cert_no_ssl(monkeypatch): - monkeypatch.setattr("flask.cli.ssl", None) + monkeypatch.setitem(sys.modules, "ssl", None) + with pytest.raises(click.BadParameter): run_command.make_context("run", ["--cert", "not_here"]) From d506af1b1f27877f2101a20c6826e99f30427348 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 23 May 2022 10:08:43 -0700 Subject: [PATCH 020/229] update requirements --- .pre-commit-config.yaml | 2 +- requirements/dev.txt | 10 +++++----- requirements/docs.txt | 6 +++--- requirements/tests.txt | 4 ++-- requirements/typing.txt | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa395c5780..e5a89e660c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ ci: autoupdate_schedule: monthly repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.32.0 + rev: v2.32.1 hooks: - id: pyupgrade args: ["--py36-plus"] diff --git a/requirements/dev.txt b/requirements/dev.txt index 3699907483..2cdefc9687 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -10,19 +10,19 @@ -r typing.txt cfgv==3.3.1 # via pre-commit -click==8.1.2 +click==8.1.3 # via # pip-compile-multi # pip-tools distlib==0.3.4 # via virtualenv -filelock==3.6.0 +filelock==3.7.0 # via # tox # virtualenv greenlet==1.1.2 ; python_version < "3.11" # via -r requirements/tests.in -identify==2.5.0 +identify==2.5.1 # via pre-commit nodeenv==1.6.0 # via pre-commit @@ -30,11 +30,11 @@ pep517==0.12.0 # via pip-tools pip-compile-multi==2.4.5 # via -r requirements/dev.in -pip-tools==6.6.0 +pip-tools==6.6.1 # via pip-compile-multi platformdirs==2.5.2 # via virtualenv -pre-commit==2.18.1 +pre-commit==2.19.0 # via -r requirements/dev.in pyyaml==6.0 # via pre-commit diff --git a/requirements/docs.txt b/requirements/docs.txt index 963d81e92e..f94d30b8c5 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -9,7 +9,7 @@ alabaster==0.7.12 # via sphinx babel==2.10.1 # via sphinx -certifi==2021.10.8 +certifi==2022.5.18.1 # via requests charset-normalizer==2.0.12 # via requests @@ -21,7 +21,7 @@ idna==3.3 # via requests imagesize==1.3.0 # via sphinx -jinja2==3.1.1 +jinja2==3.1.2 # via sphinx markupsafe==2.1.1 # via jinja2 @@ -35,7 +35,7 @@ pygments==2.12.0 # via # sphinx # sphinx-tabs -pyparsing==3.0.8 +pyparsing==3.0.9 # via packaging pytz==2022.1 # via babel diff --git a/requirements/tests.txt b/requirements/tests.txt index 7833cabd21..7a3f89d7f5 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -5,7 +5,7 @@ # # pip-compile-multi # -asgiref==3.5.0 +asgiref==3.5.2 # via -r requirements/tests.in attrs==21.4.0 # via pytest @@ -21,7 +21,7 @@ pluggy==1.0.0 # via pytest py==1.11.0 # via pytest -pyparsing==3.0.8 +pyparsing==3.0.9 # via packaging pytest==7.1.2 # via -r requirements/tests.in diff --git a/requirements/typing.txt b/requirements/typing.txt index f1aa528402..bf5e2a39ac 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -7,7 +7,7 @@ # cffi==1.15.0 # via cryptography -cryptography==37.0.1 +cryptography==37.0.2 # via -r requirements/typing.in mypy==0.950 # via -r requirements/typing.in @@ -21,7 +21,7 @@ types-contextvars==2.4.5 # via -r requirements/typing.in types-dataclasses==0.6.5 # via -r requirements/typing.in -types-setuptools==57.4.14 +types-setuptools==57.4.15 # via -r requirements/typing.in typing-extensions==4.2.0 # via mypy From 9252be9c9ebe46dd6415b24999a869ed87eace57 Mon Sep 17 00:00:00 2001 From: lecovi Date: Mon, 2 May 2022 17:28:23 -0300 Subject: [PATCH 021/229] docs: new configuration format for celery --- docs/patterns/celery.rst | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/patterns/celery.rst b/docs/patterns/celery.rst index 38a9a02523..228a04a8aa 100644 --- a/docs/patterns/celery.rst +++ b/docs/patterns/celery.rst @@ -34,17 +34,15 @@ Celery without any reconfiguration with Flask, it becomes a bit nicer by subclassing tasks and adding support for Flask's application contexts and hooking it up with the Flask configuration. -This is all that is necessary to properly integrate Celery with Flask:: +This is all that is necessary to integrate Celery with Flask: + +.. code-block:: python from celery import Celery def make_celery(app): - celery = Celery( - app.import_name, - backend=app.config['CELERY_RESULT_BACKEND'], - broker=app.config['CELERY_BROKER_URL'] - ) - celery.conf.update(app.config) + celery = Celery(app.import_name) + celery.conf.update(app.config["CELERY_CONFIG"]) class ContextTask(celery.Task): def __call__(self, *args, **kwargs): @@ -59,6 +57,12 @@ from the application config, updates the rest of the Celery config from the Flask config and then creates a subclass of the task that wraps the task execution in an application context. +.. note:: + Celery 5.x deprecated uppercase configuration keys, and 6.x will + remove them. See their official `migration guide`_. + +.. _migration guide: https://docs.celeryproject.org/en/stable/userguide/configuration.html#conf-old-settings-map. + An example task --------------- @@ -69,10 +73,10 @@ application using the factory from above, and then use it to define the task. :: from flask import Flask flask_app = Flask(__name__) - flask_app.config.update( - CELERY_BROKER_URL='redis://localhost:6379', - CELERY_RESULT_BACKEND='redis://localhost:6379' - ) + flask_app.config.update(CELERY_CONFIG={ + 'broker_url': 'redis://localhost:6379', + 'result_backend': 'redis://localhost:6379', + }) celery = make_celery(flask_app) @celery.task() From c45c81938a1715eb4bdf9eaf6ac9d75becb8c191 Mon Sep 17 00:00:00 2001 From: Nathan Brown Date: Thu, 12 May 2022 19:57:50 -0400 Subject: [PATCH 022/229] Add link to additional packaging info --- docs/tutorial/install.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/install.rst b/docs/tutorial/install.rst index 3d7d7c602b..9e808f1461 100644 --- a/docs/tutorial/install.rst +++ b/docs/tutorial/install.rst @@ -67,10 +67,12 @@ This tells Python to copy everything in the ``static`` and ``templates`` directories, and the ``schema.sql`` file, but to exclude all bytecode files. -See the `official packaging guide`_ for another explanation of the files +See the official `Packaging tutorial `_ and +`detailed guide `_ for more explanation of the files and options used. -.. _official packaging guide: https://packaging.python.org/tutorials/packaging-projects/ +.. _packaging tutorial: https://packaging.python.org/tutorials/packaging-projects/ +.. _packaging guide: https://packaging.python.org/guides/distributing-packages-using-setuptools/ Install the Project From a4f63e0390e4f9aba543514d16479ce6f40e46be Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 23 May 2022 10:54:02 -0700 Subject: [PATCH 023/229] start version 2.1.3 --- CHANGES.rst | 9 +++++++++ src/flask/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0e4bebfacd..12d898701d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,14 @@ .. currentmodule:: flask +Version 2.1.3 +------------- + +Unreleased + +- Inline some optional imports that are only used for certain CLI + commands. :pr:`4606` + + Version 2.1.2 ------------- diff --git a/src/flask/__init__.py b/src/flask/__init__.py index f684f57a6a..380de0f450 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -42,4 +42,4 @@ from .templating import render_template as render_template from .templating import render_template_string as render_template_string -__version__ = "2.1.2" +__version__ = "2.1.3.dev0" From 8cb950671f39c64707b6aaebfc60d7a8f1157da5 Mon Sep 17 00:00:00 2001 From: Justin Bull Date: Thu, 19 May 2022 13:34:46 -0400 Subject: [PATCH 024/229] use bound typevar to accept Flask and Werkzeug Response classes --- CHANGES.rst | 1 + src/flask/typing.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 12d898701d..a181badcab 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,7 @@ Unreleased - Inline some optional imports that are only used for certain CLI commands. :pr:`4606` +- Relax type annotation for ``after_request`` functions. :issue:`4600` Version 2.1.2 diff --git a/src/flask/typing.py b/src/flask/typing.py index a839a7e48b..ec7c7969bf 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -4,7 +4,7 @@ if t.TYPE_CHECKING: from _typeshed.wsgi import WSGIApplication # noqa: F401 from werkzeug.datastructures import Headers # noqa: F401 - from werkzeug.wrappers.response import Response # noqa: F401 + from werkzeug.wrappers import Response # noqa: F401 # The possible types that are directly convertible or are a Response object. ResponseValue = t.Union[ @@ -35,8 +35,13 @@ "WSGIApplication", ] +# Allow any subclass of werkzeug.Response, such as the one from Flask, +# as a callback argument. Using werkzeug.Response directly makes a +# callback annotated with flask.Response fail type checking. +ResponseClass = t.TypeVar("ResponseClass", bound="Response") + AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named -AfterRequestCallable = t.Callable[["Response"], "Response"] +AfterRequestCallable = t.Callable[[ResponseClass], ResponseClass] BeforeFirstRequestCallable = t.Callable[[], None] BeforeRequestCallable = t.Callable[[], t.Optional[ResponseReturnValue]] TeardownCallable = t.Callable[[t.Optional[BaseException]], None] From 61f62e6005f72c44a90026e2589b6bc5234f2f24 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 23 May 2022 13:15:26 -0700 Subject: [PATCH 025/229] access flask types through namespace alias --- src/flask/app.py | 56 +++++++++++++++++++---------------------- src/flask/blueprints.py | 54 +++++++++++++++++---------------------- src/flask/ctx.py | 6 ++--- src/flask/scaffold.py | 49 ++++++++++++++++-------------------- src/flask/views.py | 8 +++--- 5 files changed, 77 insertions(+), 96 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 348bc7f74f..34ea5b294f 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -26,6 +26,7 @@ from . import cli from . import json +from . import typing as ft from .config import Config from .config import ConfigAttribute from .ctx import _AppCtxGlobals @@ -58,12 +59,6 @@ from .signals import request_tearing_down from .templating import DispatchingJinjaLoader from .templating import Environment -from .typing import BeforeFirstRequestCallable -from .typing import ResponseReturnValue -from .typing import TeardownCallable -from .typing import TemplateFilterCallable -from .typing import TemplateGlobalCallable -from .typing import TemplateTestCallable from .wrappers import Request from .wrappers import Response @@ -72,7 +67,6 @@ from .blueprints import Blueprint from .testing import FlaskClient from .testing import FlaskCliRunner - from .typing import ErrorHandlerCallable if sys.version_info >= (3, 8): iscoroutinefunction = inspect.iscoroutinefunction @@ -436,7 +430,7 @@ def __init__( #: :meth:`before_first_request` decorator. #: #: .. versionadded:: 0.8 - self.before_first_request_funcs: t.List[BeforeFirstRequestCallable] = [] + self.before_first_request_funcs: t.List[ft.BeforeFirstRequestCallable] = [] #: A list of functions that are called when the application context #: is destroyed. Since the application context is also torn down @@ -444,7 +438,7 @@ def __init__( #: from databases. #: #: .. versionadded:: 0.9 - self.teardown_appcontext_funcs: t.List[TeardownCallable] = [] + self.teardown_appcontext_funcs: t.List[ft.TeardownCallable] = [] #: A list of shell context processor functions that should be run #: when a shell context is created. @@ -1096,7 +1090,7 @@ def add_url_rule( @setupmethod def template_filter( self, name: t.Optional[str] = None - ) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]: + ) -> t.Callable[[ft.TemplateFilterCallable], ft.TemplateFilterCallable]: """A decorator that is used to register custom template filter. You can specify a name for the filter, otherwise the function name will be used. Example:: @@ -1109,7 +1103,7 @@ def reverse(s): function name will be used. """ - def decorator(f: TemplateFilterCallable) -> TemplateFilterCallable: + def decorator(f: ft.TemplateFilterCallable) -> ft.TemplateFilterCallable: self.add_template_filter(f, name=name) return f @@ -1117,7 +1111,7 @@ def decorator(f: TemplateFilterCallable) -> TemplateFilterCallable: @setupmethod def add_template_filter( - self, f: TemplateFilterCallable, name: t.Optional[str] = None + self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None ) -> None: """Register a custom template filter. Works exactly like the :meth:`template_filter` decorator. @@ -1130,7 +1124,7 @@ def add_template_filter( @setupmethod def template_test( self, name: t.Optional[str] = None - ) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]: + ) -> t.Callable[[ft.TemplateTestCallable], ft.TemplateTestCallable]: """A decorator that is used to register custom template test. You can specify a name for the test, otherwise the function name will be used. Example:: @@ -1150,7 +1144,7 @@ def is_prime(n): function name will be used. """ - def decorator(f: TemplateTestCallable) -> TemplateTestCallable: + def decorator(f: ft.TemplateTestCallable) -> ft.TemplateTestCallable: self.add_template_test(f, name=name) return f @@ -1158,7 +1152,7 @@ def decorator(f: TemplateTestCallable) -> TemplateTestCallable: @setupmethod def add_template_test( - self, f: TemplateTestCallable, name: t.Optional[str] = None + self, f: ft.TemplateTestCallable, name: t.Optional[str] = None ) -> None: """Register a custom template test. Works exactly like the :meth:`template_test` decorator. @@ -1173,7 +1167,7 @@ def add_template_test( @setupmethod def template_global( self, name: t.Optional[str] = None - ) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]: + ) -> t.Callable[[ft.TemplateGlobalCallable], ft.TemplateGlobalCallable]: """A decorator that is used to register a custom template global function. You can specify a name for the global function, otherwise the function name will be used. Example:: @@ -1188,7 +1182,7 @@ def double(n): function name will be used. """ - def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable: + def decorator(f: ft.TemplateGlobalCallable) -> ft.TemplateGlobalCallable: self.add_template_global(f, name=name) return f @@ -1196,7 +1190,7 @@ def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable: @setupmethod def add_template_global( - self, f: TemplateGlobalCallable, name: t.Optional[str] = None + self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None ) -> None: """Register a custom template global function. Works exactly like the :meth:`template_global` decorator. @@ -1210,8 +1204,8 @@ def add_template_global( @setupmethod def before_first_request( - self, f: BeforeFirstRequestCallable - ) -> BeforeFirstRequestCallable: + self, f: ft.BeforeFirstRequestCallable + ) -> ft.BeforeFirstRequestCallable: """Registers a function to be run before the first request to this instance of the application. @@ -1224,7 +1218,7 @@ def before_first_request( return f @setupmethod - def teardown_appcontext(self, f: TeardownCallable) -> TeardownCallable: + def teardown_appcontext(self, f: ft.TeardownCallable) -> ft.TeardownCallable: """Registers a function to be called when the application context ends. These functions are typically also called when the request context is popped. @@ -1265,7 +1259,7 @@ def shell_context_processor(self, f: t.Callable) -> t.Callable: self.shell_context_processors.append(f) return f - def _find_error_handler(self, e: Exception) -> t.Optional["ErrorHandlerCallable"]: + def _find_error_handler(self, e: Exception) -> t.Optional[ft.ErrorHandlerCallable]: """Return a registered error handler for an exception in this order: blueprint handler for a specific code, app handler for a specific code, blueprint handler for an exception class, app handler for an exception @@ -1290,7 +1284,7 @@ def _find_error_handler(self, e: Exception) -> t.Optional["ErrorHandlerCallable" def handle_http_exception( self, e: HTTPException - ) -> t.Union[HTTPException, ResponseReturnValue]: + ) -> t.Union[HTTPException, ft.ResponseReturnValue]: """Handles an HTTP exception. By default this will invoke the registered error handlers and fall back to returning the exception as response. @@ -1360,7 +1354,7 @@ def trap_http_exception(self, e: Exception) -> bool: def handle_user_exception( self, e: Exception - ) -> t.Union[HTTPException, ResponseReturnValue]: + ) -> t.Union[HTTPException, ft.ResponseReturnValue]: """This method is called whenever an exception occurs that should be handled. A special case is :class:`~werkzeug .exceptions.HTTPException` which is forwarded to the @@ -1430,7 +1424,7 @@ def handle_exception(self, e: Exception) -> Response: raise e self.log_exception(exc_info) - server_error: t.Union[InternalServerError, ResponseReturnValue] + server_error: t.Union[InternalServerError, ft.ResponseReturnValue] server_error = InternalServerError(original_exception=e) handler = self._find_error_handler(server_error) @@ -1484,7 +1478,7 @@ def raise_routing_exception(self, request: Request) -> "te.NoReturn": raise FormDataRoutingRedirect(request) - def dispatch_request(self) -> ResponseReturnValue: + def dispatch_request(self) -> ft.ResponseReturnValue: """Does the request dispatching. Matches the URL and returns the return value of the view or error handler. This does not have to be a response object. In order to convert the return value to a @@ -1527,7 +1521,7 @@ def full_dispatch_request(self) -> Response: def finalize_request( self, - rv: t.Union[ResponseReturnValue, HTTPException], + rv: t.Union[ft.ResponseReturnValue, HTTPException], from_error_handler: bool = False, ) -> Response: """Given the return value from a view function this finalizes @@ -1630,7 +1624,7 @@ def async_to_sync( return asgiref_async_to_sync(func) - def make_response(self, rv: ResponseReturnValue) -> Response: + def make_response(self, rv: ft.ResponseReturnValue) -> Response: """Convert the return value from a view function to an instance of :attr:`response_class`. @@ -1722,7 +1716,9 @@ def make_response(self, rv: ResponseReturnValue) -> Response: # evaluate a WSGI callable, or coerce a different response # class to the correct type try: - rv = self.response_class.force_type(rv, request.environ) # type: ignore # noqa: B950 + rv = self.response_class.force_type( + rv, request.environ # type: ignore[arg-type] + ) except TypeError as e: raise TypeError( f"{e}\nThe view function did not return a valid" @@ -1838,7 +1834,7 @@ def handle_url_build_error( raise error - def preprocess_request(self) -> t.Optional[ResponseReturnValue]: + def preprocess_request(self) -> t.Optional[ft.ResponseReturnValue]: """Called before the request is dispatched. Calls :attr:`url_value_preprocessors` registered with the app and the current blueprint (if any). Then calls :attr:`before_request_funcs` diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 5d3b4e224c..c60183fa27 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -3,23 +3,13 @@ from collections import defaultdict from functools import update_wrapper +from . import typing as ft from .scaffold import _endpoint_from_view_func from .scaffold import _sentinel from .scaffold import Scaffold -from .typing import AfterRequestCallable -from .typing import BeforeFirstRequestCallable -from .typing import BeforeRequestCallable -from .typing import TeardownCallable -from .typing import TemplateContextProcessorCallable -from .typing import TemplateFilterCallable -from .typing import TemplateGlobalCallable -from .typing import TemplateTestCallable -from .typing import URLDefaultCallable -from .typing import URLValuePreprocessorCallable if t.TYPE_CHECKING: from .app import Flask - from .typing import ErrorHandlerCallable DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable] @@ -419,7 +409,7 @@ def add_url_rule( def app_template_filter( self, name: t.Optional[str] = None - ) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]: + ) -> t.Callable[[ft.TemplateFilterCallable], ft.TemplateFilterCallable]: """Register a custom template filter, available application wide. Like :meth:`Flask.template_filter` but for a blueprint. @@ -427,14 +417,14 @@ def app_template_filter( function name will be used. """ - def decorator(f: TemplateFilterCallable) -> TemplateFilterCallable: + def decorator(f: ft.TemplateFilterCallable) -> ft.TemplateFilterCallable: self.add_app_template_filter(f, name=name) return f return decorator def add_app_template_filter( - self, f: TemplateFilterCallable, name: t.Optional[str] = None + self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None ) -> None: """Register a custom template filter, available application wide. Like :meth:`Flask.add_template_filter` but for a blueprint. Works exactly @@ -451,7 +441,7 @@ def register_template(state: BlueprintSetupState) -> None: def app_template_test( self, name: t.Optional[str] = None - ) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]: + ) -> t.Callable[[ft.TemplateTestCallable], ft.TemplateTestCallable]: """Register a custom template test, available application wide. Like :meth:`Flask.template_test` but for a blueprint. @@ -461,14 +451,14 @@ def app_template_test( function name will be used. """ - def decorator(f: TemplateTestCallable) -> TemplateTestCallable: + def decorator(f: ft.TemplateTestCallable) -> ft.TemplateTestCallable: self.add_app_template_test(f, name=name) return f return decorator def add_app_template_test( - self, f: TemplateTestCallable, name: t.Optional[str] = None + self, f: ft.TemplateTestCallable, name: t.Optional[str] = None ) -> None: """Register a custom template test, available application wide. Like :meth:`Flask.add_template_test` but for a blueprint. Works exactly @@ -487,7 +477,7 @@ def register_template(state: BlueprintSetupState) -> None: def app_template_global( self, name: t.Optional[str] = None - ) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]: + ) -> t.Callable[[ft.TemplateGlobalCallable], ft.TemplateGlobalCallable]: """Register a custom template global, available application wide. Like :meth:`Flask.template_global` but for a blueprint. @@ -497,14 +487,14 @@ def app_template_global( function name will be used. """ - def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable: + def decorator(f: ft.TemplateGlobalCallable) -> ft.TemplateGlobalCallable: self.add_app_template_global(f, name=name) return f return decorator def add_app_template_global( - self, f: TemplateGlobalCallable, name: t.Optional[str] = None + self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None ) -> None: """Register a custom template global, available application wide. Like :meth:`Flask.add_template_global` but for a blueprint. Works exactly @@ -521,7 +511,9 @@ def register_template(state: BlueprintSetupState) -> None: self.record_once(register_template) - def before_app_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable: + def before_app_request( + self, f: ft.BeforeRequestCallable + ) -> ft.BeforeRequestCallable: """Like :meth:`Flask.before_request`. Such a function is executed before each request, even if outside of a blueprint. """ @@ -531,15 +523,15 @@ def before_app_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable: return f def before_app_first_request( - self, f: BeforeFirstRequestCallable - ) -> BeforeFirstRequestCallable: + self, f: ft.BeforeFirstRequestCallable + ) -> ft.BeforeFirstRequestCallable: """Like :meth:`Flask.before_first_request`. Such a function is executed before the first request to the application. """ self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) return f - def after_app_request(self, f: AfterRequestCallable) -> AfterRequestCallable: + def after_app_request(self, f: ft.AfterRequestCallable) -> ft.AfterRequestCallable: """Like :meth:`Flask.after_request` but for a blueprint. Such a function is executed after each request, even if outside of the blueprint. """ @@ -548,7 +540,7 @@ def after_app_request(self, f: AfterRequestCallable) -> AfterRequestCallable: ) return f - def teardown_app_request(self, f: TeardownCallable) -> TeardownCallable: + def teardown_app_request(self, f: ft.TeardownCallable) -> ft.TeardownCallable: """Like :meth:`Flask.teardown_request` but for a blueprint. Such a function is executed when tearing down each request, even if outside of the blueprint. @@ -559,8 +551,8 @@ def teardown_app_request(self, f: TeardownCallable) -> TeardownCallable: return f def app_context_processor( - self, f: TemplateContextProcessorCallable - ) -> TemplateContextProcessorCallable: + self, f: ft.TemplateContextProcessorCallable + ) -> ft.TemplateContextProcessorCallable: """Like :meth:`Flask.context_processor` but for a blueprint. Such a function is executed each request, even if outside of the blueprint. """ @@ -574,22 +566,22 @@ def app_errorhandler(self, code: t.Union[t.Type[Exception], int]) -> t.Callable: handler is used for all requests, even if outside of the blueprint. """ - def decorator(f: "ErrorHandlerCallable") -> "ErrorHandlerCallable": + def decorator(f: ft.ErrorHandlerCallable) -> ft.ErrorHandlerCallable: self.record_once(lambda s: s.app.errorhandler(code)(f)) return f return decorator def app_url_value_preprocessor( - self, f: URLValuePreprocessorCallable - ) -> URLValuePreprocessorCallable: + self, f: ft.URLValuePreprocessorCallable + ) -> ft.URLValuePreprocessorCallable: """Same as :meth:`url_value_preprocessor` but application wide.""" self.record_once( lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) ) return f - def app_url_defaults(self, f: URLDefaultCallable) -> URLDefaultCallable: + def app_url_defaults(self, f: ft.URLDefaultCallable) -> ft.URLDefaultCallable: """Same as :meth:`url_defaults` but application wide.""" self.record_once( lambda s: s.app.url_default_functions.setdefault(None, []).append(f) diff --git a/src/flask/ctx.py b/src/flask/ctx.py index 3ed8fd3a80..e6822b2d97 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -5,11 +5,11 @@ from werkzeug.exceptions import HTTPException +from . import typing as ft from .globals import _app_ctx_stack from .globals import _request_ctx_stack from .signals import appcontext_popped from .signals import appcontext_pushed -from .typing import AfterRequestCallable if t.TYPE_CHECKING: from .app import Flask @@ -109,7 +109,7 @@ def __repr__(self) -> str: return object.__repr__(self) -def after_this_request(f: AfterRequestCallable) -> AfterRequestCallable: +def after_this_request(f: ft.AfterRequestCallable) -> ft.AfterRequestCallable: """Executes a function after this request. This is useful to modify response objects. The function is passed the response object and has to return the same or a new one. @@ -341,7 +341,7 @@ def __init__( # Functions that should be executed after the request on the response # object. These will be called before the regular "after_request" # functions. - self._after_request_functions: t.List[AfterRequestCallable] = [] + self._after_request_functions: t.List[ft.AfterRequestCallable] = [] @property def g(self) -> _AppCtxGlobals: diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index acf8708b55..7cd8bab5d7 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -12,23 +12,16 @@ from werkzeug.exceptions import default_exceptions from werkzeug.exceptions import HTTPException +from . import typing as ft from .cli import AppGroup from .globals import current_app from .helpers import get_root_path from .helpers import locked_cached_property from .helpers import send_from_directory from .templating import _default_template_ctx_processor -from .typing import AfterRequestCallable -from .typing import AppOrBlueprintKey -from .typing import BeforeRequestCallable -from .typing import TeardownCallable -from .typing import TemplateContextProcessorCallable -from .typing import URLDefaultCallable -from .typing import URLValuePreprocessorCallable if t.TYPE_CHECKING: from .wrappers import Response - from .typing import ErrorHandlerCallable # a singleton sentinel value for parameter defaults _sentinel = object() @@ -143,8 +136,8 @@ def __init__( #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.error_handler_spec: t.Dict[ - AppOrBlueprintKey, - t.Dict[t.Optional[int], t.Dict[t.Type[Exception], "ErrorHandlerCallable"]], + ft.AppOrBlueprintKey, + t.Dict[t.Optional[int], t.Dict[t.Type[Exception], ft.ErrorHandlerCallable]], ] = defaultdict(lambda: defaultdict(dict)) #: A data structure of functions to call at the beginning of @@ -158,7 +151,7 @@ def __init__( #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.before_request_funcs: t.Dict[ - AppOrBlueprintKey, t.List[BeforeRequestCallable] + ft.AppOrBlueprintKey, t.List[ft.BeforeRequestCallable] ] = defaultdict(list) #: A data structure of functions to call at the end of each @@ -172,7 +165,7 @@ def __init__( #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.after_request_funcs: t.Dict[ - AppOrBlueprintKey, t.List[AfterRequestCallable] + ft.AppOrBlueprintKey, t.List[ft.AfterRequestCallable] ] = defaultdict(list) #: A data structure of functions to call at the end of each @@ -187,7 +180,7 @@ def __init__( #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.teardown_request_funcs: t.Dict[ - AppOrBlueprintKey, t.List[TeardownCallable] + ft.AppOrBlueprintKey, t.List[ft.TeardownCallable] ] = defaultdict(list) #: A data structure of functions to call to pass extra context @@ -202,7 +195,7 @@ def __init__( #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.template_context_processors: t.Dict[ - AppOrBlueprintKey, t.List[TemplateContextProcessorCallable] + ft.AppOrBlueprintKey, t.List[ft.TemplateContextProcessorCallable] ] = defaultdict(list, {None: [_default_template_ctx_processor]}) #: A data structure of functions to call to modify the keyword @@ -217,8 +210,8 @@ def __init__( #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.url_value_preprocessors: t.Dict[ - AppOrBlueprintKey, - t.List[URLValuePreprocessorCallable], + ft.AppOrBlueprintKey, + t.List[ft.URLValuePreprocessorCallable], ] = defaultdict(list) #: A data structure of functions to call to modify the keyword @@ -233,7 +226,7 @@ def __init__( #: This data structure is internal. It should not be modified #: directly and its format may change at any time. self.url_default_functions: t.Dict[ - AppOrBlueprintKey, t.List[URLDefaultCallable] + ft.AppOrBlueprintKey, t.List[ft.URLDefaultCallable] ] = defaultdict(list) def __repr__(self) -> str: @@ -534,7 +527,7 @@ def decorator(f): return decorator @setupmethod - def before_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable: + def before_request(self, f: ft.BeforeRequestCallable) -> ft.BeforeRequestCallable: """Register a function to run before each request. For example, this can be used to open a database connection, or @@ -556,7 +549,7 @@ def load_user(): return f @setupmethod - def after_request(self, f: AfterRequestCallable) -> AfterRequestCallable: + def after_request(self, f: ft.AfterRequestCallable) -> ft.AfterRequestCallable: """Register a function to run after each request to this object. The function is called with the response object, and must return @@ -572,7 +565,7 @@ def after_request(self, f: AfterRequestCallable) -> AfterRequestCallable: return f @setupmethod - def teardown_request(self, f: TeardownCallable) -> TeardownCallable: + def teardown_request(self, f: ft.TeardownCallable) -> ft.TeardownCallable: """Register a function to be run at the end of each request, regardless of whether there was an exception or not. These functions are executed when the request context is popped, even if not an @@ -612,16 +605,16 @@ def teardown_request(self, f: TeardownCallable) -> TeardownCallable: @setupmethod def context_processor( - self, f: TemplateContextProcessorCallable - ) -> TemplateContextProcessorCallable: + self, f: ft.TemplateContextProcessorCallable + ) -> ft.TemplateContextProcessorCallable: """Registers a template context processor function.""" self.template_context_processors[None].append(f) return f @setupmethod def url_value_preprocessor( - self, f: URLValuePreprocessorCallable - ) -> URLValuePreprocessorCallable: + self, f: ft.URLValuePreprocessorCallable + ) -> ft.URLValuePreprocessorCallable: """Register a URL value preprocessor function for all view functions in the application. These functions will be called before the :meth:`before_request` functions. @@ -638,7 +631,7 @@ def url_value_preprocessor( return f @setupmethod - def url_defaults(self, f: URLDefaultCallable) -> URLDefaultCallable: + def url_defaults(self, f: ft.URLDefaultCallable) -> ft.URLDefaultCallable: """Callback function for URL defaults for all view functions of the application. It's called with the endpoint and values and should update the values passed in place. @@ -649,7 +642,7 @@ def url_defaults(self, f: URLDefaultCallable) -> URLDefaultCallable: @setupmethod def errorhandler( self, code_or_exception: t.Union[t.Type[Exception], int] - ) -> t.Callable[["ErrorHandlerCallable"], "ErrorHandlerCallable"]: + ) -> t.Callable[[ft.ErrorHandlerCallable], ft.ErrorHandlerCallable]: """Register a function to handle errors by code or exception class. A decorator that is used to register a function given an @@ -679,7 +672,7 @@ def special_exception_handler(error): an arbitrary exception """ - def decorator(f: "ErrorHandlerCallable") -> "ErrorHandlerCallable": + def decorator(f: ft.ErrorHandlerCallable) -> ft.ErrorHandlerCallable: self.register_error_handler(code_or_exception, f) return f @@ -689,7 +682,7 @@ def decorator(f: "ErrorHandlerCallable") -> "ErrorHandlerCallable": def register_error_handler( self, code_or_exception: t.Union[t.Type[Exception], int], - f: "ErrorHandlerCallable", + f: ft.ErrorHandlerCallable, ) -> None: """Alternative error attach function to the :meth:`errorhandler` decorator that is more straightforward to use for non decorator diff --git a/src/flask/views.py b/src/flask/views.py index 1bd5c68b06..1dd560c62b 100644 --- a/src/flask/views.py +++ b/src/flask/views.py @@ -1,8 +1,8 @@ import typing as t +from . import typing as ft from .globals import current_app from .globals import request -from .typing import ResponseReturnValue http_method_funcs = frozenset( @@ -59,7 +59,7 @@ def dispatch_request(self): #: .. versionadded:: 0.8 decorators: t.List[t.Callable] = [] - def dispatch_request(self) -> ResponseReturnValue: + def dispatch_request(self) -> ft.ResponseReturnValue: """Subclasses have to override this method to implement the actual view function code. This method is called with all the arguments from the URL rule. @@ -79,7 +79,7 @@ def as_view( constructor of the class. """ - def view(*args: t.Any, **kwargs: t.Any) -> ResponseReturnValue: + def view(*args: t.Any, **kwargs: t.Any) -> ft.ResponseReturnValue: self = view.view_class(*class_args, **class_kwargs) # type: ignore return current_app.ensure_sync(self.dispatch_request)(*args, **kwargs) @@ -146,7 +146,7 @@ def post(self): app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) """ - def dispatch_request(self, *args: t.Any, **kwargs: t.Any) -> ResponseReturnValue: + def dispatch_request(self, *args: t.Any, **kwargs: t.Any) -> ft.ResponseReturnValue: meth = getattr(self, request.method.lower(), None) # If the request method is HEAD and we don't have a handler for it From 3ecebcdf8de02818cd409afd6edd39a09248b3ef Mon Sep 17 00:00:00 2001 From: Stanislav Bushuev Date: Tue, 31 May 2022 17:51:39 +0200 Subject: [PATCH 026/229] Add test config.from_mapping method: ignoring items with non-upper keys --- tests/test_config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_config.py b/tests/test_config.py index 944b93d7c0..cd856b2bfc 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -113,6 +113,10 @@ def test_config_from_mapping(): app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo") common_object_test(app) + app = flask.Flask(__name__) + app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo", skip_key="skip") + common_object_test(app) + app = flask.Flask(__name__) with pytest.raises(TypeError): app.config.from_mapping({}, {}) From 21d32ee0672c69573303ac78ff308109b06c9c91 Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 1 Jun 2022 11:23:09 -0700 Subject: [PATCH 027/229] update requirements --- requirements/dev.txt | 4 ++-- requirements/typing.txt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index 2cdefc9687..3667056d82 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -16,7 +16,7 @@ click==8.1.3 # pip-tools distlib==0.3.4 # via virtualenv -filelock==3.7.0 +filelock==3.7.1 # via # tox # virtualenv @@ -30,7 +30,7 @@ pep517==0.12.0 # via pip-tools pip-compile-multi==2.4.5 # via -r requirements/dev.in -pip-tools==6.6.1 +pip-tools==6.6.2 # via pip-compile-multi platformdirs==2.5.2 # via virtualenv diff --git a/requirements/typing.txt b/requirements/typing.txt index bf5e2a39ac..2e17dfb61f 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -9,7 +9,7 @@ cffi==1.15.0 # via cryptography cryptography==37.0.2 # via -r requirements/typing.in -mypy==0.950 +mypy==0.960 # via -r requirements/typing.in mypy-extensions==0.4.3 # via mypy @@ -17,11 +17,11 @@ pycparser==2.21 # via cffi tomli==2.0.1 # via mypy -types-contextvars==2.4.5 +types-contextvars==2.4.6 # via -r requirements/typing.in types-dataclasses==0.6.5 # via -r requirements/typing.in -types-setuptools==57.4.15 +types-setuptools==57.4.17 # via -r requirements/typing.in typing-extensions==4.2.0 # via mypy From 8c6f1d96ded2a22429b1ee7da6c3870cb140b587 Mon Sep 17 00:00:00 2001 From: lecovi Date: Mon, 2 May 2022 15:22:08 -0300 Subject: [PATCH 028/229] add example code for testing typing tools --- tests/typing/typing_route.py | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/typing/typing_route.py diff --git a/tests/typing/typing_route.py b/tests/typing/typing_route.py new file mode 100644 index 0000000000..0cb45333c0 --- /dev/null +++ b/tests/typing/typing_route.py @@ -0,0 +1,53 @@ +from http import HTTPStatus +from typing import Tuple +from typing import Union + +from flask import Flask +from flask import jsonify +from flask.templating import render_template +from flask.views import View +from flask.wrappers import Response + + +app = Flask(__name__) + + +@app.route("/") +def hello_world() -> str: + return "

Hello, World!

" + + +@app.route("/json") +def hello_world_json() -> Response: + return jsonify({"response": "Hello, World!"}) + + +@app.route("/template") +@app.route("/template/") +def return_template(name: Union[str, None] = None) -> str: + return render_template("index.html", name=name) + + +@app.errorhandler(HTTPStatus.INTERNAL_SERVER_ERROR) +def error_500(e) -> Tuple[str, int]: + return "

Sorry, we are having problems

", HTTPStatus.INTERNAL_SERVER_ERROR + + +@app.before_request +def before_request() -> None: + app.logger.debug("Executing a sample before_request function") + return None + + +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"), +) From 5d31ce1031e8ca24dc908c319567a76110edd87e Mon Sep 17 00:00:00 2001 From: Nick Kocharhook Date: Wed, 1 Jun 2022 12:16:21 -0700 Subject: [PATCH 029/229] Fix incorrect references to query in testing doc The [EnvironBuilder doc](https://werkzeug.palletsprojects.com/en/2.1.x/test/#werkzeug.test.EnvironBuilder) shows that the correct name for the keyword argument is `query_string`, not `query`. Using `query` results in an error. I've fixed the two places this appears in the testing doc. --- docs/testing.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 6f9d6ee169..8545bd3937 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -92,7 +92,7 @@ The ``client`` has methods that match the common HTTP request methods, such as ``client.get()`` and ``client.post()``. They take many arguments for building the request; you can find the full documentation in :class:`~werkzeug.test.EnvironBuilder`. Typically you'll use ``path``, -``query``, ``headers``, and ``data`` or ``json``. +``query_string``, ``headers``, and ``data`` or ``json``. To make a request, call the method the request should use with the path to the route to test. A :class:`~werkzeug.test.TestResponse` is returned @@ -108,9 +108,9 @@ provides ``response.text``, or use ``response.get_data(as_text=True)``. assert b"

Hello, World!

" in response.data -Pass a dict ``query={"key": "value", ...}`` to set arguments in the -query string (after the ``?`` in the URL). Pass a dict ``headers={}`` -to set request headers. +Pass a dict ``query_string={"key": "value", ...}`` to set arguments in +the query string (after the ``?`` in the URL). Pass a dict +``headers={}`` to set request headers. To send a request body in a POST or PUT request, pass a value to ``data``. If raw bytes are passed, that exact body is used. Usually, From 72cae9ce2b6a5a296b7328df8dd73240e14718e6 Mon Sep 17 00:00:00 2001 From: Numerlor Date: Sun, 5 Jun 2022 02:57:49 +0200 Subject: [PATCH 030/229] Remove extra backtick --- src/flask/scaffold.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 7cd8bab5d7..e0eab54f9e 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -123,7 +123,7 @@ def __init__( self.view_functions: t.Dict[str, t.Callable] = {} #: A data structure of registered error handlers, in the format - #: ``{scope: {code: {class: handler}}}```. The ``scope`` key is + #: ``{scope: {code: {class: handler}}}``. The ``scope`` key is #: the name of a blueprint the handlers are active for, or #: ``None`` for all requests. The ``code`` key is the HTTP #: status code for ``HTTPException``, or ``None`` for From 81be290ec8889c157c84bc7ce857f883396c5daf Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 3 Jun 2022 12:54:54 -0700 Subject: [PATCH 031/229] view function is actually type checked --- src/flask/app.py | 4 +-- src/flask/blueprints.py | 2 +- src/flask/scaffold.py | 30 ++++++++++++++++------- src/flask/typing.py | 25 +++++++++---------- tests/typing/typing_route.py | 47 +++++++++++++++++++++--------------- 5 files changed, 63 insertions(+), 45 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 34ea5b294f..6b54918887 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1033,7 +1033,7 @@ def add_url_rule( self, rule: str, endpoint: t.Optional[str] = None, - view_func: t.Optional[t.Callable] = None, + view_func: t.Optional[ft.ViewCallable] = None, provide_automatic_options: t.Optional[bool] = None, **options: t.Any, ) -> None: @@ -1681,7 +1681,7 @@ def make_response(self, rv: ft.ResponseReturnValue) -> Response: if isinstance(rv[1], (Headers, dict, tuple, list)): rv, headers = rv else: - rv, status = rv # type: ignore[misc] + rv, status = rv # type: ignore[assignment,misc] # other sized tuples are not allowed else: raise TypeError( diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index c60183fa27..cf04cd78d7 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -384,7 +384,7 @@ def add_url_rule( self, rule: str, endpoint: t.Optional[str] = None, - view_func: t.Optional[t.Callable] = None, + view_func: t.Optional[ft.ViewCallable] = None, provide_automatic_options: t.Optional[bool] = None, **options: t.Any, ) -> None: diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 7cd8bab5d7..8400e89280 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -363,48 +363,60 @@ def _method_route( method: str, rule: str, options: dict, - ) -> t.Callable[[F], F]: + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: if "methods" in options: raise TypeError("Use the 'route' decorator to use the 'methods' argument.") return self.route(rule, methods=[method], **options) - def get(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: + def get( + self, rule: str, **options: t.Any + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: """Shortcut for :meth:`route` with ``methods=["GET"]``. .. versionadded:: 2.0 """ return self._method_route("GET", rule, options) - def post(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: + def post( + self, rule: str, **options: t.Any + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: """Shortcut for :meth:`route` with ``methods=["POST"]``. .. versionadded:: 2.0 """ return self._method_route("POST", rule, options) - def put(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: + def put( + self, rule: str, **options: t.Any + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: """Shortcut for :meth:`route` with ``methods=["PUT"]``. .. versionadded:: 2.0 """ return self._method_route("PUT", rule, options) - def delete(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: + def delete( + self, rule: str, **options: t.Any + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: """Shortcut for :meth:`route` with ``methods=["DELETE"]``. .. versionadded:: 2.0 """ return self._method_route("DELETE", rule, options) - def patch(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: + def patch( + self, rule: str, **options: t.Any + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: """Shortcut for :meth:`route` with ``methods=["PATCH"]``. .. versionadded:: 2.0 """ return self._method_route("PATCH", rule, options) - def route(self, rule: str, **options: t.Any) -> t.Callable[[F], F]: + def route( + self, rule: str, **options: t.Any + ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: """Decorate a view function to register it with the given URL rule and options. Calls :meth:`add_url_rule`, which has more details about the implementation. @@ -428,7 +440,7 @@ def index(): :class:`~werkzeug.routing.Rule` object. """ - def decorator(f: F) -> F: + def decorator(f: ft.RouteDecorator) -> ft.RouteDecorator: endpoint = options.pop("endpoint", None) self.add_url_rule(rule, endpoint, f, **options) return f @@ -440,7 +452,7 @@ def add_url_rule( self, rule: str, endpoint: t.Optional[str] = None, - view_func: t.Optional[t.Callable] = None, + view_func: t.Optional[ft.ViewCallable] = None, provide_automatic_options: t.Optional[bool] = None, **options: t.Any, ) -> None: diff --git a/src/flask/typing.py b/src/flask/typing.py index ec7c7969bf..d463a8de43 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -1,37 +1,30 @@ import typing as t - if t.TYPE_CHECKING: from _typeshed.wsgi import WSGIApplication # noqa: F401 from werkzeug.datastructures import Headers # noqa: F401 from werkzeug.wrappers import Response # noqa: F401 # The possible types that are directly convertible or are a Response object. -ResponseValue = t.Union[ - "Response", - str, - bytes, - t.Dict[str, t.Any], # any jsonify-able dict - t.Iterator[str], - t.Iterator[bytes], -] -StatusCode = int +ResponseValue = t.Union["Response", str, bytes, t.Dict[str, t.Any]] # the possible types for an individual HTTP header -HeaderName = str +# This should be a Union, but mypy doesn't pass unless it's a TypeVar. HeaderValue = t.Union[str, t.List[str], t.Tuple[str, ...]] # the possible types for HTTP headers HeadersValue = t.Union[ - "Headers", t.Dict[HeaderName, HeaderValue], t.List[t.Tuple[HeaderName, HeaderValue]] + "Headers", + t.Mapping[str, HeaderValue], + t.Sequence[t.Tuple[str, HeaderValue]], ] # The possible types returned by a route function. ResponseReturnValue = t.Union[ ResponseValue, t.Tuple[ResponseValue, HeadersValue], - t.Tuple[ResponseValue, StatusCode], - t.Tuple[ResponseValue, StatusCode, HeadersValue], + t.Tuple[ResponseValue, int], + t.Tuple[ResponseValue, int, HeadersValue], "WSGIApplication", ] @@ -51,6 +44,7 @@ TemplateTestCallable = t.Callable[..., bool] URLDefaultCallable = t.Callable[[str, dict], None] URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None] + # This should take Exception, but that either breaks typing the argument # with a specific exception, or decorating multiple times with different # exceptions (and using a union type on the argument). @@ -58,3 +52,6 @@ # https://github.com/pallets/flask/issues/4295 # https://github.com/pallets/flask/issues/4297 ErrorHandlerCallable = t.Callable[[t.Any], ResponseReturnValue] + +ViewCallable = t.Callable[..., ResponseReturnValue] +RouteDecorator = t.TypeVar("RouteDecorator", bound=ViewCallable) diff --git a/tests/typing/typing_route.py b/tests/typing/typing_route.py index 0cb45333c0..ba49d1328e 100644 --- a/tests/typing/typing_route.py +++ b/tests/typing/typing_route.py @@ -1,6 +1,6 @@ +from __future__ import annotations + from http import HTTPStatus -from typing import Tuple -from typing import Union from flask import Flask from flask import jsonify @@ -8,42 +8,51 @@ from flask.views import View from flask.wrappers import Response - app = Flask(__name__) -@app.route("/") -def hello_world() -> str: +@app.route("/str") +def hello_str() -> str: return "

Hello, World!

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

Hello, World!

" + + @app.route("/json") -def hello_world_json() -> Response: +def hello_json() -> Response: return jsonify({"response": "Hello, World!"}) -@app.route("/template") -@app.route("/template/") -def return_template(name: Union[str, None] = None) -> str: - return render_template("index.html", name=name) +@app.route("/status") +@app.route("/status/") +def tuple_status(code: int = 200) -> tuple[str, int]: + return "hello", code + + +@app.route("/status-enum") +def tuple_status_enum() -> tuple[str, int]: + return "hello", HTTPStatus.OK -@app.errorhandler(HTTPStatus.INTERNAL_SERVER_ERROR) -def error_500(e) -> Tuple[str, int]: - return "

Sorry, we are having problems

", HTTPStatus.INTERNAL_SERVER_ERROR +@app.route("/headers") +def tuple_headers() -> tuple[str, dict[str, str]]: + return "Hello, World!", {"Content-Type": "text/plain"} -@app.before_request -def before_request() -> None: - app.logger.debug("Executing a sample before_request function") - return None +@app.route("/template") +@app.route("/template/") +def return_template(name: str | None = None) -> str: + return render_template("index.html", name=name) class RenderTemplateView(View): - def __init__(self: "RenderTemplateView", template_name: str) -> None: + def __init__(self: RenderTemplateView, template_name: str) -> None: self.template_name = template_name - def dispatch_request(self: "RenderTemplateView") -> str: + def dispatch_request(self: RenderTemplateView) -> str: return render_template(self.template_name) From 3351a8677e5bd1e1b8cfe2860b73b3422cde4f31 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sun, 5 Jun 2022 15:44:28 -0700 Subject: [PATCH 032/229] add errorhandler type check tests --- src/flask/blueprints.py | 6 +++-- src/flask/scaffold.py | 4 ++-- src/flask/typing.py | 1 + tests/typing/typing_error_handler.py | 33 ++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 tests/typing/typing_error_handler.py diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index cf04cd78d7..87617989e0 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -561,12 +561,14 @@ def app_context_processor( ) return f - def app_errorhandler(self, code: t.Union[t.Type[Exception], int]) -> t.Callable: + def app_errorhandler( + self, code: t.Union[t.Type[Exception], int] + ) -> t.Callable[[ft.ErrorHandlerDecorator], ft.ErrorHandlerDecorator]: """Like :meth:`Flask.errorhandler` but for a blueprint. This handler is used for all requests, even if outside of the blueprint. """ - def decorator(f: ft.ErrorHandlerCallable) -> ft.ErrorHandlerCallable: + def decorator(f: ft.ErrorHandlerDecorator) -> ft.ErrorHandlerDecorator: self.record_once(lambda s: s.app.errorhandler(code)(f)) return f diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 8400e89280..dedfe309f3 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -654,7 +654,7 @@ def url_defaults(self, f: ft.URLDefaultCallable) -> ft.URLDefaultCallable: @setupmethod def errorhandler( self, code_or_exception: t.Union[t.Type[Exception], int] - ) -> t.Callable[[ft.ErrorHandlerCallable], ft.ErrorHandlerCallable]: + ) -> t.Callable[[ft.ErrorHandlerDecorator], ft.ErrorHandlerDecorator]: """Register a function to handle errors by code or exception class. A decorator that is used to register a function given an @@ -684,7 +684,7 @@ def special_exception_handler(error): an arbitrary exception """ - def decorator(f: ft.ErrorHandlerCallable) -> ft.ErrorHandlerCallable: + def decorator(f: ft.ErrorHandlerDecorator) -> ft.ErrorHandlerDecorator: self.register_error_handler(code_or_exception, f) return f diff --git a/src/flask/typing.py b/src/flask/typing.py index d463a8de43..e6d67f2077 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -52,6 +52,7 @@ # https://github.com/pallets/flask/issues/4295 # https://github.com/pallets/flask/issues/4297 ErrorHandlerCallable = t.Callable[[t.Any], ResponseReturnValue] +ErrorHandlerDecorator = t.TypeVar("ErrorHandlerDecorator", bound=ErrorHandlerCallable) ViewCallable = t.Callable[..., ResponseReturnValue] RouteDecorator = t.TypeVar("RouteDecorator", bound=ViewCallable) diff --git a/tests/typing/typing_error_handler.py b/tests/typing/typing_error_handler.py new file mode 100644 index 0000000000..ec9c886f22 --- /dev/null +++ b/tests/typing/typing_error_handler.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from http import HTTPStatus + +from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import NotFound + +from flask import Flask + +app = Flask(__name__) + + +@app.errorhandler(400) +@app.errorhandler(HTTPStatus.BAD_REQUEST) +@app.errorhandler(BadRequest) +def handle_400(e: BadRequest) -> str: + return "" + + +@app.errorhandler(ValueError) +def handle_custom(e: ValueError) -> str: + return "" + + +@app.errorhandler(ValueError) +def handle_accept_base(e: Exception) -> str: + return "" + + +@app.errorhandler(BadRequest) +@app.errorhandler(404) +def handle_multiple(e: BadRequest | NotFound) -> str: + return "" From 48766754b8e8a16bfb075229eaaf88643c958c4e Mon Sep 17 00:00:00 2001 From: David Lord Date: Sun, 5 Jun 2022 15:49:41 -0700 Subject: [PATCH 033/229] add typing tests to mypy config --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 597eece14f..e858d13a20 100644 --- a/setup.cfg +++ b/setup.cfg @@ -87,7 +87,7 @@ per-file-ignores = src/flask/__init__.py: F401 [mypy] -files = src/flask +files = src/flask, tests/typing python_version = 3.7 show_error_codes = True allow_redefinition = True From 88bcf78439b66b5fcba9f4d3a1c56307f98dbf1d Mon Sep 17 00:00:00 2001 From: Evgeny Prigorodov Date: Thu, 26 May 2022 21:12:36 +0200 Subject: [PATCH 034/229] instance_path for namespace packages uses path closest to submodule --- CHANGES.rst | 2 ++ src/flask/scaffold.py | 37 ++++++++++++++++++++++++----------- tests/test_instance_config.py | 19 ++++++++++++++++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index a181badcab..d423aba9c1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,8 @@ Unreleased - Inline some optional imports that are only used for certain CLI commands. :pr:`4606` - Relax type annotation for ``after_request`` functions. :issue:`4600` +- ``instance_path`` for namespace packages uses the path closest to + the imported submodule. :issue:`4600` Version 2.1.2 diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 8ca804a601..147d9827ae 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -780,30 +780,46 @@ def _matching_loader_thinks_module_is_package(loader, mod_name): ) -def _find_package_path(root_mod_name): +def _find_package_path(import_name): """Find the path that contains the package or module.""" + root_mod_name, _, _ = import_name.partition(".") + try: - spec = importlib.util.find_spec(root_mod_name) + root_spec = importlib.util.find_spec(root_mod_name) - if spec is None: + if root_spec is None: raise ValueError("not found") # ImportError: the machinery told us it does not exist # ValueError: # - the module name was invalid # - the module name is __main__ - # - *we* raised `ValueError` due to `spec` being `None` + # - *we* raised `ValueError` due to `root_spec` being `None` except (ImportError, ValueError): pass # handled below else: # namespace package - if spec.origin in {"namespace", None}: - return os.path.dirname(next(iter(spec.submodule_search_locations))) + if root_spec.origin in {"namespace", None}: + package_spec = importlib.util.find_spec(import_name) + if package_spec is not None and package_spec.submodule_search_locations: + # Pick the path in the namespace that contains the submodule. + package_path = os.path.commonpath( + package_spec.submodule_search_locations + ) + search_locations = ( + location + for location in root_spec.submodule_search_locations + if package_path.startswith(location) + ) + else: + # Pick the first path. + search_locations = iter(root_spec.submodule_search_locations) + return os.path.dirname(next(search_locations)) # a package (with __init__.py) - elif spec.submodule_search_locations: - return os.path.dirname(os.path.dirname(spec.origin)) + elif root_spec.submodule_search_locations: + return os.path.dirname(os.path.dirname(root_spec.origin)) # just a normal module else: - return os.path.dirname(spec.origin) + return os.path.dirname(root_spec.origin) # we were unable to find the `package_path` using PEP 451 loaders loader = pkgutil.get_loader(root_mod_name) @@ -845,8 +861,7 @@ def find_package(import_name: str): for import. If the package is not installed, it's assumed that the package was imported from the current working directory. """ - root_mod_name, _, _ = import_name.partition(".") - package_path = _find_package_path(root_mod_name) + package_path = _find_package_path(import_name) py_prefix = os.path.abspath(sys.prefix) # installed to the system diff --git a/tests/test_instance_config.py b/tests/test_instance_config.py index ee573664ec..d7fe619194 100644 --- a/tests/test_instance_config.py +++ b/tests/test_instance_config.py @@ -59,6 +59,25 @@ def test_uninstalled_package_paths(modules_tmpdir, purge_module): assert app.instance_path == str(modules_tmpdir.join("instance")) +def test_uninstalled_namespace_paths(tmpdir, monkeypatch, purge_module): + def create_namespace(package): + project = tmpdir.join(f"project-{package}") + monkeypatch.syspath_prepend(str(project)) + project.join("namespace").join(package).join("__init__.py").write( + "import flask\napp = flask.Flask(__name__)\n", ensure=True + ) + return project + + _ = create_namespace("package1") + project2 = create_namespace("package2") + purge_module("namespace.package2") + purge_module("namespace") + + from namespace.package2 import app + + assert app.instance_path == str(project2.join("instance")) + + def test_installed_module_paths( modules_tmpdir, modules_tmpdir_prefix, purge_module, site_packages, limit_loader ): From 3ba37d2afe6511c3f3153248f7342174bea5b131 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 6 Jun 2022 08:24:05 -0700 Subject: [PATCH 035/229] fix uninstalled package tests under tox --- src/flask/scaffold.py | 18 ++++++++++++++---- tests/test_instance_config.py | 3 --- tox.ini | 1 + 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 147d9827ae..80084a19be 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -1,5 +1,6 @@ import importlib.util import os +import pathlib import pkgutil import sys import typing as t @@ -780,6 +781,15 @@ def _matching_loader_thinks_module_is_package(loader, mod_name): ) +def _path_is_relative_to(path: pathlib.PurePath, base: str) -> bool: + # Path.is_relative_to doesn't exist until Python 3.9 + try: + path.relative_to(base) + return True + except ValueError: + return False + + def _find_package_path(import_name): """Find the path that contains the package or module.""" root_mod_name, _, _ = import_name.partition(".") @@ -802,13 +812,13 @@ def _find_package_path(import_name): package_spec = importlib.util.find_spec(import_name) if package_spec is not None and package_spec.submodule_search_locations: # Pick the path in the namespace that contains the submodule. - package_path = os.path.commonpath( - package_spec.submodule_search_locations + package_path = pathlib.Path( + os.path.commonpath(package_spec.submodule_search_locations) ) search_locations = ( location for location in root_spec.submodule_search_locations - if package_path.startswith(location) + if _path_is_relative_to(package_path, location) ) else: # Pick the first path. @@ -865,7 +875,7 @@ def find_package(import_name: str): py_prefix = os.path.abspath(sys.prefix) # installed to the system - if package_path.startswith(py_prefix): + if _path_is_relative_to(pathlib.PurePath(package_path), py_prefix): return py_prefix, package_path site_parent, site_folder = os.path.split(package_path) diff --git a/tests/test_instance_config.py b/tests/test_instance_config.py index d7fe619194..53e9804279 100644 --- a/tests/test_instance_config.py +++ b/tests/test_instance_config.py @@ -15,7 +15,6 @@ def test_explicit_instance_paths(modules_tmpdir): assert app.instance_path == str(modules_tmpdir) -@pytest.mark.xfail(reason="weird interaction with tox") def test_main_module_paths(modules_tmpdir, purge_module): app = modules_tmpdir.join("main_app.py") app.write('import flask\n\napp = flask.Flask("__main__")') @@ -27,7 +26,6 @@ def test_main_module_paths(modules_tmpdir, purge_module): assert app.instance_path == os.path.join(here, "instance") -@pytest.mark.xfail(reason="weird interaction with tox") def test_uninstalled_module_paths(modules_tmpdir, purge_module): app = modules_tmpdir.join("config_module_app.py").write( "import os\n" @@ -42,7 +40,6 @@ def test_uninstalled_module_paths(modules_tmpdir, purge_module): assert app.instance_path == str(modules_tmpdir.join("instance")) -@pytest.mark.xfail(reason="weird interaction with tox") def test_uninstalled_package_paths(modules_tmpdir, purge_module): app = modules_tmpdir.mkdir("config_package_app") init = app.join("__init__.py") diff --git a/tox.ini b/tox.ini index 077d66f23e..ee4d40f689 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ envlist = skip_missing_interpreters = true [testenv] +envtmpdir = {toxworkdir}/tmp/{envname} deps = -r requirements/tests.txt min: -r requirements/tests-pallets-min.txt From b06df0a792ceb5506da47e0c8fea09902c1058f9 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 6 Jun 2022 09:17:53 -0700 Subject: [PATCH 036/229] remove outdated instance path test --- tests/test_instance_config.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/test_instance_config.py b/tests/test_instance_config.py index 53e9804279..c8cf0bf752 100644 --- a/tests/test_instance_config.py +++ b/tests/test_instance_config.py @@ -1,4 +1,3 @@ -import os import sys import pytest @@ -15,17 +14,6 @@ def test_explicit_instance_paths(modules_tmpdir): assert app.instance_path == str(modules_tmpdir) -def test_main_module_paths(modules_tmpdir, purge_module): - app = modules_tmpdir.join("main_app.py") - app.write('import flask\n\napp = flask.Flask("__main__")') - purge_module("main_app") - - from main_app import app - - here = os.path.abspath(os.getcwd()) - assert app.instance_path == os.path.join(here, "instance") - - def test_uninstalled_module_paths(modules_tmpdir, purge_module): app = modules_tmpdir.join("config_module_app.py").write( "import os\n" From 96c97dec095f204b8a557c5ea804afdd329e3c43 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 6 Jun 2022 10:00:21 -0700 Subject: [PATCH 037/229] deprecate before_first_request --- CHANGES.rst | 5 +++-- src/flask/app.py | 45 +++++++++++++++++++++++++--------------- src/flask/blueprints.py | 13 ++++++++++++ tests/test_async.py | 10 +++++---- tests/test_basic.py | 18 +++++++++------- tests/test_blueprints.py | 8 ++++--- 6 files changed, 66 insertions(+), 33 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3ce9699e0c..d5fa9fe9ad 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,8 @@ Unreleased registering the blueprint will show a warning. In the next version, this will become an error just like the application setup methods. :issue:`4571` +- ``before_first_request`` is deprecated. Run setup code when creating + the application instead. :issue:`4605` Version 2.1.3 @@ -1032,8 +1034,7 @@ Released 2011-09-29, codename Rakija earlier feedback when users forget to import view code ahead of time. - Added the ability to register callbacks that are only triggered once - at the beginning of the first request. - (:meth:`Flask.before_first_request`) + at the beginning of the first request. (``before_first_request``) - Malformed JSON data will now trigger a bad request HTTP exception instead of a value error which usually would result in a 500 internal server error if not handled. This is a backwards diff --git a/src/flask/app.py b/src/flask/app.py index 9333502968..65e95623d6 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -451,6 +451,10 @@ def __init__( #: first request to this instance. To register a function, use the #: :meth:`before_first_request` decorator. #: + #: .. deprecated:: 2.2 + #: Will be removed in Flask 2.3. Run setup code when + #: creating the application instead. + #: #: .. versionadded:: 0.8 self.before_first_request_funcs: t.List[ft.BeforeFirstRequestCallable] = [] @@ -1255,8 +1259,21 @@ def before_first_request( The function will be called without any arguments and its return value is ignored. + .. deprecated:: 2.2 + Will be removed in Flask 2.3. Run setup code when creating + the application instead. + .. versionadded:: 0.8 """ + import warnings + + warnings.warn( + "'before_first_request' is deprecated and will be removed" + " in Flask 2.3. Run setup code while creating the" + " application instead.", + DeprecationWarning, + stacklevel=2, + ) self.before_first_request_funcs.append(f) return f @@ -1552,7 +1569,17 @@ def full_dispatch_request(self) -> Response: .. versionadded:: 0.7 """ - self.try_trigger_before_first_request_functions() + # Run before_first_request functions if this is the thread's first request. + # Inlined to avoid a method call on subsequent requests. + # This is deprecated, will be removed in Flask 2.3. + if not self._got_first_request: + with self._before_request_lock: + if not self._got_first_request: + for func in self.before_first_request_funcs: + self.ensure_sync(func)() + + self._got_first_request = True + try: request_started.send(self) rv = self.preprocess_request() @@ -1591,22 +1618,6 @@ def finalize_request( ) return response - def try_trigger_before_first_request_functions(self) -> None: - """Called before each request and will ensure that it triggers - the :attr:`before_first_request_funcs` and only exactly once per - application instance (which means process usually). - - :internal: - """ - if self._got_first_request: - return - with self._before_request_lock: - if self._got_first_request: - return - for func in self.before_first_request_funcs: - self.ensure_sync(func)() - self._got_first_request = True - def make_default_options_response(self) -> Response: """This method is called to create the default ``OPTIONS`` response. This can be changed through subclassing to change the default diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 7ed599a043..76b3606746 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -543,7 +543,20 @@ def before_app_first_request( ) -> ft.BeforeFirstRequestCallable: """Like :meth:`Flask.before_first_request`. Such a function is executed before the first request to the application. + + .. deprecated:: 2.2 + Will be removed in Flask 2.3. Run setup code when creating + the application instead. """ + import warnings + + warnings.warn( + "'before_app_first_request' is deprecated and will be" + " removed in Flask 2.3. Use 'record_once' instead to run" + " setup code when registering the blueprint.", + DeprecationWarning, + stacklevel=2, + ) self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) return f diff --git a/tests/test_async.py b/tests/test_async.py index c8bb9bf038..38be27c9a7 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -107,10 +107,12 @@ def test_async_before_after_request(): def index(): return "" - @app.before_first_request - async def before_first(): - nonlocal app_first_called - app_first_called = True + with pytest.deprecated_call(): + + @app.before_first_request + async def before_first(): + nonlocal app_first_called + app_first_called = True @app.before_request async def before(): diff --git a/tests/test_basic.py b/tests/test_basic.py index fa9f902df2..7c1f4197ed 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1684,9 +1684,11 @@ def index(): def test_before_first_request_functions(app, client): got = [] - @app.before_first_request - def foo(): - got.append(42) + with pytest.deprecated_call(): + + @app.before_first_request + def foo(): + got.append(42) client.get("/") assert got == [42] @@ -1698,10 +1700,12 @@ def foo(): def test_before_first_request_functions_concurrent(app, client): got = [] - @app.before_first_request - def foo(): - time.sleep(0.2) - got.append(42) + with pytest.deprecated_call(): + + @app.before_first_request + def foo(): + time.sleep(0.2) + got.append(42) def get_and_assert(): client.get("/") diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index fbe9eeee53..1bf130f52b 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -722,9 +722,11 @@ def test_app_request_processing(app, client): bp = flask.Blueprint("bp", __name__) evts = [] - @bp.before_app_first_request - def before_first_request(): - evts.append("first") + with pytest.deprecated_call(): + + @bp.before_app_first_request + def before_first_request(): + evts.append("first") @bp.before_app_request def before_app(): From 6e23239567efde1c2e272db87c53444d6c22a1bb Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 6 Jun 2022 11:04:04 -0700 Subject: [PATCH 038/229] add View.init_every_request attribute --- CHANGES.rst | 4 +++- src/flask/views.py | 54 ++++++++++++++++++++++++++++++++++----------- tests/test_views.py | 18 +++++++++++++++ 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d5fa9fe9ad..ce85e67340 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,7 +22,9 @@ Unreleased :issue:`4571` - ``before_first_request`` is deprecated. Run setup code when creating the application instead. :issue:`4605` - +- Added the ``View.init_every_request`` class attribute. If a view + subclass sets this to ``False``, the view will not create a new + instance on every request. :issue:`2520`. Version 2.1.3 ------------- diff --git a/src/flask/views.py b/src/flask/views.py index 1dd560c62b..b2d3b87eab 100644 --- a/src/flask/views.py +++ b/src/flask/views.py @@ -59,6 +59,18 @@ def dispatch_request(self): #: .. versionadded:: 0.8 decorators: t.List[t.Callable] = [] + #: Create a new instance of this view class for every request by + #: default. If a view subclass sets this to ``False``, the same + #: instance is used for every request. + #: + #: A single instance is more efficient, especially if complex setup + #: is done during init. However, storing data on ``self`` is no + #: longer safe across requests, and :data:`~flask.g` should be used + #: instead. + #: + #: .. versionadded:: 2.2 + init_every_request: t.ClassVar[bool] = True + def dispatch_request(self) -> ft.ResponseReturnValue: """Subclasses have to override this method to implement the actual view function code. This method is called with all @@ -69,19 +81,35 @@ def dispatch_request(self) -> ft.ResponseReturnValue: @classmethod def as_view( cls, name: str, *class_args: t.Any, **class_kwargs: t.Any - ) -> t.Callable: - """Converts the class into an actual view function that can be used - with the routing system. Internally this generates a function on the - fly which will instantiate the :class:`View` on each request and call - the :meth:`dispatch_request` method on it. - - The arguments passed to :meth:`as_view` are forwarded to the - constructor of the class. + ) -> ft.ViewCallable: + """Convert the class into a view function that can be registered + for a route. + + By default, the generated view will create a new instance of the + view class for every request and call its + :meth:`dispatch_request` method. If the view class sets + :attr:`init_every_request` to ``False``, the same instance will + be used for every request. + + The arguments passed to this method are forwarded to the view + class ``__init__`` method. + + .. versionchanged:: 2.2 + Added the ``init_every_request`` class attribute. """ + if cls.init_every_request: + + def view(**kwargs: t.Any) -> ft.ResponseReturnValue: + self = view.view_class( # type: ignore[attr-defined] + *class_args, **class_kwargs + ) + return current_app.ensure_sync(self.dispatch_request)(**kwargs) + + else: + self = cls(*class_args, **class_kwargs) - def view(*args: t.Any, **kwargs: t.Any) -> ft.ResponseReturnValue: - self = view.view_class(*class_args, **class_kwargs) # type: ignore - return current_app.ensure_sync(self.dispatch_request)(*args, **kwargs) + def view(**kwargs: t.Any) -> ft.ResponseReturnValue: + return current_app.ensure_sync(self.dispatch_request)(**kwargs) if cls.decorators: view.__name__ = name @@ -146,7 +174,7 @@ def post(self): app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) """ - def dispatch_request(self, *args: t.Any, **kwargs: t.Any) -> ft.ResponseReturnValue: + def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue: meth = getattr(self, request.method.lower(), None) # If the request method is HEAD and we don't have a handler for it @@ -155,4 +183,4 @@ def dispatch_request(self, *args: t.Any, **kwargs: t.Any) -> ft.ResponseReturnVa meth = getattr(self, "get", None) assert meth is not None, f"Unimplemented method {request.method!r}" - return current_app.ensure_sync(meth)(*args, **kwargs) + return current_app.ensure_sync(meth)(**kwargs) diff --git a/tests/test_views.py b/tests/test_views.py index 0e21525225..8d870def21 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -240,3 +240,21 @@ class View(GetView, OtherView): assert client.get("/").data == b"GET" assert client.post("/").status_code == 405 assert sorted(View.methods) == ["GET"] + + +def test_init_once(app, client): + n = 0 + + class CountInit(flask.views.View): + init_every_request = False + + def __init__(self): + nonlocal n + n += 1 + + def dispatch_request(self): + return str(n) + + app.add_url_rule("/", view_func=CountInit.as_view("index")) + assert client.get("/").data == b"1" + assert client.get("/").data == b"1" From bab5a65e6eaee20ac1706fc9439ee538f59e652f Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 6 Jun 2022 15:33:30 -0700 Subject: [PATCH 039/229] rewrite class-based view docs --- docs/views.rst | 426 +++++++++++++++++++++++++++------------------ src/flask/views.py | 100 ++++++----- 2 files changed, 311 insertions(+), 215 deletions(-) diff --git a/docs/views.rst b/docs/views.rst index 63d26c5c31..b7c5ba200e 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -1,235 +1,319 @@ -Pluggable Views -=============== +Class-based Views +================= -.. versionadded:: 0.7 +.. currentmodule:: flask.views -Flask 0.7 introduces pluggable views inspired by the generic views from -Django which are based on classes instead of functions. The main -intention is that you can replace parts of the implementations and this -way have customizable pluggable views. +This page introduces using the :class:`View` and :class:`MethodView` +classes to write class-based views. + +A class-based view is a class that acts as a view function. Because it +is a class, different instances of the class can be created with +different arguments, to change the behavior of the view. This is also +known as generic, reusable, or pluggable views. + +An example of where this is useful is defining a class that creates an +API based on the database model it is initialized with. + +For more complex API behavior and customization, look into the various +API extensions for Flask. -Basic Principle ---------------- -Consider you have a function that loads a list of objects from the -database and renders into a template:: +Basic Reusable View +------------------- - @app.route('/users/') - def show_users(page): +Let's walk through an example converting a view function to a view +class. We start with a view function that queries a list of users then +renders a template to show the list. + +.. code-block:: python + + @app.route("/users/") + def user_list(): users = User.query.all() - return render_template('users.html', users=users) + return render_template("users.html", users=users) -This is simple and flexible, but if you want to provide this view in a -generic fashion that can be adapted to other models and templates as well -you might want more flexibility. This is where pluggable class-based -views come into place. As the first step to convert this into a class -based view you would do this:: +This works for the user model, but let's say you also had more models +that needed list pages. You'd need to write another view function for +each model, even though the only thing that would change is the model +and template name. +Instead, you can write a :class:`View` subclass that will query a model +and render a template. As the first step, we'll convert the view to a +class without any customization. - from flask.views import View +.. code-block:: python - class ShowUsers(View): + from flask.views import View + class UserList(View): def dispatch_request(self): users = User.query.all() - return render_template('users.html', objects=users) + return render_template("users.html", objects=users) - app.add_url_rule('/users/', view_func=ShowUsers.as_view('show_users')) + app.add_url_rule("/users/", view_func=UserList.as_view("user_list")) -As you can see what you have to do is to create a subclass of -:class:`flask.views.View` and implement -:meth:`~flask.views.View.dispatch_request`. Then we have to convert that -class into an actual view function by using the -:meth:`~flask.views.View.as_view` class method. The string you pass to -that function is the name of the endpoint that view will then have. But -this by itself is not helpful, so let's refactor the code a bit:: +The :meth:`View.dispatch_request` method is the equivalent of the view +function. Calling :meth:`View.as_view` method will create a view +function that can be registered on the app with its +:meth:`~flask.Flask.add_url_rule` method. The first argument to +``as_view`` is the name to use to refer to the view with +:func:`~flask.url_for`. +.. note:: - from flask.views import View + You can't decorate the class with ``@app.route()`` the way you'd + do with a basic view function. - class ListView(View): +Next, we need to be able to register the same view class for different +models and templates, to make it more useful than the original function. +The class will take two arguments, the model and template, and store +them on ``self``. Then ``dispatch_request`` can reference these instead +of hard-coded values. - def get_template_name(self): - raise NotImplementedError() +.. code-block:: python - def render_template(self, context): - return render_template(self.get_template_name(), **context) + class ListView(View): + def __init__(self, model, template): + self.model = model + self.template = template def dispatch_request(self): - context = {'objects': self.get_objects()} - return self.render_template(context) + items = self.model.query.all() + return render_template(self.template, items=items) + +Remember, we create the view function with ``View.as_view()`` instead of +creating the class directly. Any extra arguments passed to ``as_view`` +are then passed when creating the class. Now we can register the same +view to handle multiple models. + +.. code-block:: python - class UserView(ListView): + app.add_url_rule( + "/users/", + view_func=ListView.as_view("user_list", User, "users.html"), + ) + app.add_url_rule( + "/stories/", + view_func=ListView.as_view("story_list", Story, "stories.html"), + ) - def get_template_name(self): - return 'users.html' - def get_objects(self): - return User.query.all() +URL Variables +------------- + +Any variables captured by the URL are passed as keyword arguments to the +``dispatch_request`` method, as they would be for a regular view +function. + +.. code-block:: python + + class DetailView(View): + def __init__(self, model): + self.model = model + self.template = f"{model.__name__.lower()}/detail.html" + + def dispatch_request(self, id) + item = self.model.query.get_or_404(id) + return render_template(self.template, item=item) + + app.add_url_rule("/users/", view_func=DetailView.as_view("user_detail")) + + +View Lifetime and ``self`` +-------------------------- + +By default, a new instance of the view class is created every time a +request is handled. This means that it is safe to write other data to +``self`` during the request, since the next request will not see it, +unlike other forms of global state. + +However, if your view class needs to do a lot of complex initialization, +doing it for every request is unnecessary and can be inefficient. To +avoid this, set :attr:`View.init_every_request` to ``False``, which will +only create one instance of the class and use it for every request. In +this case, writing to ``self`` is not safe. If you need to store data +during the request, use :data:`~flask.g` instead. + +In the ``ListView`` example, nothing writes to ``self`` during the +request, so it is more efficient to create a single instance. + +.. code-block:: python + + class ListView(View): + init_every_request = False -This of course is not that helpful for such a small example, but it's good -enough to explain the basic principle. When you have a class-based view -the question comes up what ``self`` points to. The way this works is that -whenever the request is dispatched a new instance of the class is created -and the :meth:`~flask.views.View.dispatch_request` method is called with -the parameters from the URL rule. The class itself is instantiated with -the parameters passed to the :meth:`~flask.views.View.as_view` function. -For instance you can write a class like this:: + def __init__(self, model, template): + self.model = model + self.template = template - class RenderTemplateView(View): - def __init__(self, template_name): - self.template_name = template_name def dispatch_request(self): - return render_template(self.template_name) + items = self.model.query.all() + return render_template(self.template, items=items) -And then you can register it like this:: +Different instances will still be created each for each ``as_view`` +call, but not for each request to those views. + + +View Decorators +--------------- + +The view class itself is not the view function. View decorators need to +be applied to the view function returned by ``as_view``, not the class +itself. Set :attr:`View.decorators` to a list of decorators to apply. + +.. code-block:: python + + class UserList(View): + decorators = [cache(minutes=2), login_required] + + app.add_url_rule('/users/', view_func=UserList.as_view()) + +If you didn't set ``decorators``, you could apply them manually instead. +This is equivalent to: + +.. code-block:: python + + view = UserList.as_view("users_list") + view = cache(minutes=2)(view) + view = login_required(view) + app.add_url_rule('/users/', view_func=view) + +Keep in mind that order matters. If you're used to ``@decorator`` style, +this is equivalent to: + +.. code-block:: python + + @app.route("/users/") + @login_required + @cache(minutes=2) + def user_list(): + ... - app.add_url_rule('/about', view_func=RenderTemplateView.as_view( - 'about_page', template_name='about.html')) Method Hints ------------ -Pluggable views are attached to the application like a regular function by -either using :func:`~flask.Flask.route` or better -:meth:`~flask.Flask.add_url_rule`. That however also means that you would -have to provide the names of the HTTP methods the view supports when you -attach this. In order to move that information to the class you can -provide a :attr:`~flask.views.View.methods` attribute that has this -information:: +A common pattern is to register a view with ``methods=["GET", "POST"]``, +then check ``request.method == "POST"`` to decide what to do. Setting +:attr:`View.methods` is equivalent to passing the list of methods to +``add_url_rule`` or ``route``. + +.. code-block:: python class MyView(View): - methods = ['GET', 'POST'] + methods = ["GET", "POST"] def dispatch_request(self): - if request.method == 'POST': + if request.method == "POST": ... ... - app.add_url_rule('/myview', view_func=MyView.as_view('myview')) - -Method Based Dispatching ------------------------- + app.add_url_rule('/my-view', view_func=MyView.as_view('my-view')) -For RESTful APIs it's especially helpful to execute a different function -for each HTTP method. With the :class:`flask.views.MethodView` you can -easily do that. Each HTTP method maps to a method of the class with the -same name (just in lowercase):: +This is equivalent to the following, except further subclasses can +inherit or change the methods. - from flask.views import MethodView +.. code-block:: python - class UserAPI(MethodView): + app.add_url_rule( + "/my-view", + view_func=MyView.as_view("my-view"), + methods=["GET", "POST"], + ) - def get(self): - users = User.query.all() - ... - def post(self): - user = User.from_form_data(request.form) - ... +Method Dispatching and APIs +--------------------------- - app.add_url_rule('/users/', view_func=UserAPI.as_view('users')) +For APIs it can be helpful to use a different function for each HTTP +method. :class:`MethodView` extends the basic :class:`View` to dispatch +to different methods of the class based on the request method. Each HTTP +method maps to a method of the class with the same (lowercase) name. -That way you also don't have to provide the -:attr:`~flask.views.View.methods` attribute. It's automatically set based -on the methods defined in the class. +:class:`MethodView` automatically sets :attr:`View.methods` based on the +methods defined by the class. It even knows how to handle subclasses +that override or define other methods. -Decorating Views ----------------- +We can make a generic ``ItemAPI`` class that provides get (detail), +patch (edit), and delete methods for a given model. A ``GroupAPI`` can +provide get (list) and post (create) methods. -Since the view class itself is not the view function that is added to the -routing system it does not make much sense to decorate the class itself. -Instead you either have to decorate the return value of -:meth:`~flask.views.View.as_view` by hand:: +.. code-block:: python - def user_required(f): - """Checks whether user is logged in or raises error 401.""" - def decorator(*args, **kwargs): - if not g.user: - abort(401) - return f(*args, **kwargs) - return decorator + from flask.views import MethodView - view = user_required(UserAPI.as_view('users')) - app.add_url_rule('/users/', view_func=view) + class ItemAPI(MethodView): + init_every_request = False -Starting with Flask 0.8 there is also an alternative way where you can -specify a list of decorators to apply in the class declaration:: + def __init__(self, model): + self.model + self.validator = generate_validator(model) - class UserAPI(MethodView): - decorators = [user_required] + def _get_item(self, id): + return self.model.query.get_or_404(id) -Due to the implicit self from the caller's perspective you cannot use -regular view decorators on the individual methods of the view however, -keep this in mind. + def get(self, id): + user = self._get_item(id) + return jsonify(item.to_json()) -Method Views for APIs ---------------------- + def patch(self, id): + item = self._get_item(id) + errors = self.validator.validate(item, request.json) -Web APIs are often working very closely with HTTP verbs so it makes a lot -of sense to implement such an API based on the -:class:`~flask.views.MethodView`. That said, you will notice that the API -will require different URL rules that go to the same method view most of -the time. For instance consider that you are exposing a user object on -the web: + if errors: + return jsonify(errors), 400 -=============== =============== ====================================== -URL Method Description ---------------- --------------- -------------------------------------- -``/users/`` ``GET`` Gives a list of all users -``/users/`` ``POST`` Creates a new user -``/users/`` ``GET`` Shows a single user -``/users/`` ``PUT`` Updates a single user -``/users/`` ``DELETE`` Deletes a single user -=============== =============== ====================================== + item.update_from_json(request.json) + db.session.commit() + return jsonify(item.to_json()) -So how would you go about doing that with the -:class:`~flask.views.MethodView`? The trick is to take advantage of the -fact that you can provide multiple rules to the same view. + def delete(self, id): + item = self._get_item(id) + db.session.delete(item) + db.session.commit() + return "", 204 -Let's assume for the moment the view would look like this:: + class GroupAPI(MethodView): + init_every_request = False - class UserAPI(MethodView): + def __init__(self, model): + self.model = model + self.validator = generate_validator(model, create=True) - def get(self, user_id): - if user_id is None: - # return a list of users - pass - else: - # expose a single user - pass + def get(self): + items = self.model.query.all() + return jsonify([item.to_json() for item in items]) def post(self): - # create a new user - pass - - def delete(self, user_id): - # delete a single user - pass - - def put(self, user_id): - # update a single user - pass - -So how do we hook this up with the routing system? By adding two rules -and explicitly mentioning the methods for each:: - - user_view = UserAPI.as_view('user_api') - app.add_url_rule('/users/', defaults={'user_id': None}, - view_func=user_view, methods=['GET',]) - app.add_url_rule('/users/', view_func=user_view, methods=['POST',]) - app.add_url_rule('/users/', view_func=user_view, - methods=['GET', 'PUT', 'DELETE']) - -If you have a lot of APIs that look similar you can refactor that -registration code:: - - def register_api(view, endpoint, url, pk='id', pk_type='int'): - view_func = view.as_view(endpoint) - app.add_url_rule(url, defaults={pk: None}, - view_func=view_func, methods=['GET',]) - app.add_url_rule(url, view_func=view_func, methods=['POST',]) - app.add_url_rule(f'{url}<{pk_type}:{pk}>', view_func=view_func, - methods=['GET', 'PUT', 'DELETE']) - - register_api(UserAPI, 'user_api', '/users/', pk='user_id') + errors = self.validator.validate(request.json) + + if errors: + return jsonify(errors), 400 + + db.session.add(self.model.from_json(request.json)) + db.session.commit() + return jsonify(item.to_json()) + + def register_api(app, model, url): + app.add_url_rule(f"/{name}/", view_func=ItemAPI(f"{name}-item", model)) + app.add_url_rule(f"/{name}/", view_func=GroupAPI(f"{name}-group", model)) + + register_api(app, User, "users") + register_api(app, Story, "stories") + +This produces the following views, a standard REST API! + +================= ========== =================== +URL Method Description +----------------- ---------- ------------------- +``/users/`` ``GET`` List all users +``/users/`` ``POST`` Create a new user +``/users/`` ``GET`` Show a single user +``/users/`` ``PATCH`` Update a user +``/users/`` ``DELETE`` Delete a user +``/stories/`` ``GET`` List all stories +``/stories/`` ``POST`` Create a new story +``/stories/`` ``GET`` Show a single story +``/stories/`` ``PATCH`` Update a story +``/stories/`` ``DELETE`` Delete a story +================= ========== =================== diff --git a/src/flask/views.py b/src/flask/views.py index b2d3b87eab..def95e4d9f 100644 --- a/src/flask/views.py +++ b/src/flask/views.py @@ -11,53 +11,54 @@ class View: - """Alternative way to use view functions. A subclass has to implement - :meth:`dispatch_request` which is called with the view arguments from - the URL routing system. If :attr:`methods` is provided the methods - do not have to be passed to the :meth:`~flask.Flask.add_url_rule` - method explicitly:: + """Subclass this class and override :meth:`dispatch_request` to + create a generic class-based view. Call :meth:`as_view` to create a + view function that creates an instance of the class with the given + arguments and calls its ``dispatch_request`` method with any URL + variables. - class MyView(View): - methods = ['GET'] + See :doc:`views` for a detailed guide. - def dispatch_request(self, name): - return f"Hello {name}!" + .. code-block:: python + + class Hello(View): + init_every_request = False - app.add_url_rule('/hello/', view_func=MyView.as_view('myview')) + def dispatch_request(self, name): + return f"Hello, {name}!" - When you want to decorate a pluggable view you will have to either do that - when the view function is created (by wrapping the return value of - :meth:`as_view`) or you can use the :attr:`decorators` attribute:: + app.add_url_rule( + "/hello/", view_func=Hello.as_view("hello") + ) - class SecretView(View): - methods = ['GET'] - decorators = [superuser_required] + Set :attr:`methods` on the class to change what methods the view + accepts. - def dispatch_request(self): - ... + Set :attr:`decorators` on the class to apply a list of decorators to + the generated view function. Decorators applied to the class itself + will not be applied to the generated view function! - The decorators stored in the decorators list are applied one after another - when the view function is created. Note that you can *not* use the class - based decorators since those would decorate the view class and not the - generated view function! + Set :attr:`init_every_request` to ``False`` for efficiency, unless + you need to store request-global data on ``self``. """ - #: A list of methods this view can handle. - methods: t.Optional[t.List[str]] = None + #: The methods this view is registered for. Uses the same default + #: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and + #: ``add_url_rule`` by default. + methods: t.ClassVar[t.Optional[t.List[str]]] = None - #: Setting this disables or force-enables the automatic options handling. - provide_automatic_options: t.Optional[bool] = None + #: Control whether the ``OPTIONS`` method is handled automatically. + #: Uses the same default (``True``) as ``route`` and + #: ``add_url_rule`` by default. + provide_automatic_options: t.ClassVar[t.Optional[bool]] = None - #: The canonical way to decorate class-based views is to decorate the - #: return value of as_view(). However since this moves parts of the - #: logic from the class declaration to the place where it's hooked - #: into the routing system. - #: - #: You can place one or more decorators in this list and whenever the - #: view function is created the result is automatically decorated. + #: A list of decorators to apply, in order, to the generated view + #: function. Remember that ``@decorator`` syntax is applied bottom + #: to top, so the first decorator in the list would be the bottom + #: decorator. #: #: .. versionadded:: 0.8 - decorators: t.List[t.Callable] = [] + decorators: t.ClassVar[t.List[t.Callable]] = [] #: Create a new instance of this view class for every request by #: default. If a view subclass sets this to ``False``, the same @@ -72,9 +73,9 @@ def dispatch_request(self): init_every_request: t.ClassVar[bool] = True def dispatch_request(self) -> ft.ResponseReturnValue: - """Subclasses have to override this method to implement the - actual view function code. This method is called with all - the arguments from the URL rule. + """The actual view function behavior. Subclasses must override + this and return a valid response. Any variables from the URL + rule are passed as keyword arguments. """ raise NotImplementedError() @@ -159,19 +160,30 @@ def __init__(cls, name, bases, d): class MethodView(View, metaclass=MethodViewType): - """A class-based view that dispatches request methods to the corresponding - class methods. For example, if you implement a ``get`` method, it will be - used to handle ``GET`` requests. :: + """Dispatches request methods to the corresponding instance methods. + For example, if you implement a ``get`` method, it will be used to + handle ``GET`` requests. + + This can be useful for defining a REST API. + + :attr:`methods` is automatically set based on the methods defined on + the class. + + See :doc:`views` for a detailed guide. + + .. code-block:: python class CounterAPI(MethodView): def get(self): - return session.get('counter', 0) + return str(session.get("counter", 0)) def post(self): - session['counter'] = session.get('counter', 0) + 1 - return 'OK' + session["counter"] = session.get("counter", 0) + 1 + return redirect(url_for("counter")) - app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) + app.add_url_rule( + "/counter", view_func=CounterAPI.as_view("counter") + ) """ def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue: From 45174bf9a1e96e51ce589c7aa2c151249d1fdf11 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 6 Jun 2022 15:40:04 -0700 Subject: [PATCH 040/229] use __init_subclass__ instead of metaclass for MethodView --- src/flask/views.py | 48 ++++++++++++++++++---------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/src/flask/views.py b/src/flask/views.py index def95e4d9f..7aac3dd5a5 100644 --- a/src/flask/views.py +++ b/src/flask/views.py @@ -45,7 +45,7 @@ def dispatch_request(self, name): #: The methods this view is registered for. Uses the same default #: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and #: ``add_url_rule`` by default. - methods: t.ClassVar[t.Optional[t.List[str]]] = None + methods: t.ClassVar[t.Optional[t.Collection[str]]] = None #: Control whether the ``OPTIONS`` method is handled automatically. #: Uses the same default (``True``) as ``route`` and @@ -132,34 +132,7 @@ def view(**kwargs: t.Any) -> ft.ResponseReturnValue: return view -class MethodViewType(type): - """Metaclass for :class:`MethodView` that determines what methods the view - defines. - """ - - def __init__(cls, name, bases, d): - super().__init__(name, bases, d) - - if "methods" not in d: - methods = set() - - for base in bases: - if getattr(base, "methods", None): - methods.update(base.methods) - - for key in http_method_funcs: - if hasattr(cls, key): - methods.add(key.upper()) - - # If we have no method at all in there we don't want to add a - # method list. This is for instance the case for the base class - # or another subclass of a base method view that does not introduce - # new methods. - if methods: - cls.methods = methods - - -class MethodView(View, metaclass=MethodViewType): +class MethodView(View): """Dispatches request methods to the corresponding instance methods. For example, if you implement a ``get`` method, it will be used to handle ``GET`` requests. @@ -186,6 +159,23 @@ def post(self): ) """ + def __init_subclass__(cls, **kwargs: t.Any) -> None: + super().__init_subclass__(**kwargs) + + if "methods" not in cls.__dict__: + methods = set() + + for base in cls.__bases__: + if getattr(base, "methods", None): + methods.update(base.methods) # type: ignore[attr-defined] + + for key in http_method_funcs: + if hasattr(cls, key): + methods.add(key.upper()) + + if methods: + cls.methods = methods + def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue: meth = getattr(self, request.method.lower(), None) From 6f6e3289da182593fb296da072049ade729adeea Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 14 Feb 2022 10:33:25 -0800 Subject: [PATCH 041/229] remove javascript fetch polyfill --- examples/javascript/README.rst | 16 ++++++++-------- .../javascript/js_example/templates/base.html | 8 ++++---- .../javascript/js_example/templates/fetch.html | 7 ++----- .../templates/{plain.html => xhr.html} | 5 +++-- examples/javascript/js_example/views.py | 4 ++-- examples/javascript/setup.cfg | 2 +- examples/javascript/tests/test_js_example.py | 4 ++-- 7 files changed, 22 insertions(+), 24 deletions(-) rename examples/javascript/js_example/templates/{plain.html => xhr.html} (79%) diff --git a/examples/javascript/README.rst b/examples/javascript/README.rst index b25bdb4e41..b6e340dfc9 100644 --- a/examples/javascript/README.rst +++ b/examples/javascript/README.rst @@ -3,15 +3,15 @@ JavaScript Ajax Example Demonstrates how to post form data and process a JSON response using JavaScript. This allows making requests without navigating away from the -page. Demonstrates using |XMLHttpRequest|_, |fetch|_, and -|jQuery.ajax|_. See the `Flask docs`_ about jQuery and Ajax. - -.. |XMLHttpRequest| replace:: ``XMLHttpRequest`` -.. _XMLHttpRequest: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest +page. Demonstrates using |fetch|_, |XMLHttpRequest|_, and +|jQuery.ajax|_. See the `Flask docs`_ about JavaScript and Ajax. .. |fetch| replace:: ``fetch`` .. _fetch: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch +.. |XMLHttpRequest| replace:: ``XMLHttpRequest`` +.. _XMLHttpRequest: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest + .. |jQuery.ajax| replace:: ``jQuery.ajax`` .. _jQuery.ajax: https://api.jquery.com/jQuery.ajax/ @@ -21,7 +21,7 @@ page. Demonstrates using |XMLHttpRequest|_, |fetch|_, and Install ------- -:: +.. code-block:: text $ python3 -m venv venv $ . venv/bin/activate @@ -31,7 +31,7 @@ Install Run --- -:: +.. code-block:: text $ export FLASK_APP=js_example $ flask run @@ -42,7 +42,7 @@ Open http://127.0.0.1:5000 in a browser. Test ---- -:: +.. code-block:: text $ pip install -e '.[test]' $ coverage run -m pytest diff --git a/examples/javascript/js_example/templates/base.html b/examples/javascript/js_example/templates/base.html index 50ce0e9c4b..a4d35bd7d7 100644 --- a/examples/javascript/js_example/templates/base.html +++ b/examples/javascript/js_example/templates/base.html @@ -1,7 +1,7 @@ JavaScript Example - - + + diff --git a/examples/javascript/js_example/templates/fetch.html b/examples/javascript/js_example/templates/fetch.html index 780ecec505..e2944b8575 100644 --- a/examples/javascript/js_example/templates/fetch.html +++ b/examples/javascript/js_example/templates/fetch.html @@ -2,14 +2,11 @@ {% block intro %} fetch - is the new plain JavaScript way to make requests. It's - supported in all modern browsers except IE, which requires a - polyfill. + is the modern plain JavaScript way to make requests. It's + supported in all modern browsers. {% endblock %} {% block script %} - - + +A less common pattern is to add the data to a ``data-`` attribute on an +HTML tag. In this case, you must use single quotes around the value, not +double quotes, otherwise you will produce invalid or unsafe HTML. + +.. code-block:: jinja + +
+ + +Generating URLs +--------------- + +The other way to get data from the server to JavaScript is to make a +request for it. First, you need to know the URL to request. + +The simplest way to generate URLs is to continue to use +:func:`~flask.url_for` when rendering the template. For example: + +.. code-block:: javascript + + const user_url = {{ url_for("user", id=current_user.id)|tojson }} + fetch(user_url).then(...) + +However, you might need to generate a URL based on information you only +know in JavaScript. As discussed above, JavaScript runs in the user's +browser, not as part of the template rendering, so you can't use +``url_for`` at that point. + +In this case, you need to know the "root URL" under which your +application is served. In simple setups, this is ``/``, but it might +also be something else, like ``https://example.com/myapp/``. + +A simple way to tell your JavaScript code about this root is to set it +as a global variable when rendering the template. Then you can use it +when generating URLs from JavaScript. + +.. code-block:: javascript + + const SCRIPT_ROOT = {{ request.script_root|tojson }} + let user_id = ... // do something to get a user id from the page + let user_url = `${SCRIPT_ROOT}/user/${user_id}` + fetch(user_url).then(...) + + +Making a Request with ``fetch`` +------------------------------- + +|fetch|_ takes two arguments, a URL and an object with other options, +and returns a |Promise|_. We won't cover all the available options, and +will only use ``then()`` on the promise, not other callbacks or +``await`` syntax. Read the linked MDN docs for more information about +those features. + +By default, the GET method is used. If the response contains JSON, it +can be used with a ``then()`` callback chain. + +.. code-block:: javascript + + const room_url = {{ url_for("room_detail", id=room.id)|tojson }} + fetch(room_url) + .then(response => response.json()) + .then(data => { + // data is a parsed JSON object + }) + +To send data, use a data method such as POST, and pass the ``body`` +option. The most common types for data are form data or JSON data. + +To send form data, pass a populated |FormData|_ object. This uses the +same format as an HTML form, and would be accessed with ``request.form`` +in a Flask view. + +.. code-block:: javascript + + let data = new FormData() + data.append("name": "Flask Room") + data.append("description": "Talk about Flask here.") + fetch(room_url, { + "method": "POST", + "body": data, + }).then(...) + +In general, prefer sending request data as form data, as would be used +when submitting an HTML form. JSON can represent more complex data, but +unless you need that it's better to stick with the simpler format. When +sending JSON data, the ``Content-Type: application/json`` header must be +sent as well, otherwise Flask will return a 400 error. + +.. code-block:: javascript + + let data = { + "name": "Flask Room", + "description": "Talk about Flask here.", + } + fetch(room_url, { + "method": "POST", + "headers": {"Content-Type": "application/json"}, + "body": JSON.stringify(data), + }).then(...) + +.. |Promise| replace:: ``Promise`` +.. _Promise: https://developer.mozilla.org/Web/JavaScript/Reference/Global_Objects/Promise +.. |FormData| replace:: ``FormData`` +.. _FormData: https://developer.mozilla.org/en-US/docs/Web/API/FormData + + +Following Redirects +------------------- + +A response might be a redirect, for example if you logged in with +JavaScript instead of a traditional HTML form, and your view returned +a redirect instead of JSON. JavaScript requests do follow redirects, but +they don't change the page. If you want to make the page change you can +inspect the response and apply the redirect manually. + +.. code-block:: javascript + + fetch("/login", {"body": ...}).then( + response => { + if (response.redirected) { + window.location = response.url + } else { + showLoginError() + } + } + ) + + +Replacing Content +----------------- + +A response might be new HTML, either a new section of the page to add or +replace, or an entirely new page. In general, if you're returning the +entire page, it would be better to handle that with a redirect as shown +in the previous section. The following example shows how to a ``
`` +with the HTML returned by a request. + +.. code-block:: html + +
+ {{ include "geology_fact.html" }} +
+ + + +Return JSON from Views +---------------------- + +To return a JSON object from your API view, you can directly return a +dict from the view. It will be serialized to JSON automatically. + +.. code-block:: python + + @app.route("/user/") + def user_detail(id): + user = User.query.get_or_404(id) + return { + "username": User.username, + "email": User.email, + "picture": url_for("static", filename=f"users/{id}/profile.png"), + } + +If you want to return another JSON type, use the +:func:`~flask.json.jsonify` function, which creates a response object +with the given data serialized to JSON. + +.. code-block:: python + + from flask import jsonify + + @app.route("/users") + def user_list(): + users = User.query.order_by(User.name).all() + return jsonify([u.to_json() for u in users]) + +It is usually not a good idea to return file data in a JSON response. +JSON cannot represent binary data directly, so it must be base64 +encoded, which can be slow, takes more bandwidth to send, and is not as +easy to cache. Instead, serve files using one view, and generate a URL +to the desired file to include in the JSON. Then the client can make a +separate request to get the linked resource after getting the JSON. + + +Receiving JSON in Views +----------------------- + +Use the :attr:`~flask.Request.json` property of the +:data:`~flask.request` object to decode the request's body as JSON. If +the body is not valid JSON, or the ``Content-Type`` header is not set to +``application/json``, a 400 Bad Request error will be raised. + +.. code-block:: python + + from flask import request + + @app.post("/user/") + def user_update(id): + user = User.query.get_or_404(id) + user.update_from_json(request.json) + db.session.commit() + return user.to_json() diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index 0a75bb71a5..7ac6856eac 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -1,148 +1,6 @@ +:orphan: + AJAX with jQuery ================ -`jQuery`_ is a small JavaScript library commonly used to simplify working -with the DOM and JavaScript in general. It is the perfect tool to make -web applications more dynamic by exchanging JSON between server and -client. - -JSON itself is a very lightweight transport format, very similar to how -Python primitives (numbers, strings, dicts and lists) look like which is -widely supported and very easy to parse. It became popular a few years -ago and quickly replaced XML as transport format in web applications. - -.. _jQuery: https://jquery.com/ - -Loading jQuery --------------- - -In order to use jQuery, you have to download it first and place it in the -static folder of your application and then ensure it's loaded. Ideally -you have a layout template that is used for all pages where you just have -to add a script statement to the bottom of your ```` to load jQuery: - -.. sourcecode:: html - - - -Another method is using Google's `AJAX Libraries API -`_ to load jQuery: - -.. sourcecode:: html - - - - -In this case you have to put jQuery into your static folder as a fallback, but it will -first try to load it directly from Google. This has the advantage that your -website will probably load faster for users if they went to at least one -other website before using the same jQuery version from Google because it -will already be in the browser cache. - -Where is My Site? ------------------ - -Do you know where your application is? If you are developing the answer -is quite simple: it's on localhost port something and directly on the root -of that server. But what if you later decide to move your application to -a different location? For example to ``http://example.com/myapp``? On -the server side this never was a problem because we were using the handy -:func:`~flask.url_for` function that could answer that question for -us, but if we are using jQuery we should not hardcode the path to -the application but make that dynamic, so how can we do that? - -A simple method would be to add a script tag to our page that sets a -global variable to the prefix to the root of the application. Something -like this: - -.. sourcecode:: html+jinja - - - - -JSON View Functions -------------------- - -Now let's create a server side function that accepts two URL arguments of -numbers which should be added together and then sent back to the -application in a JSON object. This is a really ridiculous example and is -something you usually would do on the client side alone, but a simple -example that shows how you would use jQuery and Flask nonetheless:: - - from flask import Flask, jsonify, render_template, request - app = Flask(__name__) - - @app.route('/_add_numbers') - def add_numbers(): - a = request.args.get('a', 0, type=int) - b = request.args.get('b', 0, type=int) - return jsonify(result=a + b) - - @app.route('/') - def index(): - return render_template('index.html') - -As you can see I also added an `index` method here that renders a -template. This template will load jQuery as above and have a little form where -we can add two numbers and a link to trigger the function on the server -side. - -Note that we are using the :meth:`~werkzeug.datastructures.MultiDict.get` method here -which will never fail. If the key is missing a default value (here ``0``) -is returned. Furthermore it can convert values to a specific type (like -in our case `int`). This is especially handy for code that is -triggered by a script (APIs, JavaScript etc.) because you don't need -special error reporting in that case. - -The HTML --------- - -Your index.html template either has to extend a :file:`layout.html` template with -jQuery loaded and the `$SCRIPT_ROOT` variable set, or do that on the top. -Here's the HTML code needed for our little application (:file:`index.html`). -Notice that we also drop the script directly into the HTML here. It is -usually a better idea to have that in a separate script file: - -.. sourcecode:: html - - -

jQuery Example

-

+ - = - ? -

calculate server side - -I won't go into detail here about how jQuery works, just a very quick -explanation of the little bit of code above: - -1. ``$(function() { ... })`` specifies code that should run once the - browser is done loading the basic parts of the page. -2. ``$('selector')`` selects an element and lets you operate on it. -3. ``element.bind('event', func)`` specifies a function that should run - when the user clicked on the element. If that function returns - `false`, the default behavior will not kick in (in this case, navigate - to the `#` URL). -4. ``$.getJSON(url, data, func)`` sends a ``GET`` request to `url` and will - send the contents of the `data` object as query parameters. Once the - data arrived, it will call the given function with the return value as - argument. Note that we can use the `$SCRIPT_ROOT` variable here that - we set earlier. - -Check out the :gh:`example source ` for a full -application demonstrating the code on this page, as well as the same -thing using ``XMLHttpRequest`` and ``fetch``. +Obsolete, see :doc:`/patterns/javascript` instead. From 2ea77c278282f04a8a18c3ced88ef1e048f8ed4a Mon Sep 17 00:00:00 2001 From: David Lord Date: Sun, 12 Jun 2022 13:45:31 -0700 Subject: [PATCH 044/229] rewrite deployment docs --- docs/async-await.rst | 4 +- docs/deploying/apache-httpd.rst | 66 ++++++++ docs/deploying/asgi.rst | 2 - docs/deploying/cgi.rst | 61 ------- docs/deploying/eventlet.rst | 80 +++++++++ docs/deploying/fastcgi.rst | 238 -------------------------- docs/deploying/gevent.rst | 80 +++++++++ docs/deploying/gunicorn.rst | 130 ++++++++++++++ docs/deploying/index.rst | 99 ++++++++--- docs/deploying/mod_wsgi.rst | 253 ++++++++-------------------- docs/deploying/nginx.rst | 69 ++++++++ docs/deploying/proxy_fix.rst | 33 ++++ docs/deploying/uwsgi.rst | 176 +++++++++++++------ docs/deploying/waitress.rst | 75 +++++++++ docs/deploying/wsgi-standalone.rst | 262 ----------------------------- 15 files changed, 803 insertions(+), 825 deletions(-) create mode 100644 docs/deploying/apache-httpd.rst delete mode 100644 docs/deploying/cgi.rst create mode 100644 docs/deploying/eventlet.rst delete mode 100644 docs/deploying/fastcgi.rst create mode 100644 docs/deploying/gevent.rst create mode 100644 docs/deploying/gunicorn.rst create mode 100644 docs/deploying/nginx.rst create mode 100644 docs/deploying/proxy_fix.rst create mode 100644 docs/deploying/waitress.rst delete mode 100644 docs/deploying/wsgi-standalone.rst diff --git a/docs/async-await.rst b/docs/async-await.rst index 4c70f96142..cea8460231 100644 --- a/docs/async-await.rst +++ b/docs/async-await.rst @@ -70,8 +70,8 @@ If you wish to use background tasks it is best to use a task queue to trigger background work, rather than spawn tasks in a view function. With that in mind you can spawn asyncio tasks by serving Flask with an ASGI server and utilising the asgiref WsgiToAsgi adapter -as described in :ref:`asgi`. This works as the adapter creates an -event loop that runs continually. +as described in :doc:`deploying/asgi`. This works as the adapter creates +an event loop that runs continually. When to use Quart instead diff --git a/docs/deploying/apache-httpd.rst b/docs/deploying/apache-httpd.rst new file mode 100644 index 0000000000..bdeaf62657 --- /dev/null +++ b/docs/deploying/apache-httpd.rst @@ -0,0 +1,66 @@ +Apache httpd +============ + +`Apache httpd`_ is a fast, production level HTTP server. When serving +your application with one of the WSGI servers listed in :doc:`index`, it +is often good or necessary to put a dedicated HTTP server in front of +it. This "reverse proxy" can handle incoming requests, TLS, and other +security and performance concerns better than the WSGI server. + +httpd can be installed using your system package manager, or a pre-built +executable for Windows. Installing and running httpd itself is outside +the scope of this doc. This page outlines the basics of configuring +httpd to proxy your application. Be sure to read its documentation to +understand what features are available. + +.. _Apache httpd: https://httpd.apache.org/ + + +Domain Name +----------- + +Acquiring and configuring a domain name is outside the scope of this +doc. In general, you will buy a domain name from a registrar, pay for +server space with a hosting provider, and then point your registrar +at the hosting provider's name servers. + +To simulate this, you can also edit your ``hosts`` file, located at +``/etc/hosts`` on Linux. Add a line that associates a name with the +local IP. + +Modern Linux systems may be configured to treat any domain name that +ends with ``.localhost`` like this without adding it to the ``hosts`` +file. + +.. code-block:: python + :caption: ``/etc/hosts`` + + 127.0.0.1 hello.localhost + + +Configuration +------------- + +The httpd configuration is located at ``/etc/httpd/conf/httpd.conf`` on +Linux. It may be different depending on your operating system. Check the +docs and look for ``httpd.conf``. + +Remove or comment out any existing ``DocumentRoot`` directive. Add the +config lines below. We'll assume the WSGI server is listening locally at +``http://127.0.0.1:8000``. + +.. code-block:: apache + :caption: ``/etc/httpd/conf/httpd.conf`` + + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + ProxyPass / http://127.0.0.1:8000/ + RequestHeader set X-Forwarded-Proto http + RequestHeader set X-Forwarded-Prefix / + +The ``LoadModule`` lines might already exist. If so, make sure they are +uncommented instead of adding them manually. + +Then :doc:`proxy_fix` so that your application uses the ``X-Forwarded`` +headers. ``X-Forwarded-For`` and ``X-Forwarded-Host`` are automatically +set by ``ProxyPass``. diff --git a/docs/deploying/asgi.rst b/docs/deploying/asgi.rst index 39cd76b729..36acff8ac5 100644 --- a/docs/deploying/asgi.rst +++ b/docs/deploying/asgi.rst @@ -1,5 +1,3 @@ -.. _asgi: - ASGI ==== diff --git a/docs/deploying/cgi.rst b/docs/deploying/cgi.rst deleted file mode 100644 index 4c1cdfbf0f..0000000000 --- a/docs/deploying/cgi.rst +++ /dev/null @@ -1,61 +0,0 @@ -CGI -=== - -If all other deployment methods do not work, CGI will work for sure. -CGI is supported by all major servers but usually has a sub-optimal -performance. - -This is also the way you can use a Flask application on Google's `App -Engine`_, where execution happens in a CGI-like environment. - -.. admonition:: Watch Out - - Please make sure in advance that any ``app.run()`` calls you might - have in your application file are inside an ``if __name__ == - '__main__':`` block or moved to a separate file. Just make sure it's - not called because this will always start a local WSGI server which - we do not want if we deploy that application to CGI / app engine. - - With CGI, you will also have to make sure that your code does not contain - any ``print`` statements, or that ``sys.stdout`` is overridden by something - that doesn't write into the HTTP response. - -Creating a `.cgi` file ----------------------- - -First you need to create the CGI application file. Let's call it -:file:`yourapplication.cgi`:: - - #!/usr/bin/python - from wsgiref.handlers import CGIHandler - from yourapplication import app - - CGIHandler().run(app) - -Server Setup ------------- - -Usually there are two ways to configure the server. Either just copy the -``.cgi`` into a :file:`cgi-bin` (and use `mod_rewrite` or something similar to -rewrite the URL) or let the server point to the file directly. - -In Apache for example you can put something like this into the config: - -.. sourcecode:: apache - - ScriptAlias /app /path/to/the/application.cgi - -On shared webhosting, though, you might not have access to your Apache config. -In this case, a file called ``.htaccess``, sitting in the public directory -you want your app to be available, works too but the ``ScriptAlias`` directive -won't work in that case: - -.. sourcecode:: apache - - RewriteEngine On - RewriteCond %{REQUEST_FILENAME} !-f # Don't interfere with static files - RewriteRule ^(.*)$ /path/to/the/application.cgi/$1 [L] - -For more information consult the documentation of your webserver. - -.. _App Engine: https://cloud.google.com/appengine/docs/ diff --git a/docs/deploying/eventlet.rst b/docs/deploying/eventlet.rst new file mode 100644 index 0000000000..8842ce05aa --- /dev/null +++ b/docs/deploying/eventlet.rst @@ -0,0 +1,80 @@ +eventlet +======== + +Prefer using :doc:`gunicorn` with eventlet workers rather than using +`eventlet`_ directly. Gunicorn provides a much more configurable and +production-tested server. + +`eventlet`_ allows writing asynchronous, coroutine-based code that looks +like standard synchronous Python. It uses `greenlet`_ to enable task +switching without writing ``async/await`` or using ``asyncio``. + +:doc:`gevent` is another library that does the same thing. Certain +dependencies you have, or other considerations, may affect which of the +two you choose to use. + +eventlet provides a WSGI server that can handle many connections at once +instead of one per worker process. You must actually use eventlet in +your own code to see any benefit to using the server. + +.. _eventlet: https://eventlet.net/ +.. _greenlet: https://greenlet.readthedocs.io/en/latest/ + + +Installing +---------- + +When using eventlet, greenlet>=1.0 is required, otherwise context locals +such as ``request`` will not work as expected. When using PyPy, +PyPy>=7.3.7 is required. + +Create a virtualenv, install your application, then install +``eventlet``. + +.. code-block:: text + + $ cd hello-app + $ python -m venv venv + $ . venv/bin/activate + $ pip install . # install your application + $ pip install eventlet + + +Running +------- + +To use eventlet to serve your application, write a script that imports +its ```wsgi.server``, as well as your app or app factory. + +.. code-block:: python + :caption: ``wsgi.py`` + + import eventlet + from eventlet import wsgi + from hello import create_app + + app = create_app() + wsgi.server(eventlet.listen(("127.0.0.1", 8000), app) + +.. code-block:: text + + $ python wsgi.py + (x) wsgi starting up on http://127.0.0.1:8000 + + +Binding Externally +------------------ + +eventlet should not be run as root because it would cause your +application code to run as root, which is not secure. However, this +means it will not be possible to bind to port 80 or 443. Instead, a +reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used +in front of eventlet. + +You can bind to all external IPs on a non-privileged port by using +``0.0.0.0`` in the server arguments shown in the previous section. +Don't do this when using a reverse proxy setup, otherwise it will be +possible to bypass the proxy. + +``0.0.0.0`` is not a valid address to navigate to, you'd use a specific +IP address in your browser. diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst deleted file mode 100644 index d3614d377d..0000000000 --- a/docs/deploying/fastcgi.rst +++ /dev/null @@ -1,238 +0,0 @@ -FastCGI -======= - -FastCGI is a deployment option on servers like `nginx`_, `lighttpd`_, and -`cherokee`_; see :doc:`uwsgi` and :doc:`wsgi-standalone` for other options. -To use your WSGI application with any of them you will need a FastCGI -server first. The most popular one is `flup`_ which we will use for -this guide. Make sure to have it installed to follow along. - -.. admonition:: Watch Out - - Please make sure in advance that any ``app.run()`` calls you might - have in your application file are inside an ``if __name__ == - '__main__':`` block or moved to a separate file. Just make sure it's - not called because this will always start a local WSGI server which - we do not want if we deploy that application to FastCGI. - -Creating a `.fcgi` file ------------------------ - -First you need to create the FastCGI server file. Let's call it -`yourapplication.fcgi`:: - - #!/usr/bin/python - from flup.server.fcgi import WSGIServer - from yourapplication import app - - if __name__ == '__main__': - WSGIServer(app).run() - -This is enough for Apache to work, however nginx and older versions of -lighttpd need a socket to be explicitly passed to communicate with the -FastCGI server. For that to work you need to pass the path to the -socket to the :class:`~flup.server.fcgi.WSGIServer`:: - - WSGIServer(application, bindAddress='/path/to/fcgi.sock').run() - -The path has to be the exact same path you define in the server -config. - -Save the :file:`yourapplication.fcgi` file somewhere you will find it again. -It makes sense to have that in :file:`/var/www/yourapplication` or something -similar. - -Make sure to set the executable bit on that file so that the servers -can execute it: - -.. sourcecode:: text - - $ chmod +x /var/www/yourapplication/yourapplication.fcgi - -Configuring Apache ------------------- - -The example above is good enough for a basic Apache deployment but your -`.fcgi` file will appear in your application URL e.g. -``example.com/yourapplication.fcgi/news/``. There are few ways to configure -your application so that yourapplication.fcgi does not appear in the URL. -A preferable way is to use the ScriptAlias and SetHandler configuration -directives to route requests to the FastCGI server. The following example -uses FastCgiServer to start 5 instances of the application which will -handle all incoming requests:: - - LoadModule fastcgi_module /usr/lib64/httpd/modules/mod_fastcgi.so - - FastCgiServer /var/www/html/yourapplication/app.fcgi -idle-timeout 300 -processes 5 - - - ServerName webapp1.mydomain.com - DocumentRoot /var/www/html/yourapplication - - AddHandler fastcgi-script fcgi - ScriptAlias / /var/www/html/yourapplication/app.fcgi/ - - - SetHandler fastcgi-script - - - -These processes will be managed by Apache. If you're using a standalone -FastCGI server, you can use the FastCgiExternalServer directive instead. -Note that in the following the path is not real, it's simply used as an -identifier to other -directives such as AliasMatch:: - - FastCgiServer /var/www/html/yourapplication -host 127.0.0.1:3000 - -If you cannot set ScriptAlias, for example on a shared web host, you can use -WSGI middleware to remove yourapplication.fcgi from the URLs. Set .htaccess:: - - - AddHandler fcgid-script .fcgi - - SetHandler fcgid-script - Options +FollowSymLinks +ExecCGI - - - - - Options +FollowSymlinks - RewriteEngine On - RewriteBase / - RewriteCond %{REQUEST_FILENAME} !-f - RewriteRule ^(.*)$ yourapplication.fcgi/$1 [QSA,L] - - -Set yourapplication.fcgi:: - - #!/usr/bin/python - #: optional path to your local python site-packages folder - import sys - sys.path.insert(0, '/lib/python/site-packages') - - from flup.server.fcgi import WSGIServer - from yourapplication import app - - class ScriptNameStripper(object): - def __init__(self, app): - self.app = app - - def __call__(self, environ, start_response): - environ['SCRIPT_NAME'] = '' - return self.app(environ, start_response) - - app = ScriptNameStripper(app) - - if __name__ == '__main__': - WSGIServer(app).run() - -Configuring lighttpd --------------------- - -A basic FastCGI configuration for lighttpd looks like that:: - - fastcgi.server = ("/yourapplication.fcgi" => - (( - "socket" => "/tmp/yourapplication-fcgi.sock", - "bin-path" => "/var/www/yourapplication/yourapplication.fcgi", - "check-local" => "disable", - "max-procs" => 1 - )) - ) - - alias.url = ( - "/static/" => "/path/to/your/static/" - ) - - url.rewrite-once = ( - "^(/static($|/.*))$" => "$1", - "^(/.*)$" => "/yourapplication.fcgi$1" - ) - -Remember to enable the FastCGI, alias and rewrite modules. This configuration -binds the application to ``/yourapplication``. If you want the application to -work in the URL root you have to work around a lighttpd bug with the -:class:`~werkzeug.contrib.fixers.LighttpdCGIRootFix` middleware. - -Make sure to apply it only if you are mounting the application the URL -root. Also, see the Lighty docs for more information on `FastCGI and Python -`_ (note that -explicitly passing a socket to run() is no longer necessary). - -Configuring nginx ------------------ - -Installing FastCGI applications on nginx is a bit different because by -default no FastCGI parameters are forwarded. - -A basic Flask FastCGI configuration for nginx looks like this:: - - location = /yourapplication { rewrite ^ /yourapplication/ last; } - location /yourapplication { try_files $uri @yourapplication; } - location @yourapplication { - include fastcgi_params; - fastcgi_split_path_info ^(/yourapplication)(.*)$; - fastcgi_param PATH_INFO $fastcgi_path_info; - fastcgi_param SCRIPT_NAME $fastcgi_script_name; - fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; - } - -This configuration binds the application to ``/yourapplication``. If you -want to have it in the URL root it's a bit simpler because you don't -have to figure out how to calculate ``PATH_INFO`` and ``SCRIPT_NAME``:: - - location / { try_files $uri @yourapplication; } - location @yourapplication { - include fastcgi_params; - fastcgi_param PATH_INFO $fastcgi_script_name; - fastcgi_param SCRIPT_NAME ""; - fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; - } - -Running FastCGI Processes -------------------------- - -Since nginx and others do not load FastCGI apps, you have to do it by -yourself. `Supervisor can manage FastCGI processes. -`_ -You can look around for other FastCGI process managers or write a script -to run your `.fcgi` file at boot, e.g. using a SysV ``init.d`` script. -For a temporary solution, you can always run the ``.fcgi`` script inside -GNU screen. See ``man screen`` for details, and note that this is a -manual solution which does not persist across system restart:: - - $ screen - $ /var/www/yourapplication/yourapplication.fcgi - -Debugging ---------- - -FastCGI deployments tend to be hard to debug on most web servers. Very -often the only thing the server log tells you is something along the -lines of "premature end of headers". In order to debug the application -the only thing that can really give you ideas why it breaks is switching -to the correct user and executing the application by hand. - -This example assumes your application is called `application.fcgi` and -that your web server user is `www-data`:: - - $ su www-data - $ cd /var/www/yourapplication - $ python application.fcgi - Traceback (most recent call last): - File "yourapplication.fcgi", line 4, in - ImportError: No module named yourapplication - -In this case the error seems to be "yourapplication" not being on the -python path. Common problems are: - -- Relative paths being used. Don't rely on the current working directory. -- The code depending on environment variables that are not set by the - web server. -- Different python interpreters being used. - -.. _nginx: https://nginx.org/ -.. _lighttpd: https://www.lighttpd.net/ -.. _cherokee: https://cherokee-project.com/ -.. _flup: https://pypi.org/project/flup/ diff --git a/docs/deploying/gevent.rst b/docs/deploying/gevent.rst new file mode 100644 index 0000000000..aae63e89e8 --- /dev/null +++ b/docs/deploying/gevent.rst @@ -0,0 +1,80 @@ +gevent +====== + +Prefer using :doc:`gunicorn` or :doc:`uwsgi` with gevent workers rather +than using `gevent`_ directly. Gunicorn and uWSGI provide much more +configurable and production-tested servers. + +`gevent`_ allows writing asynchronous, coroutine-based code that looks +like standard synchronous Python. It uses `greenlet`_ to enable task +switching without writing ``async/await`` or using ``asyncio``. + +:doc:`eventlet` is another library that does the same thing. Certain +dependencies you have, or other considerations, may affect which of the +two you choose to use. + +gevent provides a WSGI server that can handle many connections at once +instead of one per worker process. You must actually use gevent in your +own code to see any benefit to using the server. + +.. _gevent: https://www.gevent.org/ +.. _greenlet: https://greenlet.readthedocs.io/en/latest/ + + +Installing +---------- + +When using gevent, greenlet>=1.0 is required, otherwise context locals +such as ``request`` will not work as expected. When using PyPy, +PyPy>=7.3.7 is required. + +Create a virtualenv, install your application, then install ``gevent``. + +.. code-block:: text + + $ cd hello-app + $ python -m venv venv + $ . venv/bin/activate + $ pip install . # install your application + $ pip install gevent + + +Running +------- + +To use gevent to serve your application, write a script that imports its +``WSGIServer``, as well as your app or app factory. + +.. code-block:: python + :caption: ``wsgi.py`` + + from gevent.pywsgi import WSGIServer + from hello import create_app + + app = create_app() + http_server = WSGIServer(("127.0.0.1", 8000), app) + http_server.serve_forever() + +.. code-block:: text + + $ python wsgi.py + +No output is shown when the server starts. + + +Binding Externally +------------------ + +gevent should not be run as root because it would cause your +application code to run as root, which is not secure. However, this +means it will not be possible to bind to port 80 or 443. Instead, a +reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used +in front of gevent. + +You can bind to all external IPs on a non-privileged port by using +``0.0.0.0`` in the server arguments shown in the previous section. Don't +do this when using a reverse proxy setup, otherwise it will be possible +to bypass the proxy. + +``0.0.0.0`` is not a valid address to navigate to, you'd use a specific +IP address in your browser. diff --git a/docs/deploying/gunicorn.rst b/docs/deploying/gunicorn.rst new file mode 100644 index 0000000000..93d11d3964 --- /dev/null +++ b/docs/deploying/gunicorn.rst @@ -0,0 +1,130 @@ +Gunicorn +======== + +`Gunicorn`_ is a pure Python WSGI server with simple configuration and +multiple worker implementations for performance tuning. + +* It tends to integrate easily with hosting platforms. +* It does not support Windows (but does run on WSL). +* It is easy to install as it does not require additional dependencies + or compilation. +* It has built-in async worker support using gevent or eventlet. + +This page outlines the basics of running Gunicorn. Be sure to read its +`documentation`_ and use ``gunicorn --help`` to understand what features +are available. + +.. _Gunicorn: https://gunicorn.org/ +.. _documentation: https://docs.gunicorn.org/ + + +Installing +---------- + +Gunicorn is easy to install, as it does not require external +dependencies or compilation. It runs on Windows only under WSL. + +Create a virtualenv, install your application, then install +``gunicorn``. + +.. code-block:: text + + $ cd hello-app + $ python -m venv venv + $ . venv/bin/activate + $ pip install . # install your application + $ pip install gunicorn + + +Running +------- + +The only required argument to Gunicorn tells it how to load your Flask +application. The syntax is ``{module_import}:{app_variable}``. +``module_import`` is the dotted import name to the module with your +application. ``app_variable`` is the variable with the application. It +can also be a function call (with any arguments) if you're using the +app factory pattern. + +.. code-block:: text + + # equivalent to 'from hello import app' + $ gunicorn -w 4 'hello:app' + + # equivalent to 'from hello import create_app; create_app()' + $ gunicorn -w 4 'hello:create_app()' + + Starting gunicorn 20.1.0 + Listening at: http://127.0.0.1:8000 (x) + Using worker: sync + Booting worker with pid: x + Booting worker with pid: x + Booting worker with pid: x + Booting worker with pid: x + +The ``-w`` option specifies the number of processes to run; a starting +value could be ``CPU * 2``. The default is only 1 worker, which is +probably not what you want for the default worker type. + +Logs for each request aren't shown by default, only worker info and +errors are shown. To show access logs on stdout, use the +``--access-logfile=-`` option. + + +Binding Externally +------------------ + +Gunicorn should not be run as root because it would cause your +application code to run as root, which is not secure. However, this +means it will not be possible to bind to port 80 or 443. Instead, a +reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used +in front of Gunicorn. + +You can bind to all external IPs on a non-privileged port using the +``-b 0.0.0.0`` option. Don't do this when using a reverse proxy setup, +otherwise it will be possible to bypass the proxy. + +.. code-block:: text + + $ gunicorn -w 4 -b 0.0.0.0 'hello:create_app()' + Listening at: http://0.0.0.0:8000 (x) + +``0.0.0.0`` is not a valid address to navigate to, you'd use a specific +IP address in your browser. + + +Async with gevent or eventlet +----------------------------- + +The default sync worker is appropriate for many use cases. If you need +asynchronous support, Gunicorn provides workers using either `gevent`_ +or `eventlet`_. This is not the same as Python's ``async/await``, or the +ASGI server spec. You must actually use gevent/eventlet in your own code +to see any benefit to using the workers. + +When using either gevent or eventlet, greenlet>=1.0 is required, +otherwise context locals such as ``request`` will not work as expected. +When using PyPy, PyPy>=7.3.7 is required. + +To use gevent: + +.. code-block:: text + + $ gunicorn -k gevent 'hello:create_app()' + Starting gunicorn 20.1.0 + Listening at: http://127.0.0.1:8000 (x) + Using worker: gevent + Booting worker with pid: x + +To use eventlet: + +.. code-block:: text + + $ gunicorn -k eventlet 'hello:create_app()' + Starting gunicorn 20.1.0 + Listening at: http://127.0.0.1:8000 (x) + Using worker: eventlet + Booting worker with pid: x + +.. _gevent: https://www.gevent.org/ +.. _eventlet: https://eventlet.net/ diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index e1ed926944..2e48682a5d 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -1,35 +1,80 @@ -Deployment Options -================== +Deploying to Production +======================= -While lightweight and easy to use, **Flask's built-in server is not suitable -for production** as it doesn't scale well. Some of the options available for -properly running Flask in production are documented here. +After developing your application, you'll want to make it available +publicly to other users. When you're developing locally, you're probably +using the built-in development server, debugger, and reloader. These +should not be used in production. Instead, you should use a dedicated +WSGI server or hosting platform, some of which will be described here. -If you want to deploy your Flask application to a WSGI server not listed here, -look up the server documentation about how to use a WSGI app with it. Just -remember that your :class:`Flask` application object is the actual WSGI -application. +"Production" means "not development", which applies whether you're +serving your application publicly to millions of users or privately / +locally to a single user. **Do not use the development server when +deploying to production. It is intended for use only during local +development. It is not designed to be particularly secure, stable, or +efficient.** +Self-Hosted Options +------------------- -Hosted options --------------- +Flask is a WSGI *application*. A WSGI *server* is used to run the +application, converting incoming HTTP requests to the standard WSGI +environ, and converting outgoing WSGI responses to HTTP responses. -- `Deploying Flask on Heroku `_ -- `Deploying Flask on Google App Engine `_ -- `Deploying Flask on Google Cloud Run `_ -- `Deploying Flask on AWS Elastic Beanstalk `_ -- `Deploying on Azure (IIS) `_ -- `Deploying on PythonAnywhere `_ +The primary goal of these docs is to familiarize you with the concepts +involved in running a WSGI application using a production WSGI server +and HTTP server. There are many WSGI servers and HTTP servers, with many +configuration possibilities. The pages below discuss the most common +servers, and show the basics of running each one. The next section +discusses platforms that can manage this for you. -Self-hosted options -------------------- +.. toctree:: + :maxdepth: 1 + + gunicorn + waitress + mod_wsgi + uwsgi + gevent + eventlet + asgi + +WSGI servers have HTTP servers built-in. However, a dedicated HTTP +server may be safer, more efficient, or more capable. Putting an HTTP +server in front of the WSGI server is called a "reverse proxy." .. toctree:: - :maxdepth: 2 - - wsgi-standalone - uwsgi - mod_wsgi - fastcgi - cgi - asgi + :maxdepth: 1 + + proxy_fix + nginx + apache-httpd + +This list is not exhaustive, and you should evaluate these and other +servers based on your application's needs. Different servers will have +different capabilities, configuration, and support. + + +Hosting Platforms +----------------- + +There are many services available for hosting web applications without +needing to maintain your own server, networking, domain, etc. Some +services may have a free tier up to a certain time or bandwidth. Many of +these services use one of the WSGI servers described above, or a similar +interface. The links below are for some of the most common platforms, +which have instructions for Flask, WSGI, or Python. + +- `PythonAnywhere `_ +- `Heroku `_ +- `Google App Engine `_ +- `Google Cloud Run `_ +- `AWS Elastic Beanstalk `_ +- `Microsoft Azure `_ + +This list is not exhaustive, and you should evaluate these and other +services based on your application's needs. Different services will have +different capabilities, configuration, pricing, and support. + +You'll probably need to :doc:`proxy_fix` when using most hosting +platforms. diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index 801dfa5c1f..5ae5e2cf43 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -1,216 +1,105 @@ -mod_wsgi (Apache) -================= +mod_wsgi +======== -If you are using the `Apache`_ webserver, consider using `mod_wsgi`_. +`mod_wsgi`_ is a WSGI server integrated with the `Apache httpd`_ server. +The modern `mod_wsgi-express`_ command makes it easy to configure and +start the server without needing to write Apache httpd configuration. -.. admonition:: Watch Out +* Tightly integrated with Apache httpd. +* Supports Windows directly. +* Requires a compiler to install. Requires Apache installed separately + on Windows. +* Does not require a reverse proxy setup. - Please make sure in advance that any ``app.run()`` calls you might - have in your application file are inside an ``if __name__ == - '__main__':`` block or moved to a separate file. Just make sure it's - not called because this will always start a local WSGI server which - we do not want if we deploy that application to mod_wsgi. +This page outlines the basics of running mod_wsgi-express, not the more +complex installation and configuration with httpd. Be sure to read the +`mod_wsgi-express`_, `mod_wsgi`_, and `Apache httpd`_ documentation to +understand what features are available. -.. _Apache: https://httpd.apache.org/ +.. _mod_wsgi-express: https://pypi.org/project/mod-wsgi/ +.. _mod_wsgi: https://modwsgi.readthedocs.io/ +.. _Apache httpd: https://httpd.apache.org/ -Installing `mod_wsgi` ---------------------- -If you don't have `mod_wsgi` installed yet you have to either install it -using a package manager or compile it yourself. The mod_wsgi -`installation instructions`_ cover source installations on UNIX systems. +Installing +---------- -If you are using Ubuntu/Debian you can apt-get it and activate it as -follows: +On Linux/Mac, the most straightforward way to install mod_wsgi is to +install the ``mod_wsgi-standalone`` package, which will compile an +up-to-date version of Apache httpd as well. -.. sourcecode:: text +Create a virtualenv, install your application, then install +``mod_wsgi-standalone``. - $ apt-get install libapache2-mod-wsgi-py3 +.. code-block:: text -If you are using a yum based distribution (Fedora, OpenSUSE, etc..) you -can install it as follows: + $ cd hello-app + $ python -m venv venv + $ . venv/bin/activate + $ pip install . # install your application + $ pip install mod_wsgi-standalone -.. sourcecode:: text +If you want to use the system-installed version of Apache httpd +(required on Windows, optional but faster on Linux/Mac), install the +``mod_wsgi`` package instead. You will get an error if Apache and its +development headers are not available. How to install them depends on +what OS and package manager you use. - $ yum install mod_wsgi +.. code-block:: text -On FreeBSD install `mod_wsgi` by compiling the `www/mod_wsgi` port or by -using pkg_add: + $ pip install mod_wsgi -.. sourcecode:: text - $ pkg install ap24-py37-mod_wsgi +Running +------- -If you are using pkgsrc you can install `mod_wsgi` by compiling the -`www/ap2-wsgi` package. +The only argument to ``mod_wsgi-express`` specifies a script containing +your Flask application, which must be called ``application``. You can +write a small script to import your app with this name, or to create it +if using the app factory pattern. -If you encounter segfaulting child processes after the first apache -reload you can safely ignore them. Just restart the server. +.. code-block:: python + :caption: ``wsgi.py`` -Creating a `.wsgi` file ------------------------ + from hello import app -To run your application you need a :file:`yourapplication.wsgi` file. -This file contains the code `mod_wsgi` is executing on startup -to get the application object. The object called `application` -in that file is then used as application. + application = app -For most applications the following file should be sufficient:: +.. code-block:: python + :caption: ``wsgi.py`` - from yourapplication import app as application + from hello import create_app -If a factory function is used in a :file:`__init__.py` file, then the function should be imported:: - - from yourapplication import create_app application = create_app() -If you don't have a factory function for application creation but a singleton -instance you can directly import that one as `application`. - -Store that file somewhere that you will find it again (e.g.: -:file:`/var/www/yourapplication`) and make sure that `yourapplication` and all -the libraries that are in use are on the python load path. If you don't -want to install it system wide consider using a `virtual python`_ -instance. Keep in mind that you will have to actually install your -application into the virtualenv as well. Alternatively there is the -option to just patch the path in the ``.wsgi`` file before the import:: - - import sys - sys.path.insert(0, '/path/to/the/application') - -Configuring Apache ------------------- - -The last thing you have to do is to create an Apache configuration file -for your application. In this example we are telling `mod_wsgi` to -execute the application under a different user for security reasons: - -.. sourcecode:: apache - - - ServerName example.com - - WSGIDaemonProcess yourapplication user=user1 group=group1 threads=5 - WSGIScriptAlias / /var/www/yourapplication/yourapplication.wsgi - - - WSGIProcessGroup yourapplication - WSGIApplicationGroup %{GLOBAL} - Order deny,allow - Allow from all - - - -Note: WSGIDaemonProcess isn't implemented in Windows and Apache will -refuse to run with the above configuration. On a Windows system, eliminate those lines: - -.. sourcecode:: apache - - - ServerName example.com - WSGIScriptAlias / C:\yourdir\yourapp.wsgi - - Order deny,allow - Allow from all - - - -Note: There have been some changes in access control configuration -for `Apache 2.4`_. - -.. _Apache 2.4: https://httpd.apache.org/docs/trunk/upgrading.html - -Most notably, the syntax for directory permissions has changed from httpd 2.2 - -.. sourcecode:: apache - - Order allow,deny - Allow from all +Now run the ``mod_wsgi-express start-server`` command. -to httpd 2.4 syntax +.. code-block:: text -.. sourcecode:: apache + $ mod_wsgi-express start-server wsgi.py --processes 4 - Require all granted +The ``--processes`` option specifies the number of worker processes to +run; a starting value could be ``CPU * 2``. +Logs for each request aren't show in the terminal. If an error occurs, +its information is written to the error log file shown when starting the +server. -For more information consult the `mod_wsgi documentation`_. -.. _mod_wsgi: https://github.com/GrahamDumpleton/mod_wsgi -.. _installation instructions: https://modwsgi.readthedocs.io/en/develop/installation.html -.. _virtual python: https://pypi.org/project/virtualenv/ -.. _mod_wsgi documentation: https://modwsgi.readthedocs.io/en/develop/index.html - -Troubleshooting ---------------- - -If your application does not run, follow this guide to troubleshoot: - -**Problem:** application does not run, errorlog shows SystemExit ignored - You have an ``app.run()`` call in your application file that is not - guarded by an ``if __name__ == '__main__':`` condition. Either - remove that :meth:`~flask.Flask.run` call from the file and move it - into a separate :file:`run.py` file or put it into such an if block. - -**Problem:** application gives permission errors - Probably caused by your application running as the wrong user. Make - sure the folders the application needs access to have the proper - privileges set and the application runs as the correct user - (``user`` and ``group`` parameter to the `WSGIDaemonProcess` - directive) - -**Problem:** application dies with an error on print - Keep in mind that mod_wsgi disallows doing anything with - :data:`sys.stdout` and :data:`sys.stderr`. You can disable this - protection from the config by setting the `WSGIRestrictStdout` to - ``off``: - - .. sourcecode:: apache - - WSGIRestrictStdout Off - - Alternatively you can also replace the standard out in the .wsgi file - with a different stream:: - - import sys - sys.stdout = sys.stderr - -**Problem:** accessing resources gives IO errors - Your application probably is a single .py file you symlinked into - the site-packages folder. Please be aware that this does not work, - instead you either have to put the folder into the pythonpath the - file is stored in, or convert your application into a package. - - The reason for this is that for non-installed packages, the module - filename is used to locate the resources and for symlinks the wrong - filename is picked up. - -Support for Automatic Reloading -------------------------------- - -To help deployment tools you can activate support for automatic -reloading. Whenever something changes the ``.wsgi`` file, `mod_wsgi` will -reload all the daemon processes for us. - -For that, just add the following directive to your `Directory` section: - -.. sourcecode:: apache - - WSGIScriptReloading On - -Working with Virtual Environments ---------------------------------- +Binding Externally +------------------ -Virtual environments have the advantage that they never install the -required dependencies system wide so you have a better control over what -is used where. If you want to use a virtual environment with mod_wsgi -you have to modify your ``.wsgi`` file slightly. +Unlike the other WSGI servers in these docs, mod_wsgi can be run as +root to bind to privileged ports like 80 and 443. However, it must be +configured to drop permissions to a different user and group for the +worker processes. -Add the following lines to the top of your ``.wsgi`` file:: +For example, if you created a ``hello`` user and group, you should +install your virtualenv and application as that user, then tell +mod_wsgi to drop to that user after starting. - activate_this = '/path/to/env/bin/activate_this.py' - with open(activate_this) as file_: - exec(file_.read(), dict(__file__=activate_this)) +.. code-block:: text -This sets up the load paths according to the settings of the virtual -environment. Keep in mind that the path has to be absolute. + $ sudo /home/hello/venv/bin/mod_wsgi-express start-server \ + /home/hello/wsgi.py \ + --user hello --group hello --port 80 --processes 4 diff --git a/docs/deploying/nginx.rst b/docs/deploying/nginx.rst new file mode 100644 index 0000000000..6b25c073e8 --- /dev/null +++ b/docs/deploying/nginx.rst @@ -0,0 +1,69 @@ +nginx +===== + +`nginx`_ is a fast, production level HTTP server. When serving your +application with one of the WSGI servers listed in :doc:`index`, it is +often good or necessary to put a dedicated HTTP server in front of it. +This "reverse proxy" can handle incoming requests, TLS, and other +security and performance concerns better than the WSGI server. + +Nginx can be installed using your system package manager, or a pre-built +executable for Windows. Installing and running Nginx itself is outside +the scope of this doc. This page outlines the basics of configuring +Nginx to proxy your application. Be sure to read its documentation to +understand what features are available. + +.. _nginx: https://nginx.org/ + + +Domain Name +----------- + +Acquiring and configuring a domain name is outside the scope of this +doc. In general, you will buy a domain name from a registrar, pay for +server space with a hosting provider, and then point your registrar +at the hosting provider's name servers. + +To simulate this, you can also edit your ``hosts`` file, located at +``/etc/hosts`` on Linux. Add a line that associates a name with the +local IP. + +Modern Linux systems may be configured to treat any domain name that +ends with ``.localhost`` like this without adding it to the ``hosts`` +file. + +.. code-block:: python + :caption: ``/etc/hosts`` + + 127.0.0.1 hello.localhost + + +Configuration +------------- + +The nginx configuration is located at ``/etc/nginx/nginx.conf`` on +Linux. It may be different depending on your operating system. Check the +docs and look for ``nginx.conf``. + +Remove or comment out any existing ``server`` section. Add a ``server`` +section and use the ``proxy_pass`` directive to point to the address the +WSGI server is listening on. We'll assume the WSGI server is listening +locally at ``http://127.0.0.1:8000``. + +.. code-block:: nginx + :caption: ``/etc/nginx.conf`` + + server { + listen 80; + server_name _; + + location / { + proxy_pass http://127.0.0.1:8000/; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Prefix /; + } + } + +Then :doc:`proxy_fix` so that your application uses these headers. diff --git a/docs/deploying/proxy_fix.rst b/docs/deploying/proxy_fix.rst new file mode 100644 index 0000000000..e2c42e8257 --- /dev/null +++ b/docs/deploying/proxy_fix.rst @@ -0,0 +1,33 @@ +Tell Flask it is Behind a Proxy +=============================== + +When using a reverse proxy, or many Python hosting platforms, the proxy +will intercept and forward all external requests to the local WSGI +server. + +From the WSGI server and Flask application's perspectives, requests are +now coming from the HTTP server to the local address, rather than from +the remote address to the external server address. + +HTTP servers should set ``X-Forwarded-`` headers to pass on the real +values to the application. The application can then be told to trust and +use those values by wrapping it with the +:doc:`werkzeug:middleware/proxy_fix` middleware provided by Werkzeug. + +This middleware should only be used if the application is actually +behind a proxy, and should be configured with the number of proxies that +are chained in front of it. Not all proxies set all the headers. Since +incoming headers can be faked, you must set how many proxies are setting +each header so the middleware knows what to trust. + +.. code-block:: python + + from werkzeug.middleware.proxy_fix import ProxyFix + + app.wsgi_app = ProxyFix( + app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 + ) + +Remember, only apply this middleware if you are behind a proxy, and set +the correct number of proxies that set each header. It can be a security +issue if you get this configuration wrong. diff --git a/docs/deploying/uwsgi.rst b/docs/deploying/uwsgi.rst index b6958dc09d..2da5efe2d9 100644 --- a/docs/deploying/uwsgi.rst +++ b/docs/deploying/uwsgi.rst @@ -1,71 +1,145 @@ uWSGI ===== -uWSGI is a deployment option on servers like `nginx`_, `lighttpd`_, and -`cherokee`_; see :doc:`fastcgi` and :doc:`wsgi-standalone` for other options. -To use your WSGI application with uWSGI protocol you will need a uWSGI server -first. uWSGI is both a protocol and an application server; the application -server can serve uWSGI, FastCGI, and HTTP protocols. +`uWSGI`_ is a fast, compiled server suite with extensive configuration +and capabilities beyond a basic server. -The most popular uWSGI server is `uwsgi`_, which we will use for this -guide. Make sure to have it installed to follow along. +* It can be very performant due to being a compiled program. +* It is complex to configure beyond the basic application, and has so + many options that it can be difficult for beginners to understand. +* It does not support Windows (but does run on WSL). +* It requires a compiler to install in some cases. -.. admonition:: Watch Out +This page outlines the basics of running uWSGI. Be sure to read its +documentation to understand what features are available. - Please make sure in advance that any ``app.run()`` calls you might - have in your application file are inside an ``if __name__ == - '__main__':`` block or moved to a separate file. Just make sure it's - not called because this will always start a local WSGI server which - we do not want if we deploy that application to uWSGI. +.. _uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/ -Starting your app with uwsgi ----------------------------- -`uwsgi` is designed to operate on WSGI callables found in python modules. +Installing +---------- -Given a flask application in myapp.py, use the following command: +uWSGI has multiple ways to install it. The most straightforward is to +install the ``pyuwsgi`` package, which provides precompiled wheels for +common platforms. However, it does not provide SSL support, which can be +provided with a reverse proxy instead. -.. sourcecode:: text +Create a virtualenv, install your application, then install ``pyuwsgi``. - $ uwsgi -s /tmp/yourapplication.sock --manage-script-name --mount /yourapplication=myapp:app +.. code-block:: text -The ``--manage-script-name`` will move the handling of ``SCRIPT_NAME`` -to uwsgi, since it is smarter about that. -It is used together with the ``--mount`` directive which will make -requests to ``/yourapplication`` be directed to ``myapp:app``. -If your application is accessible at root level, you can use a -single ``/`` instead of ``/yourapplication``. ``myapp`` refers to the name of -the file of your flask application (without extension) or the module which -provides ``app``. ``app`` is the callable inside of your application (usually -the line reads ``app = Flask(__name__)``). + $ cd hello-app + $ python -m venv venv + $ . venv/bin/activate + $ pip install . # install your application + $ pip install pyuwsgi -If you want to deploy your flask application inside of a virtual environment, -you need to also add ``--virtualenv /path/to/virtual/environment``. You might -also need to add ``--plugin python`` or ``--plugin python3`` depending on which -python version you use for your project. +If you have a compiler available, you can install the ``uwsgi`` package +instead. Or install the ``pyuwsgi`` package from sdist instead of wheel. +Either method will include SSL support. -Configuring nginx +.. code-block:: text + + $ pip install uwsgi + + # or + $ pip install --no-binary pyuwsgi pyuwsgi + + +Running +------- + +The most basic way to run uWSGI is to tell it to start an HTTP server +and import your application. + +.. code-block:: text + + $ uwsgi --http 127.0.0.1:8000 --master -p 4 -w hello:app + + *** Starting uWSGI 2.0.20 (64bit) on [x] *** + *** Operational MODE: preforking *** + mounting hello:app on / + spawned uWSGI master process (pid: x) + spawned uWSGI worker 1 (pid: x, cores: 1) + spawned uWSGI worker 2 (pid: x, cores: 1) + spawned uWSGI worker 3 (pid: x, cores: 1) + spawned uWSGI worker 4 (pid: x, cores: 1) + spawned uWSGI http 1 (pid: x) + +If you're using the app factory pattern, you'll need to create a small +Python file to create the app, then point uWSGI at that. + +.. code-block:: python + :caption: ``wsgi.py`` + + from hello import create_app + + app = create_app() + +.. code-block:: text + + $ uwsgi --http 127.0.0.1:8000 --master -p 4 -w wsgi:app + +The ``--http`` option starts an HTTP server at 127.0.0.1 port 8000. The +``--master`` option specifies the standard worker manager. The ``-p`` +option starts 4 worker processes; a starting value could be ``CPU * 2``. +The ``-w`` option tells uWSGI how to import your application + + +Binding Externally +------------------ + +uWSGI should not be run as root with the configuration shown in this doc +because it would cause your application code to run as root, which is +not secure. However, this means it will not be possible to bind to port +80 or 443. Instead, a reverse proxy such as :doc:`nginx` or +:doc:`apache-httpd` should be used in front of uWSGI. It is possible to +run uWSGI as root securely, but that is beyond the scope of this doc. + +uWSGI has optimized integration with `Nginx uWSGI`_ and +`Apache mod_proxy_uwsgi`_, and possibly other servers, instead of using +a standard HTTP proxy. That configuration is beyond the scope of this +doc, see the links for more information. + +.. _Nginx uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/Nginx.html +.. _Apache mod_proxy_uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/Apache.html#mod-proxy-uwsgi + +You can bind to all external IPs on a non-privileged port using the +``--http 0.0.0.0:8000`` option. Don't do this when using a reverse proxy +setup, otherwise it will be possible to bypass the proxy. + +.. code-block:: text + + $ uwsgi --http 0.0.0.0:8000 --master -p 4 -w wsgi:app + +``0.0.0.0`` is not a valid address to navigate to, you'd use a specific +IP address in your browser. + + +Async with gevent ----------------- -A basic flask nginx configuration looks like this:: +The default sync worker is appropriate for many use cases. If you need +asynchronous support, uWSGI provides a `gevent`_ worker. This is not the +same as Python's ``async/await``, or the ASGI server spec. You must +actually use gevent in your own code to see any benefit to using the +worker. + +When using gevent, greenlet>=1.0 is required, otherwise context locals +such as ``request`` will not work as expected. When using PyPy, +PyPy>=7.3.7 is required. + +.. code-block:: text - location = /yourapplication { rewrite ^ /yourapplication/; } - location /yourapplication { try_files $uri @yourapplication; } - location @yourapplication { - include uwsgi_params; - uwsgi_pass unix:/tmp/yourapplication.sock; - } + $ uwsgi --http 127.0.0.1:8000 --master --gevent 100 -w wsgi:app -This configuration binds the application to ``/yourapplication``. If you want -to have it in the URL root its a bit simpler:: + *** Starting uWSGI 2.0.20 (64bit) on [x] *** + *** Operational MODE: async *** + mounting hello:app on / + spawned uWSGI master process (pid: x) + spawned uWSGI worker 1 (pid: x, cores: 100) + spawned uWSGI http 1 (pid: x) + *** running gevent loop engine [addr:x] *** - location / { try_files $uri @yourapplication; } - location @yourapplication { - include uwsgi_params; - uwsgi_pass unix:/tmp/yourapplication.sock; - } -.. _nginx: https://nginx.org/ -.. _lighttpd: https://www.lighttpd.net/ -.. _cherokee: https://cherokee-project.com/ -.. _uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/ +.. _gevent: https://www.gevent.org/ diff --git a/docs/deploying/waitress.rst b/docs/deploying/waitress.rst new file mode 100644 index 0000000000..9b2fe13f93 --- /dev/null +++ b/docs/deploying/waitress.rst @@ -0,0 +1,75 @@ +Waitress +======== + +`Waitress`_ is a pure Python WSGI server. + +* It is easy to configure. +* It supports Windows directly. +* It is easy to install as it does not require additional dependencies + or compilation. +* It does not support streaming requests, full request data is always + buffered. +* It uses a single process with multiple thread workers. + +This page outlines the basics of running Waitress. Be sure to read its +documentation and ``waitress-serve --help`` to understand what features +are available. + +.. _Waitress: https://docs.pylonsproject.org/projects/waitress/ + + +Installing +---------- + +Create a virtualenv, install your application, then install +``waitress``. + +.. code-block:: text + + $ cd hello-app + $ python -m venv venv + $ . venv/bin/activate + $ pip install . # install your application + $ pip install waitress + + +Running +------- + +The only required argument to ``waitress-serve`` tells it how to load +your Flask application. The syntax is ``{module}:{app}``. ``module`` is +the dotted import name to the module with your application. ``app`` is +the variable with the application. If you're using the app factory +pattern, use ``--call {module}:{factory}`` instead. + +.. code-block:: text + + # equivalent to 'from hello import app' + $ waitress-serve hello:app --host 127.0.0.1 + + # equivalent to 'from hello import create_app; create_app()' + $ waitress-serve --call hello:create_app --host 127.0.0.1 + + Serving on http://127.0.0.1:8080 + +The ``--host`` option binds the server to local ``127.0.0.1`` only. + +Logs for each request aren't shown, only errors are shown. Logging can +be configured through the Python interface instead of the command line. + + +Binding Externally +------------------ + +Waitress should not be run as root because it would cause your +application code to run as root, which is not secure. However, this +means it will not be possible to bind to port 80 or 443. Instead, a +reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used +in front of Waitress. + +You can bind to all external IPs on a non-privileged port by not +specifying the ``--host`` option. Don't do this when using a revers +proxy setup, otherwise it will be possible to bypass the proxy. + +``0.0.0.0`` is not a valid address to navigate to, you'd use a specific +IP address in your browser. diff --git a/docs/deploying/wsgi-standalone.rst b/docs/deploying/wsgi-standalone.rst deleted file mode 100644 index e66eacbaa3..0000000000 --- a/docs/deploying/wsgi-standalone.rst +++ /dev/null @@ -1,262 +0,0 @@ -Standalone WSGI Servers -======================= - -Most WSGI servers also provide HTTP servers, so they can run a WSGI -application and make it available externally. - -It may still be a good idea to run the server behind a dedicated HTTP -server such as Apache or Nginx. See :ref:`deploying-proxy-setups` if you -run into issues with that. - - -Gunicorn --------- - -`Gunicorn`_ is a WSGI and HTTP server for UNIX. To run a Flask -application, tell Gunicorn how to import your Flask app object. - -.. code-block:: text - - $ gunicorn -w 4 -b 0.0.0.0:5000 your_project:app - -The ``-w 4`` option uses 4 workers to handle 4 requests at once. The -``-b 0.0.0.0:5000`` serves the application on all interfaces on port -5000. - -Gunicorn provides many options for configuring the server, either -through a configuration file or with command line options. Use -``gunicorn --help`` or see the docs for more information. - -The command expects the name of your module or package to import and -the application instance within the module. If you use the application -factory pattern, you can pass a call to that. - -.. code-block:: text - - $ gunicorn -w 4 -b 0.0.0.0:5000 "myproject:create_app()" - - -Async with Gevent or Eventlet -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The default sync worker is appropriate for many use cases. If you need -asynchronous support, Gunicorn provides workers using either `gevent`_ -or `eventlet`_. This is not the same as Python's ``async/await``, or the -ASGI server spec. - -When using either gevent or eventlet, greenlet>=1.0 is required, -otherwise context locals such as ``request`` will not work as expected. -When using PyPy, PyPy>=7.3.7 is required. - -To use gevent: - -.. code-block:: text - - $ gunicorn -k gevent -b 0.0.0.0:5000 your_project:app - -To use eventlet: - -.. code-block:: text - - $ gunicorn -k eventlet -b 0.0.0.0:5000 your_project:app - - -.. _Gunicorn: https://gunicorn.org/ -.. _gevent: http://www.gevent.org/ -.. _eventlet: https://eventlet.net/ -.. _greenlet: https://greenlet.readthedocs.io/en/latest/ - - -uWSGI ------ - -`uWSGI`_ is a fast application server written in C. It is very -configurable, which makes it more complicated to setup than Gunicorn. -It also provides many other utilities for writing robust web -applications. To run a Flask application, tell uWSGI how to import -your Flask app object. - -.. code-block:: text - - $ uwsgi --master -p 4 --http 0.0.0.0:5000 -w your_project:app - -The ``-p 4`` option uses 4 workers to handle 4 requests at once. The -``--http 0.0.0.0:5000`` serves the application on all interfaces on port -5000. - -uWSGI has optimized integration with Nginx and Apache instead of using -a standard HTTP proxy. See :doc:`configuring uWSGI and Nginx `. - - -Async with Gevent -~~~~~~~~~~~~~~~~~ - -The default sync worker is appropriate for many use cases. If you need -asynchronous support, uWSGI provides workers using `gevent`_. It also -supports other async modes, see the docs for more information. This is -not the same as Python's ``async/await``, or the ASGI server spec. - -When using gevent, greenlet>=1.0 is required, otherwise context locals -such as ``request`` will not work as expected. When using PyPy, -PyPy>=7.3.7 is required. - -.. code-block:: text - - $ uwsgi --master --gevent 100 --http 0.0.0.0:5000 -w your_project:app - -.. _uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/ - - -Gevent ------- - -Prefer using `Gunicorn`_ with Gevent workers rather than using Gevent -directly. Gunicorn provides a much more configurable and -production-tested server. See the section on Gunicorn above. - -`Gevent`_ allows writing asynchronous, coroutine-based code that looks -like standard synchronous Python. It uses `greenlet`_ to enable task -switching without writing ``async/await`` or using ``asyncio``. - -It provides a WSGI server that can handle many connections at once -instead of one per worker process. - -`Eventlet`_, described below, is another library that does the same -thing. Certain dependencies you have, or other consideration, may affect -which of the two you choose to use - -To use gevent to serve your application, import its ``WSGIServer`` and -use it to run your ``app``. - -.. code-block:: python - - from gevent.pywsgi import WSGIServer - from your_project import app - - http_server = WSGIServer(("", 5000), app) - http_server.serve_forever() - - -Eventlet --------- - -Prefer using `Gunicorn`_ with Eventlet workers rather than using -Eventlet directly. Gunicorn provides a much more configurable and -production-tested server. See the section on Gunicorn above. - -`Eventlet`_ allows writing asynchronous, coroutine-based code that looks -like standard synchronous Python. It uses `greenlet`_ to enable task -switching without writing ``async/await`` or using ``asyncio``. - -It provides a WSGI server that can handle many connections at once -instead of one per worker process. - -`Gevent`_, described above, is another library that does the same -thing. Certain dependencies you have, or other consideration, may affect -which of the two you choose to use - -To use eventlet to serve your application, import its ``wsgi.server`` -and use it to run your ``app``. - -.. code-block:: python - - import eventlet - from eventlet import wsgi - from your_project import app - - wsgi.server(eventlet.listen(("", 5000), app) - - -Twisted Web ------------ - -`Twisted Web`_ is the web server shipped with `Twisted`_, a mature, -non-blocking event-driven networking library. Twisted Web comes with a -standard WSGI container which can be controlled from the command line using -the ``twistd`` utility: - -.. code-block:: text - - $ twistd web --wsgi myproject.app - -This example will run a Flask application called ``app`` from a module named -``myproject``. - -Twisted Web supports many flags and options, and the ``twistd`` utility does -as well; see ``twistd -h`` and ``twistd web -h`` for more information. For -example, to run a Twisted Web server in the foreground, on port 8080, with an -application from ``myproject``: - -.. code-block:: text - - $ twistd -n web --port tcp:8080 --wsgi myproject.app - -.. _Twisted: https://twistedmatrix.com/trac/ -.. _Twisted Web: https://twistedmatrix.com/trac/wiki/TwistedWeb - - -.. _deploying-proxy-setups: - -Proxy Setups ------------- - -If you deploy your application using one of these servers behind an HTTP proxy -you will need to rewrite a few headers in order for the application to work. -The two problematic values in the WSGI environment usually are ``REMOTE_ADDR`` -and ``HTTP_HOST``. You can configure your httpd to pass these headers, or you -can fix them in middleware. Werkzeug ships a fixer that will solve some common -setups, but you might want to write your own WSGI middleware for specific -setups. - -Here's a simple nginx configuration which proxies to an application served on -localhost at port 8000, setting appropriate headers: - -.. sourcecode:: nginx - - server { - listen 80; - - server_name _; - - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; - - location / { - proxy_pass http://127.0.0.1:8000/; - proxy_redirect off; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - } - -If your httpd is not providing these headers, the most common setup invokes the -host being set from ``X-Forwarded-Host`` and the remote address from -``X-Forwarded-For``:: - - from werkzeug.middleware.proxy_fix import ProxyFix - app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) - -.. admonition:: Trusting Headers - - Please keep in mind that it is a security issue to use such a middleware in - a non-proxy setup because it will blindly trust the incoming headers which - might be forged by malicious clients. - -If you want to rewrite the headers from another header, you might want to -use a fixer like this:: - - class CustomProxyFix(object): - - def __init__(self, app): - self.app = app - - def __call__(self, environ, start_response): - host = environ.get('HTTP_X_FHOST', '') - if host: - environ['HTTP_HOST'] = host - return self.app(environ, start_response) - - app.wsgi_app = CustomProxyFix(app.wsgi_app) From a9878c018bdecc377788204a3bc93497ec1f050a Mon Sep 17 00:00:00 2001 From: David Lord Date: Sun, 12 Jun 2022 16:03:51 -0700 Subject: [PATCH 045/229] remove mod_wsgi-standalone --- docs/deploying/mod_wsgi.rst | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index 5ae5e2cf43..eae973deb6 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -7,8 +7,7 @@ start the server without needing to write Apache httpd configuration. * Tightly integrated with Apache httpd. * Supports Windows directly. -* Requires a compiler to install. Requires Apache installed separately - on Windows. +* Requires a compiler and the Apache development headers to install. * Does not require a reverse proxy setup. This page outlines the basics of running mod_wsgi-express, not the more @@ -24,12 +23,12 @@ understand what features are available. Installing ---------- -On Linux/Mac, the most straightforward way to install mod_wsgi is to -install the ``mod_wsgi-standalone`` package, which will compile an -up-to-date version of Apache httpd as well. +Installing mod_wsgi requires a compiler and the Apache server and +development headers installed. You will get an error if they are not. +How to install them depends on the OS and package manager that you use. Create a virtualenv, install your application, then install -``mod_wsgi-standalone``. +``mod_wsgi``. .. code-block:: text @@ -37,16 +36,6 @@ Create a virtualenv, install your application, then install $ python -m venv venv $ . venv/bin/activate $ pip install . # install your application - $ pip install mod_wsgi-standalone - -If you want to use the system-installed version of Apache httpd -(required on Windows, optional but faster on Linux/Mac), install the -``mod_wsgi`` package instead. You will get an error if Apache and its -development headers are not available. How to install them depends on -what OS and package manager you use. - -.. code-block:: text - $ pip install mod_wsgi From 9c50b8fc1ca160803f7368fc44f12591da856fd4 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 13 Jun 2022 06:08:45 -0700 Subject: [PATCH 046/229] fix formatting --- docs/deploying/eventlet.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying/eventlet.rst b/docs/deploying/eventlet.rst index 8842ce05aa..243be5ebb3 100644 --- a/docs/deploying/eventlet.rst +++ b/docs/deploying/eventlet.rst @@ -44,7 +44,7 @@ Running ------- To use eventlet to serve your application, write a script that imports -its ```wsgi.server``, as well as your app or app factory. +its ``wsgi.server``, as well as your app or app factory. .. code-block:: python :caption: ``wsgi.py`` From 4f03a769d44f6740490dd4ddf70e094c12a6090c Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 15 Jun 2022 12:45:22 -0700 Subject: [PATCH 047/229] edit some cli messages dev server message doesn't show one of the lines in grey app.run message uses click.secho instead of warning --- src/flask/app.py | 16 ++++++++++++---- src/flask/cli.py | 15 +++++---------- src/flask/debughelpers.py | 15 --------------- 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 65e95623d6..360916dbf5 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -10,6 +10,7 @@ from threading import Lock from types import TracebackType +import click from werkzeug.datastructures import Headers from werkzeug.datastructures import ImmutableDict from werkzeug.exceptions import Aborter @@ -23,6 +24,7 @@ from werkzeug.routing import RequestRedirect from werkzeug.routing import RoutingException from werkzeug.routing import Rule +from werkzeug.serving import is_running_from_reloader from werkzeug.urls import url_quote from werkzeug.utils import redirect as _wz_redirect from werkzeug.wrappers import Response as BaseResponse @@ -908,12 +910,18 @@ def run( The default port is now picked from the ``SERVER_NAME`` variable. """ - # Change this into a no-op if the server is invoked from the - # command line. Have a look at cli.py for more information. + # Ignore this call so that it doesn't start another server if + # the 'flask run' command is used. if os.environ.get("FLASK_RUN_FROM_CLI") == "true": - from .debughelpers import explain_ignored_app_run + if not is_running_from_reloader(): + click.secho( + " * Ignoring a call to 'app.run()', the server is" + " already being run with the 'flask run' command.\n" + " Only call 'app.run()' in an 'if __name__ ==" + ' "__main__"\' guard.', + fg="red", + ) - explain_ignored_app_run() return if get_load_dotenv(load_dotenv): diff --git a/src/flask/cli.py b/src/flask/cli.py index 77c1e25a9c..6e0359d9a8 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -11,6 +11,7 @@ from threading import Thread import click +from werkzeug.serving import is_running_from_reloader from werkzeug.utils import import_string from .globals import current_app @@ -273,7 +274,7 @@ def __init__(self, loader, use_eager_loading=None): self._bg_loading_exc = None if use_eager_loading is None: - use_eager_loading = os.environ.get("WERKZEUG_RUN_MAIN") != "true" + use_eager_loading = not is_running_from_reloader() if use_eager_loading: self._load_unlocked() @@ -546,12 +547,6 @@ def list_commands(self, ctx): return sorted(rv) def main(self, *args, **kwargs): - # Set a global flag that indicates that we were invoked from the - # command line interface. This is detected by Flask.run to make the - # call into a no-op. This is necessary to avoid ugly errors when the - # script that is loaded here also attempts to start a server. - os.environ["FLASK_RUN_FROM_CLI"] = "true" - if get_load_dotenv(self.load_dotenv): load_dotenv() @@ -637,7 +632,7 @@ def show_server_banner(env, debug, app_import_path, eager_loading): """Show extra startup messages the first time the server is run, ignoring the reloader. """ - if os.environ.get("WERKZEUG_RUN_MAIN") == "true": + if is_running_from_reloader(): return if app_import_path is not None: @@ -653,10 +648,10 @@ def show_server_banner(env, debug, app_import_path, eager_loading): if env == "production": click.secho( " WARNING: This is a development server. Do not use it in" - " a production deployment.", + " a production deployment.\n Use a production WSGI server" + " instead.", fg="red", ) - click.secho(" Use a production WSGI server instead.", dim=True) if debug is not None: click.echo(f" * Debug mode: {'on' if debug else 'off'}") diff --git a/src/flask/debughelpers.py b/src/flask/debughelpers.py index 27d378c24a..b1e3ce1bc7 100644 --- a/src/flask/debughelpers.py +++ b/src/flask/debughelpers.py @@ -1,6 +1,4 @@ -import os import typing as t -from warnings import warn from .app import Flask from .blueprints import Blueprint @@ -159,16 +157,3 @@ def explain_template_loading_attempts(app: Flask, template, attempts) -> None: info.append(" See https://flask.palletsprojects.com/blueprints/#templates") app.logger.info("\n".join(info)) - - -def explain_ignored_app_run() -> None: - if os.environ.get("WERKZEUG_RUN_MAIN") != "true": - warn( - Warning( - "Silently ignoring app.run() because the application is" - " run from the flask command line executable. Consider" - ' putting app.run() behind an if __name__ == "__main__"' - " guard to silence this warning." - ), - stacklevel=3, - ) From aa801c431ad1f979bce3ea1afbd88e7796ebfd7e Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 15 Jun 2022 14:07:00 -0700 Subject: [PATCH 048/229] FlaskGroup can be nested --- CHANGES.rst | 3 +++ src/flask/cli.py | 30 ++++++++++++++++++++---------- tests/test_cli.py | 13 +++++++++++++ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index ce85e67340..84ffbf0618 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -25,6 +25,9 @@ Unreleased - Added the ``View.init_every_request`` class attribute. If a view subclass sets this to ``False``, the view will not create a new instance on every request. :issue:`2520`. +- A ``flask.cli.FlaskGroup`` Click group can be nested as a + sub-command in a custom CLI. :issue:`3263` + Version 2.1.3 ------------- diff --git a/src/flask/cli.py b/src/flask/cli.py index 6e0359d9a8..a4e366d7c7 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -5,6 +5,7 @@ import re import sys import traceback +import typing as t from functools import update_wrapper from operator import attrgetter from threading import Lock @@ -478,7 +479,13 @@ def __init__( if add_version_option: params.append(version_option) - AppGroup.__init__(self, params=params, **extra) + if "context_settings" not in extra: + extra["context_settings"] = {} + + extra["context_settings"].setdefault("auto_envvar_prefix", "FLASK") + + super().__init__(params=params, **extra) + self.create_app = create_app self.load_dotenv = load_dotenv self.set_debug_flag = set_debug_flag @@ -546,20 +553,22 @@ def list_commands(self, ctx): return sorted(rv) - def main(self, *args, **kwargs): + def make_context( + self, + info_name: t.Optional[str], + args: t.List[str], + parent: t.Optional[click.Context] = None, + **extra: t.Any, + ) -> click.Context: if get_load_dotenv(self.load_dotenv): load_dotenv() - obj = kwargs.get("obj") - - if obj is None: - obj = ScriptInfo( + 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 ) - kwargs["obj"] = obj - kwargs.setdefault("auto_envvar_prefix", "FLASK") - return super().main(*args, **kwargs) + return super().make_context(info_name, args, parent=parent, **extra) def _path_is_ancestor(path, other): @@ -958,6 +967,7 @@ def routes_command(sort: str, all_methods: bool) -> None: cli = FlaskGroup( + name="flask", help="""\ A general utility script for Flask applications. @@ -973,7 +983,7 @@ def routes_command(sort: str, all_methods: bool) -> None: """.format( cmd="export" if os.name == "posix" else "set", prefix="$ " if os.name == "posix" else "> ", - ) + ), ) diff --git a/tests/test_cli.py b/tests/test_cli.py index c9dd5ade06..7a8e9af9cb 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -388,6 +388,19 @@ def test(): assert result.output == f"{not set_debug_flag}\n" +def test_flaskgroup_nested(app, runner): + cli = click.Group("cli") + flask_group = FlaskGroup(name="flask", create_app=lambda: app) + cli.add_command(flask_group) + + @flask_group.command() + def show(): + click.echo(current_app.name) + + result = runner.invoke(cli, ["flask", "show"]) + assert result.output == "flask_test\n" + + def test_no_command_echo_loading_error(): from flask.cli import cli From 99fa3c36abc03cd5b3407df34dce74e879ea377a Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 17 Jun 2022 07:54:06 -0700 Subject: [PATCH 049/229] add --app, --env, --debug, and --env-file CLI options --- CHANGES.rst | 6 ++ src/flask/cli.py | 239 ++++++++++++++++++++++++++++++++++--------- src/flask/helpers.py | 6 +- 3 files changed, 197 insertions(+), 54 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 84ffbf0618..0dea8e3a0e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -27,6 +27,12 @@ Unreleased instance on every request. :issue:`2520`. - A ``flask.cli.FlaskGroup`` Click group can be nested as a sub-command in a custom CLI. :issue:`3263` +- Add ``--app``, ``--env``, and ``--debug`` options to the ``flask`` + CLI, instead of requiring that they are set through environment + variables. :issue:`2836` +- Add ``--env-file`` option to the ``flask`` CLI. This allows + specifying a dotenv file to load in addition to ``.env`` and + ``.flaskenv``. :issue:`3108` Version 2.1.3 diff --git a/src/flask/cli.py b/src/flask/cli.py index a4e366d7c7..40f1de5496 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import ast import inspect import os @@ -12,6 +14,7 @@ from threading import Thread import click +from click.core import ParameterSource from werkzeug.serving import is_running_from_reloader from werkzeug.utils import import_string @@ -20,6 +23,9 @@ from .helpers import get_env from .helpers import get_load_dotenv +if t.TYPE_CHECKING: + from .app import Flask + class NoAppException(click.UsageError): """Raised if an application cannot be found or loaded.""" @@ -46,8 +52,8 @@ def find_best_app(module): elif len(matches) > 1: raise NoAppException( "Detected multiple Flask applications in module" - f" {module.__name__!r}. Use 'FLASK_APP={module.__name__}:name'" - f" to specify the correct one." + f" '{module.__name__}'. Use '{module.__name__}:name'" + " to specify the correct one." ) # Search for app factory functions. @@ -65,15 +71,15 @@ def find_best_app(module): raise raise NoAppException( - f"Detected factory {attr_name!r} in module {module.__name__!r}," + f"Detected factory '{attr_name}' in module '{module.__name__}'," " but could not call it without arguments. Use" - f" \"FLASK_APP='{module.__name__}:{attr_name}(args)'\"" + f" '{module.__name__}:{attr_name}(args)'" " to specify arguments." ) from e raise NoAppException( "Failed to find Flask application or factory in module" - f" {module.__name__!r}. Use 'FLASK_APP={module.__name__}:name'" + f" '{module.__name__}'. Use '{module.__name__}:name'" " to specify one." ) @@ -253,7 +259,7 @@ def get_version(ctx, param, value): version_option = click.Option( ["--version"], - help="Show the flask version", + help="Show the Flask version.", expose_value=False, callback=get_version, is_flag=True, @@ -338,19 +344,24 @@ class ScriptInfo: onwards as click object. """ - def __init__(self, app_import_path=None, create_app=None, set_debug_flag=True): + def __init__( + self, + app_import_path: str | None = None, + create_app: t.Callable[..., Flask] | None = None, + set_debug_flag: bool = True, + ) -> None: #: Optionally the import path for the Flask application. - self.app_import_path = app_import_path or os.environ.get("FLASK_APP") + self.app_import_path = app_import_path #: Optionally a function that is passed the script info to create #: the instance of the application. self.create_app = create_app #: A dictionary with arbitrary data that can be associated with #: this script info. - self.data = {} + self.data: t.Dict[t.Any, t.Any] = {} self.set_debug_flag = set_debug_flag - self._loaded_app = None + self._loaded_app: Flask | None = None - def load_app(self): + def load_app(self) -> Flask: """Loads the Flask app (if not yet loaded) and returns it. Calling this multiple times will just result in the already loaded app to be returned. @@ -379,9 +390,10 @@ def load_app(self): if not app: raise NoAppException( - "Could not locate a Flask application. You did not provide " - 'the "FLASK_APP" environment variable, and a "wsgi.py" or ' - '"app.py" module was not found in the current directory.' + "Could not locate a Flask application. Use the" + " 'flask --app' option, 'FLASK_APP' environment" + " variable, or a 'wsgi.py' or 'app.py' file in the" + " current directory." ) if self.set_debug_flag: @@ -442,6 +454,117 @@ def group(self, *args, **kwargs): return click.Group.group(self, *args, **kwargs) +def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None: + if value is None: + return None + + info = ctx.ensure_object(ScriptInfo) + info.app_import_path = value + return value + + +# This option is eager so the app will be available if --help is given. +# --help is also eager, so --app must be before it in the param list. +# no_args_is_help bypasses eager processing, so this option must be +# processed manually in that case to ensure FLASK_APP gets picked up. +_app_option = click.Option( + ["-A", "--app"], + metavar="IMPORT", + help=( + "The Flask application or factory function to load, in the form 'module:name'." + " Module can be a dotted import or file path. Name is not required if it is" + " 'app', 'application', 'create_app', or 'make_app', and can be 'name(args)' to" + " pass arguments." + ), + is_eager=True, + expose_value=False, + callback=_set_app, +) + + +def _set_env(ctx: click.Context, param: click.Option, value: str | None) -> str | None: + if value is None: + return None + + # Set with env var instead of ScriptInfo.load so that it can be + # accessed early during a factory function. + os.environ["FLASK_ENV"] = value + return value + + +_env_option = click.Option( + ["-E", "--env"], + metavar="NAME", + help=( + "The execution environment name to set in 'app.env'. Defaults to" + " 'production'. 'development' will enable 'app.debug' and start the" + " debugger and reloader when running the server." + ), + expose_value=False, + callback=_set_env, +) + + +def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None: + # If the flag isn't provided, it will default to False. Don't use + # that, let debug be set by env in that case. + source = ctx.get_parameter_source(param.name) # type: ignore[arg-type] + + if source is not None and source in ( + ParameterSource.DEFAULT, + ParameterSource.DEFAULT_MAP, + ): + return None + + # Set with env var instead of ScriptInfo.load so that it can be + # accessed early during a factory function. + os.environ["FLASK_DEBUG"] = "1" if value else "0" + return value + + +_debug_option = click.Option( + ["--debug/--no-debug"], + help="Set 'app.debug' separately from '--env'.", + expose_value=False, + callback=_set_debug, +) + + +def _env_file_callback( + ctx: click.Context, param: click.Option, value: str | None +) -> str | None: + if value is None: + return None + + import importlib + + try: + importlib.import_module("dotenv") + except ImportError: + raise click.BadParameter( + "python-dotenv must be installed to load an env file.", + ctx=ctx, + param=param, + ) from None + + # Don't check FLASK_SKIP_DOTENV, that only disables automatically + # loading .env and .flaskenv files. + load_dotenv(value) + return value + + +# This option is eager so env vars are loaded as early as possible to be +# used by other options. +_env_file_option = click.Option( + ["-e", "--env-file"], + type=click.Path(exists=True, dir_okay=False), + help="Load environment variables from this file. python-dotenv must be installed.", + is_eager=True, + expose_value=False, + callback=_env_file_callback, +) + + class FlaskGroup(AppGroup): """Special subclass of the :class:`AppGroup` group that supports loading more commands from the configured Flask app. Normally a @@ -460,6 +583,10 @@ class FlaskGroup(AppGroup): :param set_debug_flag: Set the app's debug flag based on the active environment + .. versionchanged:: 2.2 + Added the ``-A/--app``, ``-E/--env``, ``--debug/--no-debug``, + and ``-e/--env-file`` options. + .. versionchanged:: 1.0 If installed, python-dotenv will be used to load environment variables from :file:`.env` and :file:`.flaskenv` files. @@ -467,14 +594,19 @@ class FlaskGroup(AppGroup): def __init__( self, - add_default_commands=True, - create_app=None, - add_version_option=True, - load_dotenv=True, - set_debug_flag=True, - **extra, - ): + add_default_commands: bool = True, + create_app: t.Callable[..., Flask] | None = None, + add_version_option: bool = True, + load_dotenv: bool = True, + set_debug_flag: bool = True, + **extra: t.Any, + ) -> None: params = list(extra.pop("params", None) or ()) + # Processing is done with option callbacks instead of a group + # callback. This allows users to make a custom group callback + # without losing the behavior. --env-file must come first so + # that it is eagerly evaluated before --app. + params.extend((_env_file_option, _app_option, _env_option, _debug_option)) if add_version_option: params.append(version_option) @@ -555,11 +687,13 @@ def list_commands(self, ctx): def make_context( self, - info_name: t.Optional[str], - args: t.List[str], - parent: t.Optional[click.Context] = None, + info_name: str | None, + args: list[str], + parent: click.Context | None = None, **extra: t.Any, ) -> click.Context: + # 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() @@ -570,6 +704,16 @@ def make_context( return super().make_context(info_name, args, parent=parent, **extra) + def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: + if not args and self.no_args_is_help: + # Attempt to load --env-file and --app early in case they + # were given as env vars. Otherwise no_args_is_help will not + # see commands from app.cli. + _env_file_option.handle_parse_result(ctx, {}, []) + _app_option.handle_parse_result(ctx, {}, []) + + return super().parse_args(ctx, args) + def _path_is_ancestor(path, other): """Take ``other`` and remove the length of ``path`` from it. Then join it @@ -578,7 +722,7 @@ def _path_is_ancestor(path, other): return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other -def load_dotenv(path=None): +def load_dotenv(path: str | os.PathLike | None = None) -> bool: """Load "dotenv" files in order of precedence to set environment variables. If an env var is already set it is not overwritten, so earlier files in the @@ -591,13 +735,17 @@ def load_dotenv(path=None): :param path: Load the file at this location instead of searching. :return: ``True`` if a file was loaded. - .. versionchanged:: 1.1.0 - Returns ``False`` when python-dotenv is not installed, or when - the given path isn't a file. + .. versionchanged:: 2.0 + The current directory is not changed to the location of the + loaded file. .. versionchanged:: 2.0 When loading the env files, set the default encoding to UTF-8. + .. versionchanged:: 1.1.0 + Returns ``False`` when python-dotenv is not installed, or when + the given path isn't a file. + .. versionadded:: 1.0 """ try: @@ -613,15 +761,15 @@ def load_dotenv(path=None): return False - # if the given path specifies the actual file then return True, - # else False + # Always return after attempting to load a given path, don't load + # the default files. if path is not None: if os.path.isfile(path): return dotenv.load_dotenv(path, encoding="utf-8") return False - new_dir = None + loaded = False for name in (".env", ".flaskenv"): path = dotenv.find_dotenv(name, usecwd=True) @@ -629,12 +777,10 @@ def load_dotenv(path=None): if not path: continue - if new_dir is None: - new_dir = os.path.dirname(path) - dotenv.load_dotenv(path, encoding="utf-8") + loaded = True - return new_dir is not None # at least one file was located and loaded + return loaded # True if at least one file was located and loaded. def show_server_banner(env, debug, app_import_path, eager_loading): @@ -837,9 +983,10 @@ def run_command( This server is for development purposes only. It does not provide the stability, security, or performance of production WSGI servers. - The reloader and debugger are enabled by default if - FLASK_ENV=development or FLASK_DEBUG=1. + The reloader and debugger are enabled by default with the + '--env development' or '--debug' options. """ + app = DispatchingApp(info.load_app, use_eager_loading=eager_loading) debug = get_debug_flag() if reload is None: @@ -849,7 +996,6 @@ def run_command( debugger = debug show_server_banner(get_env(), debug, info.app_import_path, eager_loading) - app = DispatchingApp(info.load_app, use_eager_loading=eager_loading) from werkzeug.serving import run_simple @@ -971,19 +1117,10 @@ def routes_command(sort: str, all_methods: bool) -> None: help="""\ A general utility script for Flask applications. -Provides commands from Flask, extensions, and the application. Loads the -application defined in the FLASK_APP environment variable, or from a wsgi.py -file. Setting the FLASK_ENV environment variable to 'development' will enable -debug mode. - -\b - {prefix}{cmd} FLASK_APP=hello.py - {prefix}{cmd} FLASK_ENV=development - {prefix}flask run -""".format( - cmd="export" if os.name == "posix" else "set", - prefix="$ " if os.name == "posix" else "> ", - ), +An application to load must be given with the '--app' option, +'FLASK_APP' environment variable, or with a 'wsgi.py' or 'app.py' file +in the current directory. +""", ) diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 3b61635ca6..d1a84b9cf9 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -48,9 +48,9 @@ def get_debug_flag() -> bool: def get_load_dotenv(default: bool = True) -> bool: - """Get whether the user has disabled loading dotenv files by setting - :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load the - files. + """Get whether the user has disabled loading default dotenv files by + setting :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load + the files. :param default: What to return if the env var isn't set. """ From ab1fbef29a073fe5950ea2357f6a3a0d280eb506 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 17 Jun 2022 09:26:26 -0700 Subject: [PATCH 050/229] prefer --app over FLASK_APP in docs --- docs/cli.rst | 206 ++++++++------------------------- docs/config.rst | 68 ++++------- docs/debugging.rst | 76 ++---------- docs/patterns/appfactories.rst | 69 ++--------- docs/patterns/packages.rst | 67 ++--------- docs/quickstart.rst | 115 +++--------------- docs/reqcontext.rst | 4 +- docs/server.rst | 23 ++-- docs/tutorial/database.rst | 6 +- docs/tutorial/deploy.rst | 32 +---- docs/tutorial/factory.rst | 38 +----- docs/tutorial/install.rst | 2 +- examples/javascript/README.rst | 3 +- examples/tutorial/README.rst | 15 +-- tests/conftest.py | 1 + 15 files changed, 146 insertions(+), 579 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index 3be3aaa64c..7bab8fac26 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -15,40 +15,10 @@ Application Discovery --------------------- The ``flask`` command is installed by Flask, not your application; it must be -told where to find your application in order to use it. The ``FLASK_APP`` -environment variable is used to specify how to load the application. +told where to find your application in order to use it. The ``--app`` +option is used to specify how to load the application. -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_APP=hello - $ flask run - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_APP hello - $ flask run - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_APP=hello - > flask run - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_APP = "hello" - > flask run - -While ``FLASK_APP`` supports a variety of options for specifying your +While ``--app`` supports a variety of options for specifying your application, most use cases should be simple. Here are the typical values: (nothing) @@ -56,32 +26,32 @@ application, most use cases should be simple. Here are the typical values: automatically detecting an app (``app`` or ``application``) or factory (``create_app`` or ``make_app``). -``FLASK_APP=hello`` +``--app hello`` The given name is imported, automatically detecting an app (``app`` or ``application``) or factory (``create_app`` or ``make_app``). ---- -``FLASK_APP`` has three parts: an optional path that sets the current working +``--app`` has three parts: an optional path that sets the current working directory, a Python file or dotted import path, and an optional variable name of the instance or factory. If the name is a factory, it can optionally be followed by arguments in parentheses. The following values demonstrate these parts: -``FLASK_APP=src/hello`` +``--app src/hello`` Sets the current working directory to ``src`` then imports ``hello``. -``FLASK_APP=hello.web`` +``--app hello.web`` Imports the path ``hello.web``. -``FLASK_APP=hello:app2`` +``--app hello:app2`` Uses the ``app2`` Flask instance in ``hello``. -``FLASK_APP="hello:create_app('dev')"`` +``--app 'hello:create_app("dev")'`` The ``create_app`` factory in ``hello`` is called with the string ``'dev'`` as the argument. -If ``FLASK_APP`` is not set, the command will try to import "app" or +If ``--app`` is not set, the command will try to import "app" or "wsgi" (as a ".py" file, or package) and try to detect an application instance or factory. @@ -137,8 +107,9 @@ Environments .. versionadded:: 1.0 -The environment in which the Flask app runs is set by the -:envvar:`FLASK_ENV` environment variable. If not set it defaults to +The environment in which the Flask app executes is set by the +``FLASK_ENV`` environment variable. When using the ``flask`` command, it +can also be set with the ``--env`` option. If not set it defaults to ``production``. The other recognized environment is ``development``. Flask and extensions may choose to enable behaviors based on the environment. @@ -147,63 +118,16 @@ If the env is set to ``development``, the ``flask`` command will enable debug mode and ``flask run`` will enable the interactive debugger and reloader. -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_ENV=development - $ flask run - * Serving Flask app "hello" - * Environment: development - * Debug mode: on - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) - * Restarting with inotify reloader - * Debugger is active! - * Debugger PIN: 223-456-919 - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_ENV development - $ flask run - * Serving Flask app "hello" - * Environment: development - * Debug mode: on - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) - * Restarting with inotify reloader - * Debugger is active! - * Debugger PIN: 223-456-919 - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_ENV=development - > flask run - * Serving Flask app "hello" - * Environment: development - * Debug mode: on - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) - * Restarting with inotify reloader - * Debugger is active! - * Debugger PIN: 223-456-919 - - .. group-tab:: Powershell - - .. code-block:: text +.. code-block:: text - > $env:FLASK_ENV = "development" - > flask run - * Serving Flask app "hello" - * Environment: development - * Debug mode: on - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) - * Restarting with inotify reloader - * Debugger is active! - * Debugger PIN: 223-456-919 + $ flask --app hello --env development run + * Serving Flask app "hello" + * Environment: development + * Debug mode: on + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) + * Restarting with inotify reloader + * Debugger is active! + * Debugger PIN: 223-456-919 Watch Extra Files with the Reloader @@ -211,72 +135,31 @@ Watch Extra Files with the Reloader When using development mode, the reloader will trigger whenever your Python code or imported modules change. The reloader can watch -additional files with the ``--extra-files`` option, or the -``FLASK_RUN_EXTRA_FILES`` environment variable. Multiple paths are +additional files with the ``--extra-files`` option. Multiple paths are separated with ``:``, or ``;`` on Windows. -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ flask run --extra-files file1:dirA/file2:dirB/ - # or - $ export FLASK_RUN_EXTRA_FILES=file1:dirA/file2:dirB/ - $ flask run - * Running on http://127.0.0.1:8000/ - * Detected change in '/path/to/file1', reloading - - .. group-tab:: Fish - - .. code-block:: text - - $ flask run --extra-files file1:dirA/file2:dirB/ - # or - $ set -x FLASK_RUN_EXTRA_FILES file1 dirA/file2 dirB/ - $ flask run - * Running on http://127.0.0.1:8000/ - * Detected change in '/path/to/file1', reloading - - .. group-tab:: CMD - - .. code-block:: text - - > flask run --extra-files file1:dirA/file2:dirB/ - # or - > set FLASK_RUN_EXTRA_FILES=file1:dirA/file2:dirB/ - > flask run - * Running on http://127.0.0.1:8000/ - * Detected change in '/path/to/file1', reloading - - .. group-tab:: Powershell - - .. code-block:: text +.. code-block:: text - > flask run --extra-files file1:dirA/file2:dirB/ - # or - > $env:FLASK_RUN_EXTRA_FILES = "file1:dirA/file2:dirB/" - > flask run - * Running on http://127.0.0.1:8000/ - * Detected change in '/path/to/file1', reloading + $ flask run --extra-files file1:dirA/file2:dirB/ + * Running on http://127.0.0.1:8000/ + * Detected change in '/path/to/file1', reloading Ignore files with the Reloader ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The reloader can also ignore files using :mod:`fnmatch` patterns with -the ``--exclude-patterns`` option, or the ``FLASK_RUN_EXCLUDE_PATTERNS`` -environment variable. Multiple patterns are separated with ``:``, or -``;`` on Windows. +the ``--exclude-patterns`` option. Multiple patterns are separated with +``:``, or ``;`` on Windows. Debug Mode ---------- -Debug mode will be enabled when :envvar:`FLASK_ENV` is ``development``, -as described above. If you want to control debug mode separately, use -:envvar:`FLASK_DEBUG`. The value ``1`` enables it, ``0`` disables it. +Debug mode will be enabled when the execution environment is +``development``, as described above. If you want to control debug mode +separately, use the ``--debug/--no-debug`` option or the ``FLASK_DEBUG`` +environment variable. .. _dotenv: @@ -284,14 +167,21 @@ as described above. If you want to control debug mode separately, use Environment Variables From dotenv --------------------------------- -Rather than setting ``FLASK_APP`` each time you open a new terminal, you can -use Flask's dotenv support to set environment variables automatically. +The ``flask`` command supports setting any option for any command with +environment variables. The variables are named like ``FLASK_OPTION`` or +``FLASK_COMMAND_OPTION``, for example ``FLASK_APP`` or +``FLASK_RUN_PORT``. + +Rather than passing options every time you run a command, or environment +variables every time you open a new terminal, you can use Flask's dotenv +support to set environment variables automatically. If `python-dotenv`_ is installed, running the ``flask`` command will set -environment variables defined in the files :file:`.env` and :file:`.flaskenv`. -This can be used to avoid having to set ``FLASK_APP`` manually every time you -open a new terminal, and to set configuration using environment variables -similar to how some deployment services work. +environment variables defined in the files ``.env`` and ``.flaskenv``. +You can also specify an extra file to load with the ``--env-file`` +option. Dotenv files can be used to avoid having to set ``--app`` or +``FLASK_APP`` manually, and to set configuration using environment +variables similar to how some deployment services work. Variables set on the command line are used over those set in :file:`.env`, which are used over those set in :file:`.flaskenv`. :file:`.flaskenv` should be @@ -612,7 +502,7 @@ Custom Scripts -------------- When you are using the app factory pattern, it may be more convenient to define -your own Click script. Instead of using ``FLASK_APP`` and letting Flask load +your own Click script. Instead of using ``--app`` and letting Flask load your application, you can create your own Click object and export it as a `console script`_ entry point. @@ -646,7 +536,7 @@ Define the entry point in :file:`setup.py`:: ) Install the application in the virtualenv in editable mode and the custom -script is available. Note that you don't need to set ``FLASK_APP``. :: +script is available. Note that you don't need to set ``--app``. :: $ pip install -e . $ wiki run diff --git a/docs/config.rst b/docs/config.rst index 7a5e4da1e1..12170e9041 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -47,61 +47,33 @@ Environment and Debug Features The :data:`ENV` and :data:`DEBUG` config values are special because they may behave inconsistently if changed after the app has begun setting up. -In order to set the environment and debug mode reliably, Flask uses -environment variables. +In order to set the environment and debug mode reliably, pass options to +the ``flask`` command or use environment variables. -The environment is used to indicate to Flask, extensions, and other -programs, like Sentry, what context Flask is running in. It is -controlled with the :envvar:`FLASK_ENV` environment variable and -defaults to ``production``. +The execution environment is used to indicate to Flask, extensions, and +other programs, like Sentry, what context Flask is running in. It is +controlled with the ``FLASK_ENV`` environment variable, or the +``--env`` option when using the ``flask`` command, and defaults to +``production``. -Setting :envvar:`FLASK_ENV` to ``development`` will enable debug mode. -``flask run`` will use the interactive debugger and reloader by default -in debug mode. To control this separately from the environment, use the -:envvar:`FLASK_DEBUG` flag. - -.. versionchanged:: 1.0 - Added :envvar:`FLASK_ENV` to control the environment separately - from debug mode. The development environment enables debug mode. +Setting ``--env development`` will enable debug mode. ``flask run`` will +use the interactive debugger and reloader by default in debug mode. To +control this separately from the environment, use the +``--debug/--no-debug`` option or the ``FLASK_DEBUG`` environment +variable. To switch Flask to the development environment and enable debug mode, -set :envvar:`FLASK_ENV`: - -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_ENV=development - $ flask run - - .. group-tab:: Fish - - .. code-block:: text +set ``--env``: - $ set -x FLASK_ENV development - $ flask run - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_ENV=development - > flask run - - .. group-tab:: Powershell - - .. code-block:: text +.. code-block:: text - > $env:FLASK_ENV = "development" - > flask run + $ flask --app hello --env development run -Using the environment variables as described above is recommended. While -it is possible to set :data:`ENV` and :data:`DEBUG` in your config or -code, this is strongly discouraged. They can't be read early by the -``flask`` command, and some systems or extensions may have already -configured themselves based on a previous value. +Using the options or environment variables as described above is +recommended. While it is possible to set :data:`ENV` and :data:`DEBUG` +in your config or code, this is strongly discouraged. They can't be read +early by the ``flask`` command, and some systems or extensions may have +already configured themselves based on a previous value. Builtin Configuration Values diff --git a/docs/debugging.rst b/docs/debugging.rst index cd955312b8..b907c769cd 100644 --- a/docs/debugging.rst +++ b/docs/debugging.rst @@ -39,45 +39,19 @@ during a request. This debugger should only be used during development. security risk. Do not run the development server or debugger in a production environment. -To enable the debugger, run the development server with the -``FLASK_ENV`` environment variable set to ``development``. This puts -Flask in debug mode, which changes how it handles some errors, and -enables the debugger and reloader. +To enable the debugger, run the development server with the environment +set to ``development``. This puts Flask in debug mode, which changes how +it handles some errors, and enables the debugger and reloader. -.. tabs:: +.. code-block:: text - .. group-tab:: Bash + $ flask --app hello --env development run - .. code-block:: text - - $ export FLASK_ENV=development - $ flask run - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_ENV development - $ flask run - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_ENV=development - > flask run - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_ENV = "development" - > flask run - -``FLASK_ENV`` can only be set as an environment variable. When running +``FLASK_ENV`` can also be set as an environment variable. When running from Python code, passing ``debug=True`` enables debug mode, which is -mostly equivalent. Debug mode can be controlled separately from -``FLASK_ENV`` with the ``FLASK_DEBUG`` environment variable as well. +mostly equivalent. Debug mode can be controlled separately from the +environment with the ``--debug/--no-debug`` option or the +``FLASK_DEBUG`` environment variable. .. code-block:: python @@ -102,37 +76,9 @@ When using an external debugger, the app should still be in debug mode, but it can be useful to disable the built-in debugger and reloader, which can interfere. -When running from the command line: - -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_ENV=development - $ flask run --no-debugger --no-reload - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_ENV development - $ flask run --no-debugger --no-reload - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_ENV=development - > flask run --no-debugger --no-reload - - .. group-tab:: Powershell - - .. code-block:: text +.. code-block:: text - > $env:FLASK_ENV = "development" - > flask run --no-debugger --no-reload + $ flask --app hello --env development run --no-debugger --no-reload When running from Python: diff --git a/docs/patterns/appfactories.rst b/docs/patterns/appfactories.rst index a0e88ab3d8..415c10fa47 100644 --- a/docs/patterns/appfactories.rst +++ b/docs/patterns/appfactories.rst @@ -89,71 +89,20 @@ Using Applications To run such an application, you can use the :command:`flask` command: -.. tabs:: +.. code-block:: text - .. group-tab:: Bash + $ flask run --app hello run - .. code-block:: text +Flask will automatically detect the factory if it is named +``create_app`` or ``make_app`` in ``hello``. You can also pass arguments +to the factory like this: - $ export FLASK_APP=myapp - $ flask run +.. code-block:: text - .. group-tab:: Fish + $ flask run --app hello:create_app(local_auth=True)`` - .. code-block:: text - - $ set -x FLASK_APP myapp - $ flask run - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_APP=myapp - > flask run - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_APP = "myapp" - > flask run - -Flask will automatically detect the factory (``create_app`` or ``make_app``) -in ``myapp``. You can also pass arguments to the factory like this: - -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_APP="myapp:create_app('dev')" - $ flask run - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_APP "myapp:create_app('dev')" - $ flask run - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_APP="myapp:create_app('dev')" - > flask run - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_APP = "myapp:create_app('dev')" - > flask run - -Then the ``create_app`` factory in ``myapp`` is called with the string -``'dev'`` as the argument. See :doc:`/cli` for more detail. +Then the ``create_app`` factory in ``myapp`` is called with the keyword +argument ``local_auth=True``. See :doc:`/cli` for more detail. Factory Improvements -------------------- diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index a30ef3cbc3..13f8270ee8 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -56,70 +56,17 @@ a big problem, just add a new file called :file:`setup.py` next to the inner ], ) -In order to run the application you need to export an environment variable -that tells Flask where to find the application instance: +Install your application so it is importable: -.. tabs:: +.. code-block:: text - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_APP=yourapplication - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_APP yourapplication - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_APP=yourapplication - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_APP = "yourapplication" - -If you are outside of the project directory make sure to provide the exact -path to your application directory. Similarly you can turn on the -development features like this: - -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_ENV=development - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_ENV development - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_ENV=development - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_ENV = "development" + $ pip install -e . -In order to install and run the application you need to issue the following -commands:: +To use the ``flask`` command and run your application you need to set +the ``--app`` option that tells Flask where to find the application +instance: - $ pip install -e . - $ flask run + $ flask --app yourapplication run What did we gain from this? Now we can restructure the application a bit into multiple modules. The only thing you have to remember is the diff --git a/docs/quickstart.rst b/docs/quickstart.rst index a6956c3201..e6646c6c9e 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -39,50 +39,20 @@ Save it as :file:`hello.py` or something similar. Make sure to not call your application :file:`flask.py` because this would conflict with Flask itself. -To run the application, use the :command:`flask` command or -:command:`python -m flask`. Before you can do that you need -to tell your terminal the application to work with by exporting the -``FLASK_APP`` environment variable: +To run the application, use the ``flask`` command or +``python -m flask``. You need to tell the Flask where your application +is with the ``-app`` option. -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_APP=hello - $ flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_APP hello - $ flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_APP=hello - > flask run - * Running on http://127.0.0.1:5000/ - - .. group-tab:: Powershell - - .. code-block:: text +.. code-block:: text - > $env:FLASK_APP = "hello" - > flask run - * Running on http://127.0.0.1:5000/ + $ flask --app hello run + * Serving Flask app 'hello' (lazy loading) + * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) .. admonition:: Application Discovery Behavior As a shortcut, if the file is named ``app.py`` or ``wsgi.py``, you - don't have to set the ``FLASK_APP`` environment variable. See - :doc:`/cli` for more details. + don't have to use ``--app``. See :doc:`/cli` for more details. This launches a very simple builtin server, which is good enough for testing but probably not what you want to use in production. For @@ -114,34 +84,6 @@ handle that. This tells your operating system to listen on all public IPs. -What to do if the Server does not Start ---------------------------------------- - -In case the :command:`python -m flask` fails or :command:`flask` -does not exist, there are multiple reasons this might be the case. -First of all you need to look at the error message. - -Old Version of Flask -```````````````````` - -Versions of Flask older than 0.11 used to have different ways to start the -application. In short, the :command:`flask` command did not exist, and -neither did :command:`python -m flask`. In that case you have two options: -either upgrade to newer Flask versions or have a look at :doc:`/server` -to see the alternative method for running a server. - -Invalid Import Name -``````````````````` - -The ``FLASK_APP`` environment variable is the name of the module to import at -:command:`flask run`. In case that module is incorrectly named you will get an -import error upon start (or if debug is enabled when you navigate to the -application). It will tell you what it tried to import and why it failed. - -The most common reason is a typo or because you did not actually create an -``app`` object. - - Debug Mode ---------- @@ -162,38 +104,19 @@ error occurs during a request. security risk. Do not run the development server or debugger in a production environment. -To enable all development features, set the ``FLASK_ENV`` environment -variable to ``development`` before calling ``flask run``. - -.. tabs:: - - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_ENV=development - $ flask run +To enable all development features, set the ``--env`` option to +``development``. - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_ENV development - $ flask run - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_ENV=development - > flask run - - .. group-tab:: Powershell - - .. code-block:: text +.. code-block:: text - > $env:FLASK_ENV = "development" - > flask run + $ flask --app hello --env development run + * Serving Flask app 'hello' (lazy loading) + * Environment: development + * Debug mode: on + * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) + * Restarting with stat + * Debugger is active! + * Debugger PIN: nnn-nnn-nnn See also: diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index b67745edbe..f395e844b0 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -227,8 +227,8 @@ associated with it is destroyed. If an error occurs during development, it is useful to delay destroying the data for debugging purposes. When the development server is running in development mode (the -``FLASK_ENV`` environment variable is set to ``'development'``), the -error and data will be preserved and shown in the interactive debugger. +``--env`` option is set to ``'development'``), the error and data will +be preserved and shown in the interactive debugger. This behavior can be controlled with the :data:`PRESERVE_CONTEXT_ON_EXCEPTION` config. As described above, it diff --git a/docs/server.rst b/docs/server.rst index f674bcd70c..aa1438a3ed 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -19,9 +19,16 @@ Command Line ------------ The ``flask run`` command line script is the recommended way to run the -development server. It requires setting the ``FLASK_APP`` environment -variable to point to your application, and ``FLASK_ENV=development`` to -fully enable development mode. +development server. Use the ``--app`` option to point to your +application, and the ``--env development`` option to fully enable +development mode. + +.. code-block:: text + + $ flask --app hello --env development run + +These options (and any others) can also be set using environment +variables. .. tabs:: @@ -65,11 +72,11 @@ and using the CLI. .. note:: - Prior to Flask 1.0 the ``FLASK_ENV`` environment variable was not - supported and you needed to enable debug mode by exporting - ``FLASK_DEBUG=1``. This can still be used to control debug mode, but - you should prefer setting the development environment as shown - above. + Debug mode can be controlled separately from the development + environment with the ``--debug/--no-debug`` option or the + ``FLASK_DEBUG`` environment variable. This is how older versions of + Flask worked. You should prefer setting the development environment + as shown above. .. _address-already-in-use: diff --git a/docs/tutorial/database.rst b/docs/tutorial/database.rst index b094909eca..b285219703 100644 --- a/docs/tutorial/database.rst +++ b/docs/tutorial/database.rst @@ -196,15 +196,13 @@ previous page. If you're still running the server from the previous page, you can either stop the server, or run this command in a new terminal. If you use a new terminal, remember to change to your project directory - and activate the env as described in :doc:`/installation`. You'll - also need to set ``FLASK_APP`` and ``FLASK_ENV`` as shown on the - previous page. + and activate the env as described in :doc:`/installation`. Run the ``init-db`` command: .. code-block:: none - $ flask init-db + $ flask --app flaskr init-db Initialized the database. There will now be a ``flaskr.sqlite`` file in the ``instance`` folder in diff --git a/docs/tutorial/deploy.rst b/docs/tutorial/deploy.rst index 269402407a..436ed5e812 100644 --- a/docs/tutorial/deploy.rst +++ b/docs/tutorial/deploy.rst @@ -48,35 +48,9 @@ Pip will install your project along with its dependencies. Since this is a different machine, you need to run ``init-db`` again to create the database in the instance folder. -.. tabs:: + .. code-block:: text - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_APP=flaskr - $ flask init-db - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_APP flaskr - $ flask init-db - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_APP=flaskr - > flask init-db - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_APP = "flaskr" - > flask init-db + $ flask --app flaskr init-db When Flask detects that it's installed (not in editable mode), it uses a different directory for the instance folder. You can find it at @@ -127,7 +101,7 @@ first install it in the virtual environment: $ pip install waitress You need to tell Waitress about your application, but it doesn't use -``FLASK_APP`` like ``flask run`` does. You need to tell it to import and +``--app`` like ``flask run`` does. You need to tell it to import and call the application factory to get an application object. .. code-block:: none diff --git a/docs/tutorial/factory.rst b/docs/tutorial/factory.rst index 730818743b..cbfecab30b 100644 --- a/docs/tutorial/factory.rst +++ b/docs/tutorial/factory.rst @@ -135,43 +135,13 @@ exception, and restarts the server whenever you make changes to the code. You can leave it running and just reload the browser page as you follow the tutorial. -.. tabs:: +.. code-block:: text - .. group-tab:: Bash - - .. code-block:: text - - $ export FLASK_APP=flaskr - $ export FLASK_ENV=development - $ flask run - - .. group-tab:: Fish - - .. code-block:: text - - $ set -x FLASK_APP flaskr - $ set -x FLASK_ENV development - $ flask run - - .. group-tab:: CMD - - .. code-block:: text - - > set FLASK_APP=flaskr - > set FLASK_ENV=development - > flask run - - .. group-tab:: Powershell - - .. code-block:: text - - > $env:FLASK_APP = "flaskr" - > $env:FLASK_ENV = "development" - > flask run + $ flask --app flaskr --env development run You'll see output similar to this: -.. code-block:: none +.. code-block:: text * Serving Flask app "flaskr" * Environment: development @@ -179,7 +149,7 @@ You'll see output similar to this: * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! - * Debugger PIN: 855-212-761 + * Debugger PIN: nnn-nnn-nnn Visit http://127.0.0.1:5000/hello in a browser and you should see the "Hello, World!" message. Congratulations, you're now running your Flask diff --git a/docs/tutorial/install.rst b/docs/tutorial/install.rst index 9e808f1461..7380b30269 100644 --- a/docs/tutorial/install.rst +++ b/docs/tutorial/install.rst @@ -109,7 +109,7 @@ You can observe that the project is now installed with ``pip list``. wheel 0.30.0 Nothing changes from how you've been running your project so far. -``FLASK_APP`` is still set to ``flaskr`` and ``flask run`` still runs +``--app`` is still set to ``flaskr`` and ``flask run`` still runs the application, but you can call it from anywhere, not just the ``flask-tutorial`` directory. diff --git a/examples/javascript/README.rst b/examples/javascript/README.rst index b6e340dfc9..23c7ce436d 100644 --- a/examples/javascript/README.rst +++ b/examples/javascript/README.rst @@ -33,8 +33,7 @@ Run .. code-block:: text - $ export FLASK_APP=js_example - $ flask run + $ flask --app js_example run Open http://127.0.0.1:5000 in a browser. diff --git a/examples/tutorial/README.rst b/examples/tutorial/README.rst index 41f3f6ba71..6c7c2fe9ce 100644 --- a/examples/tutorial/README.rst +++ b/examples/tutorial/README.rst @@ -45,19 +45,10 @@ installing Flaskr:: Run --- -:: - - $ export FLASK_APP=flaskr - $ export FLASK_ENV=development - $ flask init-db - $ flask run - -Or on Windows cmd:: +.. code-block:: text - > set FLASK_APP=flaskr - > set FLASK_ENV=development - > flask init-db - > flask run + $ flask --app flaskr init-db + $ flask --app flaskr --env development run Open http://127.0.0.1:5000 in a browser. diff --git a/tests/conftest.py b/tests/conftest.py index 17ff2f3db7..1e1ba0d449 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,7 @@ def _standard_os_environ(): """ mp = monkeypatch.MonkeyPatch() out = ( + (os.environ, "FLASK_ENV_FILE", monkeypatch.notset), (os.environ, "FLASK_APP", monkeypatch.notset), (os.environ, "FLASK_ENV", monkeypatch.notset), (os.environ, "FLASK_DEBUG", monkeypatch.notset), From c9e000b9cea2e117218d460874d86301fbb43c43 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 17 Jun 2022 11:14:22 -0700 Subject: [PATCH 051/229] with_appcontext lasts for the lifetime of the click context --- CHANGES.rst | 3 +++ docs/cli.rst | 20 ++++++++----------- docs/tutorial/database.rst | 2 -- examples/tutorial/flaskr/db.py | 2 -- src/flask/cli.py | 35 ++++++++++++++++++++++++++++------ tests/test_cli.py | 29 ++++++++++++++++++++++++---- 6 files changed, 65 insertions(+), 26 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0dea8e3a0e..42d91ea3b2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -33,6 +33,9 @@ Unreleased - Add ``--env-file`` option to the ``flask`` CLI. This allows specifying a dotenv file to load in addition to ``.env`` and ``.flaskenv``. :issue:`3108` +- It is no longer required to decorate custom CLI commands on + ``app.cli`` or ``blueprint.cli`` with ``@with_appcontext``, an app + context will already be active at that point. :issue:`2410` Version 2.1.3 diff --git a/docs/cli.rst b/docs/cli.rst index 7bab8fac26..b5724d4901 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -437,12 +437,14 @@ commands directly to the application's level: Application Context ~~~~~~~~~~~~~~~~~~~ -Commands added using the Flask app's :attr:`~Flask.cli` -:meth:`~cli.AppGroup.command` decorator will be executed with an application -context pushed, so your command and extensions have access to the app and its -configuration. If you create a command using the Click :func:`~click.command` -decorator instead of the Flask decorator, you can use -:func:`~cli.with_appcontext` to get the same behavior. :: +Commands added using the Flask app's :attr:`~Flask.cli` or +:class:`~flask.cli.FlaskGroup` :meth:`~cli.AppGroup.command` decorator +will be executed with an application context pushed, so your custom +commands and parameters have access to the app and its configuration. The +:func:`~cli.with_appcontext` decorator can be used to get the same +behavior, but is not needed in most cases. + +.. code-block:: python import click from flask.cli import with_appcontext @@ -454,12 +456,6 @@ decorator instead of the Flask decorator, you can use app.cli.add_command(do_work) -If you're sure a command doesn't need the context, you can disable it:: - - @app.cli.command(with_appcontext=False) - def do_work(): - ... - Plugins ------- diff --git a/docs/tutorial/database.rst b/docs/tutorial/database.rst index b285219703..934f6008ce 100644 --- a/docs/tutorial/database.rst +++ b/docs/tutorial/database.rst @@ -40,7 +40,6 @@ response is sent. import click from flask import current_app, g - from flask.cli import with_appcontext def get_db(): @@ -128,7 +127,6 @@ Add the Python functions that will run these SQL commands to the @click.command('init-db') - @with_appcontext def init_db_command(): """Clear the existing data and create new tables.""" init_db() diff --git a/examples/tutorial/flaskr/db.py b/examples/tutorial/flaskr/db.py index f1e2dc3062..acaa4ae31f 100644 --- a/examples/tutorial/flaskr/db.py +++ b/examples/tutorial/flaskr/db.py @@ -3,7 +3,6 @@ import click from flask import current_app from flask import g -from flask.cli import with_appcontext def get_db(): @@ -39,7 +38,6 @@ def init_db(): @click.command("init-db") -@with_appcontext def init_db_command(): """Clear existing data and create new tables.""" init_db() diff --git a/src/flask/cli.py b/src/flask/cli.py index 40f1de5496..321794b69b 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -410,15 +410,25 @@ def load_app(self) -> Flask: def with_appcontext(f): """Wraps a callback so that it's guaranteed to be executed with the - script's application context. If callbacks are registered directly - to the ``app.cli`` object then they are wrapped with this function - by default unless it's disabled. + script's application context. + + Custom commands (and their options) registered under ``app.cli`` or + ``blueprint.cli`` will always have an app context available, this + decorator is not required in that case. + + .. versionchanged:: 2.2 + The app context is active for subcommands as well as the + decorated callback. The app context is always available to + ``app.cli`` command and parameter callbacks. """ @click.pass_context def decorator(__ctx, *args, **kwargs): - with __ctx.ensure_object(ScriptInfo).load_app().app_context(): - return __ctx.invoke(f, *args, **kwargs) + if not current_app: + app = __ctx.ensure_object(ScriptInfo).load_app() + __ctx.with_resource(app.app_context()) + + return __ctx.invoke(f, *args, **kwargs) return update_wrapper(decorator, f) @@ -587,6 +597,10 @@ class FlaskGroup(AppGroup): Added the ``-A/--app``, ``-E/--env``, ``--debug/--no-debug``, and ``-e/--env-file`` options. + .. versionchanged:: 2.2 + An app context is pushed when running ``app.cli`` commands, so + ``@with_appcontext`` is no longer required for those commands. + .. versionchanged:: 1.0 If installed, python-dotenv will be used to load environment variables from :file:`.env` and :file:`.flaskenv` files. @@ -660,9 +674,18 @@ def get_command(self, ctx, name): # Look up commands provided by the app, showing an error and # continuing if the app couldn't be loaded. try: - return info.load_app().cli.get_command(ctx, name) + app = info.load_app() except NoAppException as e: click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") + return None + + # Push an app context for the loaded app unless it is already + # active somehow. This makes the context available to parameter + # and command callbacks without needing @with_appcontext. + if not current_app or current_app._get_current_object() is not app: + ctx.with_resource(app.app_context()) + + return app.cli.get_command(ctx, name) def list_commands(self, ctx): self._load_plugin_commands() diff --git a/tests/test_cli.py b/tests/test_cli.py index 7a8e9af9cb..7d83d86527 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -13,6 +13,7 @@ from _pytest.monkeypatch import notset from click.testing import CliRunner +from flask import _app_ctx_stack from flask import Blueprint from flask import current_app from flask import Flask @@ -310,6 +311,26 @@ def bad_load(): lazy._flush_bg_loading_exception() +def test_app_cli_has_app_context(app, runner): + def _param_cb(ctx, param, value): + # current_app should be available in parameter callbacks + return bool(current_app) + + @app.cli.command() + @click.argument("value", callback=_param_cb) + def check(value): + app = click.get_current_context().obj.load_app() + # the loaded app should be the same as current_app + same_app = current_app._get_current_object() is app + # only one app context should be pushed + stack_size = len(_app_ctx_stack._local.stack) + return same_app, stack_size, value + + cli = FlaskGroup(create_app=lambda: app) + result = runner.invoke(cli, ["check", "x"], standalone_mode=False) + assert result.return_value == (True, 1, True) + + def test_with_appcontext(runner): @click.command() @with_appcontext @@ -323,12 +344,12 @@ def testcmd(): assert result.output == "testapp\n" -def test_appgroup(runner): +def test_appgroup_app_context(runner): @click.group(cls=AppGroup) def cli(): pass - @cli.command(with_appcontext=True) + @cli.command() def test(): click.echo(current_app.name) @@ -336,7 +357,7 @@ def test(): def subgroup(): pass - @subgroup.command(with_appcontext=True) + @subgroup.command() def test2(): click.echo(current_app.name) @@ -351,7 +372,7 @@ def test2(): assert result.output == "testappgroup\n" -def test_flaskgroup(runner): +def test_flaskgroup_app_context(runner): def create_app(): return Flask("flaskgroup") From ed42e9292811a95f2a68e06a9ae50cb5872216a3 Mon Sep 17 00:00:00 2001 From: Kevin Kirsche Date: Fri, 17 Jun 2022 10:38:17 -0400 Subject: [PATCH 052/229] session expiration datetime is UTC timezone-aware --- CHANGES.rst | 2 ++ src/flask/sessions.py | 3 ++- tests/test_basic.py | 5 +++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 42d91ea3b2..7286f2b732 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -36,6 +36,8 @@ Unreleased - It is no longer required to decorate custom CLI commands on ``app.cli`` or ``blueprint.cli`` with ``@with_appcontext``, an app context will already be active at that point. :issue:`2410` +- ``SessionInterface.get_expiration_time`` uses a timezone-aware + value. :pr:`4645` Version 2.1.3 diff --git a/src/flask/sessions.py b/src/flask/sessions.py index a14aecbac4..f49b582e1c 100644 --- a/src/flask/sessions.py +++ b/src/flask/sessions.py @@ -3,6 +3,7 @@ import warnings from collections.abc import MutableMapping from datetime import datetime +from datetime import timezone from itsdangerous import BadSignature from itsdangerous import URLSafeTimedSerializer @@ -277,7 +278,7 @@ def get_expiration_time( lifetime configured on the application. """ if session.permanent: - return datetime.utcnow() + app.permanent_session_lifetime + return datetime.now(timezone.utc) + app.permanent_session_lifetime return None def should_set_cookie(self, app: "Flask", session: SessionMixin) -> bool: diff --git a/tests/test_basic.py b/tests/test_basic.py index 7c1f4197ed..68141d031d 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -5,6 +5,7 @@ import warnings import weakref from datetime import datetime +from datetime import timezone from platform import python_implementation from threading import Thread @@ -436,7 +437,7 @@ def test(): assert "set-cookie" in rv.headers match = re.search(r"(?i)\bexpires=([^;]+)", rv.headers["set-cookie"]) expires = parse_date(match.group()) - expected = datetime.utcnow() + app.permanent_session_lifetime + expected = datetime.now(timezone.utc) + app.permanent_session_lifetime assert expires.year == expected.year assert expires.month == expected.month assert expires.day == expected.day @@ -466,7 +467,7 @@ def dump_session_contents(): def test_session_special_types(app, client): - now = datetime.utcnow().replace(microsecond=0) + now = datetime.now(timezone.utc).replace(microsecond=0) the_uuid = uuid.uuid4() @app.route("/") From 762382e436062183b1e1fa6f3bda594090a452e7 Mon Sep 17 00:00:00 2001 From: pgjones Date: Thu, 9 Jun 2022 09:30:21 +0100 Subject: [PATCH 053/229] view functions can return generators as responses directly --- CHANGES.rst | 2 ++ src/flask/app.py | 13 ++++++++++++- src/flask/typing.py | 4 +++- tests/test_basic.py | 5 +++++ tests/typing/typing_route.py | 26 ++++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7286f2b732..694d7a5679 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -38,6 +38,8 @@ Unreleased context will already be active at that point. :issue:`2410` - ``SessionInterface.get_expiration_time`` uses a timezone-aware value. :pr:`4645` +- View functions can return generators directly instead of wrapping + them in a ``Response``. :pr:`4629` Version 2.1.3 diff --git a/src/flask/app.py b/src/flask/app.py index 360916dbf5..236a47a8de 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -5,6 +5,7 @@ import sys import typing as t import weakref +from collections.abc import Iterator as _abc_Iterator from datetime import timedelta from itertools import chain from threading import Lock @@ -1843,6 +1844,10 @@ def make_response(self, rv: ft.ResponseReturnValue) -> Response: ``dict`` A dictionary that will be jsonify'd before being returned. + ``generator`` or ``iterator`` + A generator that returns ``str`` or ``bytes`` to be + streamed as the response. + ``tuple`` Either ``(body, status, headers)``, ``(body, status)``, or ``(body, headers)``, where ``body`` is any of the other types @@ -1862,6 +1867,12 @@ def make_response(self, rv: ft.ResponseReturnValue) -> Response: The function is called as a WSGI application. The result is used to create a response object. + .. versionchanged:: 2.2 + A generator will be converted to a streaming response. + + .. versionchanged:: 1.1 + A dict will be converted to a JSON response. + .. versionchanged:: 0.9 Previously a tuple was interpreted as the arguments for the response object. @@ -1900,7 +1911,7 @@ def make_response(self, rv: ft.ResponseReturnValue) -> Response: # make sure the body is an instance of the response class if not isinstance(rv, self.response_class): - if isinstance(rv, (str, bytes, bytearray)): + if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, _abc_Iterator): # let the response class set the status and headers instead of # waiting to do it manually, so that the class can handle any # special logic diff --git a/src/flask/typing.py b/src/flask/typing.py index 18c2b10e94..4fb96545dd 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -6,7 +6,9 @@ from werkzeug.wrappers import Response # noqa: F401 # The possible types that are directly convertible or are a Response object. -ResponseValue = t.Union["Response", str, bytes, t.Dict[str, t.Any]] +ResponseValue = t.Union[ + "Response", str, bytes, t.Dict[str, t.Any], t.Iterator[str], t.Iterator[bytes] +] # the possible types for an individual HTTP header # This should be a Union, but mypy doesn't pass unless it's a TypeVar. diff --git a/tests/test_basic.py b/tests/test_basic.py index 68141d031d..916b7038b7 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1276,6 +1276,11 @@ def test_make_response(app, req_ctx): assert rv.data == b"W00t" assert rv.mimetype == "text/html" + rv = flask.make_response(c for c in "Hello") + assert rv.status_code == 200 + assert rv.data == b"Hello" + assert rv.mimetype == "text/html" + def test_make_response_with_response_instance(app, req_ctx): rv = flask.make_response(flask.jsonify({"msg": "W00t"}), 400) diff --git a/tests/typing/typing_route.py b/tests/typing/typing_route.py index ba49d1328e..9c518938fb 100644 --- a/tests/typing/typing_route.py +++ b/tests/typing/typing_route.py @@ -1,9 +1,11 @@ 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 @@ -26,6 +28,25 @@ def hello_json() -> Response: return jsonify({"response": "Hello, World!"}) +@app.route("/generator") +def hello_generator() -> t.Generator[str, None, None]: + def show() -> t.Generator[str, None, None]: + for x in range(100): + yield f"data:{x}\n\n" + + return show() + + +@app.route("/generator-expression") +def hello_generator_expression() -> t.Iterator[bytes]: + return (f"data:{x}\n\n".encode() for x in range(100)) + + +@app.route("/iterator") +def hello_iterator() -> t.Iterator[str]: + return iter([f"data:{x}\n\n" for x in range(100)]) + + @app.route("/status") @app.route("/status/") def tuple_status(code: int = 200) -> tuple[str, int]: @@ -48,6 +69,11 @@ 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") + + class RenderTemplateView(View): def __init__(self: RenderTemplateView, template_name: str) -> None: self.template_name = template_name From 46433e9807a1c960d6b2bb0e125cf16a90167d97 Mon Sep 17 00:00:00 2001 From: pgjones Date: Thu, 9 Jun 2022 09:21:48 +0100 Subject: [PATCH 054/229] add generate_template and generate_template_string functions --- CHANGES.rst | 2 + docs/api.rst | 4 ++ docs/patterns/streaming.rst | 81 ++++++++++++++++++++----------------- docs/templating.rst | 26 ++++++++++++ src/flask/__init__.py | 2 + src/flask/templating.py | 58 +++++++++++++++++++++++++- tests/test_templating.py | 9 +++++ 7 files changed, 142 insertions(+), 40 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 694d7a5679..64b4ef0b2a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -40,6 +40,8 @@ Unreleased value. :pr:`4645` - View functions can return generators directly instead of wrapping them in a ``Response``. :pr:`4629` +- Add ``stream_template`` and ``stream_template_string`` functions to + render a template as a stream of pieces. :pr:`4629` Version 2.1.3 diff --git a/docs/api.rst b/docs/api.rst index b3cffde26f..217473bc39 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -287,6 +287,10 @@ Template Rendering .. autofunction:: render_template_string +.. autofunction:: stream_template + +.. autofunction:: stream_template_string + .. autofunction:: get_template_attribute Configuration diff --git a/docs/patterns/streaming.rst b/docs/patterns/streaming.rst index e8571ffdad..e35ac4ab52 100644 --- a/docs/patterns/streaming.rst +++ b/docs/patterns/streaming.rst @@ -20,7 +20,7 @@ data and to then invoke that function and pass it to a response object:: def generate(): for row in iter_all_rows(): yield f"{','.join(row)}\n" - return app.response_class(generate(), mimetype='text/csv') + return generate(), {"Content-Type": "text/csv") Each ``yield`` expression is directly sent to the browser. Note though that some WSGI middlewares might break streaming, so be careful there in @@ -29,52 +29,57 @@ debug environments with profilers and other things you might have enabled. Streaming from Templates ------------------------ -The Jinja2 template engine also supports rendering templates piece by -piece. This functionality is not directly exposed by Flask because it is -quite uncommon, but you can easily do it yourself:: - - def stream_template(template_name, **context): - app.update_template_context(context) - t = app.jinja_env.get_template(template_name) - rv = t.stream(context) - rv.enable_buffering(5) - return rv - - @app.route('/my-large-page.html') - def render_large_template(): - rows = iter_all_rows() - return app.response_class(stream_template('the_template.html', rows=rows)) - -The trick here is to get the template object from the Jinja2 environment -on the application and to call :meth:`~jinja2.Template.stream` instead of -:meth:`~jinja2.Template.render` which returns a stream object instead of a -string. Since we're bypassing the Flask template render functions and -using the template object itself we have to make sure to update the render -context ourselves by calling :meth:`~flask.Flask.update_template_context`. -The template is then evaluated as the stream is iterated over. Since each -time you do a yield the server will flush the content to the client you -might want to buffer up a few items in the template which you can do with -``rv.enable_buffering(size)``. ``5`` is a sane default. +The Jinja2 template engine supports rendering a template piece by +piece, returning an iterator of strings. Flask provides the +:func:`~flask.stream_template` and :func:`~flask.stream_template_string` +functions to make this easier to use. + +.. code-block:: python + + from flask import stream_template + + @app.get("/timeline") + def timeline(): + return stream_template("timeline.html") + +The parts yielded by the render stream tend to match statement blocks in +the template. + Streaming with Context ---------------------- -.. versionadded:: 0.9 +The :data:`~flask.request` will not be active while the generator is +running, because the view has already returned at that point. If you try +to access ``request``, you'll get a ``RuntimeError``. -Note that when you stream data, the request context is already gone the -moment the function executes. Flask 0.9 provides you with a helper that -can keep the request context around during the execution of the -generator:: +If your generator function relies on data in ``request``, use the +:func:`~flask.stream_with_context` wrapper. This will keep the request +context active during the generator. + +.. code-block:: python from flask import stream_with_context, request + from markupsafe import escape @app.route('/stream') def streamed_response(): def generate(): - yield 'Hello ' - yield request.args['name'] - yield '!' - return app.response_class(stream_with_context(generate())) + yield '

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

' + return stream_with_context(generate()) + +It can also be used as a decorator. + +.. code-block:: python + + @stream_with_context + def generate(): + ... + + return generate() -Without the :func:`~flask.stream_with_context` function you would get a -:class:`RuntimeError` at that point. +The :func:`~flask.stream_template` and +:func:`~flask.stream_template_string` functions automatically +use :func:`~flask.stream_with_context` if a request is active. diff --git a/docs/templating.rst b/docs/templating.rst index dcc757c381..3cda995e44 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -201,3 +201,29 @@ templates:: You could also build `format_price` as a template filter (see :ref:`registering-filters`), but this demonstrates how to pass functions in a context processor. + +Streaming +--------- + +It can be useful to not render the whole template as one complete +string, instead render it as a stream, yielding smaller incremental +strings. This can be used for streaming HTML in chunks to speed up +initial page load, or to save memory when rendering a very large +template. + +The Jinja2 template engine supports rendering a template piece +by piece, returning an iterator of strings. Flask provides the +:func:`~flask.stream_template` and :func:`~flask.stream_template_string` +functions to make this easier to use. + +.. code-block:: python + + from flask import stream_template + + @app.get("/timeline") + def timeline(): + return stream_template("timeline.html") + +These functions automatically apply the +:func:`~flask.stream_with_context` wrapper if a request is active, so +that it remains available in the template. diff --git a/src/flask/__init__.py b/src/flask/__init__.py index bc93e0a34b..8ca1dbad0b 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -41,5 +41,7 @@ from .signals import template_rendered as template_rendered from .templating import render_template as render_template from .templating import render_template_string as render_template_string +from .templating import stream_template as stream_template +from .templating import stream_template_string as stream_template_string __version__ = "2.2.0.dev0" diff --git a/src/flask/templating.py b/src/flask/templating.py index 36a8645c85..7d92cf1e94 100644 --- a/src/flask/templating.py +++ b/src/flask/templating.py @@ -7,6 +7,9 @@ from .globals import _app_ctx_stack from .globals import _request_ctx_stack +from .globals import current_app +from .globals import request +from .helpers import stream_with_context from .signals import before_render_template from .signals import template_rendered @@ -122,8 +125,6 @@ def list_templates(self) -> t.List[str]: def _render(template: Template, context: dict, app: "Flask") -> str: - """Renders the template and fires the signal""" - before_render_template.send(app, template=template, context=context) rv = template.render(context) template_rendered.send(app, template=template, context=context) @@ -164,3 +165,56 @@ def render_template_string(source: str, **context: t.Any) -> str: ctx = _app_ctx_stack.top ctx.app.update_template_context(context) return _render(ctx.app.jinja_env.from_string(source), context, ctx.app) + + +def _stream( + app: "Flask", template: Template, context: t.Dict[str, t.Any] +) -> t.Iterator[str]: + app.update_template_context(context) + before_render_template.send(app, template=template, context=context) + + def generate() -> t.Iterator[str]: + yield from template.generate(context) + template_rendered.send(app, template=template, context=context) + + rv = generate() + + # If a request context is active, keep it while generating. + if request: + rv = stream_with_context(rv) + + return rv + + +def stream_template( + template_name_or_list: t.Union[str, Template, t.List[t.Union[str, Template]]], + **context: t.Any +) -> t.Iterator[str]: + """Render a template by name with the given context as a stream. + This returns an iterator of strings, which can be used as a + streaming response from a view. + + :param template_name_or_list: The name of the template to render. If + a list is given, the first name to exist will be rendered. + :param context: The variables to make available in the template. + + .. versionadded:: 2.2 + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.get_or_select_template(template_name_or_list) + return _stream(app, template, context) + + +def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]: + """Render a template from the given source string with the given + context as a stream. This returns an iterator of strings, which can + be used as a streaming response from a view. + + :param source: The source code of the template to render. + :param context: The variables to make available in the template. + + .. versionadded:: 2.2 + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.from_string(source) + return _stream(app, template, context) diff --git a/tests/test_templating.py b/tests/test_templating.py index a53120ea94..f0d7c1564a 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -29,6 +29,15 @@ def index(): assert rv.data == b"42" +def test_simple_stream(app, client): + @app.route("/") + def index(): + return flask.stream_template_string("{{ config }}", config=42) + + rv = client.get("/") + assert rv.data == b"42" + + def test_request_less_rendering(app, app_ctx): app.config["WORLD_NAME"] = "Special World" From abcb6c96777794da1142b0117f8c57f95cba65b7 Mon Sep 17 00:00:00 2001 From: hankhank10 Date: Thu, 23 Jun 2022 10:59:43 +0100 Subject: [PATCH 055/229] Update javascript.rst --- docs/patterns/javascript.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/patterns/javascript.rst b/docs/patterns/javascript.rst index 046029f44e..dd3bcb9b6c 100644 --- a/docs/patterns/javascript.rst +++ b/docs/patterns/javascript.rst @@ -184,8 +184,8 @@ Replacing Content A response might be new HTML, either a new section of the page to add or replace, or an entirely new page. In general, if you're returning the entire page, it would be better to handle that with a redirect as shown -in the previous section. The following example shows how to a ``
`` -with the HTML returned by a request. +in the previous section. The following example shows how to replace a +``
`` with the HTML returned by a request. .. code-block:: html From 64ab59817dedd7959b4b38d47403c86415f4a322 Mon Sep 17 00:00:00 2001 From: hankhank10 Date: Sun, 26 Jun 2022 10:14:40 +0100 Subject: [PATCH 056/229] show separate HTTP method route decorators in quickstart --- docs/quickstart.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index a6956c3201..ed345aadef 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -390,6 +390,24 @@ of the :meth:`~flask.Flask.route` decorator to handle different HTTP methods. else: return show_the_login_form() +The example above keeps all methods for the route within one function, +which can be useful if each part uses some common data. + +You can also separate views for different methods into different +functions. Flask provides a shortcut for decorating such routes with +:meth:`~flask.Flask.get`, :meth:`~flask.Flask.post`, etc. for each +common HTTP method. + +.. code-block:: python + + @app.get('/login') + def login_get(): + return show_the_login_form() + + @app.post('/login') + def login_post(): + return do_the_login() + If ``GET`` is present, Flask automatically adds support for the ``HEAD`` method and handles ``HEAD`` requests according to the `HTTP RFC`_. Likewise, ``OPTIONS`` is automatically implemented for you. From b46bfcfa63e3c7b32b66dd4870f1c8c17fe1cb46 Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 28 Jun 2022 16:28:33 -0700 Subject: [PATCH 057/229] rewrite extension development docs --- docs/extensiondev.rst | 568 ++++++++++++++++++++---------------------- docs/extensions.rst | 8 +- 2 files changed, 277 insertions(+), 299 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 34b118ce5a..25ced1870c 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -1,310 +1,284 @@ Flask Extension Development =========================== -Flask, being a microframework, often requires some repetitive steps to get -a third party library working. Many such extensions are already available -on `PyPI`_. - -If you want to create your own Flask extension for something that does not -exist yet, this guide to extension development will help you get your -extension running in no time and to feel like users would expect your -extension to behave. - -Anatomy of an Extension ------------------------ - -Extensions are all located in a package called ``flask_something`` -where "something" is the name of the library you want to bridge. So for -example if you plan to add support for a library named `simplexml` to -Flask, you would name your extension's package ``flask_simplexml``. - -The name of the actual extension (the human readable name) however would -be something like "Flask-SimpleXML". Make sure to include the name -"Flask" somewhere in that name and that you check the capitalization. -This is how users can then register dependencies to your extension in -their :file:`setup.py` files. - -But what do extensions look like themselves? An extension has to ensure -that it works with multiple Flask application instances at once. This is -a requirement because many people will use patterns like the -:doc:`/patterns/appfactories` pattern to create their application as -needed to aid unittests and to support multiple configurations. Because -of that it is crucial that your application supports that kind of -behavior. - -Most importantly the extension must be shipped with a :file:`setup.py` file and -registered on PyPI. Also the development checkout link should work so -that people can easily install the development version into their -virtualenv without having to download the library by hand. - -Flask extensions must be licensed under a BSD, MIT or more liberal license -in order to be listed in the Flask Extension Registry. Keep in mind -that the Flask Extension Registry is a moderated place and libraries will -be reviewed upfront if they behave as required. - -"Hello Flaskext!" ------------------ - -So let's get started with creating such a Flask extension. The extension -we want to create here will provide very basic support for SQLite3. - -First we create the following folder structure:: - - flask-sqlite3/ - flask_sqlite3.py - LICENSE - README - -Here's the contents of the most important files: - -setup.py -```````` - -The next file that is absolutely required is the :file:`setup.py` file which is -used to install your Flask extension. The following contents are -something you can work with:: - - """ - Flask-SQLite3 - ------------- - - This is the description for that library - """ - from setuptools import setup - - - setup( - name='Flask-SQLite3', - version='1.0', - url='http://example.com/flask-sqlite3/', - license='BSD', - author='Your Name', - author_email='your-email@example.com', - description='Very short description', - long_description=__doc__, - py_modules=['flask_sqlite3'], - # if you would be using a package instead use packages instead - # of py_modules: - # packages=['flask_sqlite3'], - zip_safe=False, - include_package_data=True, - platforms='any', - install_requires=[ - 'Flask' - ], - classifiers=[ - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - 'Topic :: Software Development :: Libraries :: Python Modules' - ] - ) - -That's a lot of code but you can really just copy/paste that from existing -extensions and adapt. - -flask_sqlite3.py -```````````````` - -Now this is where your extension code goes. But how exactly should such -an extension look like? What are the best practices? Continue reading -for some insight. - -Initializing Extensions ------------------------ - -Many extensions will need some kind of initialization step. For example, -consider an application that's currently connecting to SQLite like the -documentation suggests (:doc:`/patterns/sqlite3`). So how does the -extension know the name of the application object? - -Quite simple: you pass it to it. - -There are two recommended ways for an extension to initialize: - -initialization functions: - - If your extension is called `helloworld` you might have a function - called ``init_helloworld(app[, extra_args])`` that initializes the - extension for that application. It could attach before / after - handlers etc. +.. currentmodule:: flask -classes: +Extensions are extra packages that add functionality to a Flask +application. While `PyPI`_ contains many Flask extensions, you may not +find one that fits your need. If this is the case, you can create your +own, and publish it for others to use as well. - Classes work mostly like initialization functions but can later be - used to further change the behavior. +This guide will show how to create a Flask extension, and some of the +common patterns and requirements involved. Since extensions can do +anything, this guide won't be able to cover every possibility. -What to use depends on what you have in mind. For the SQLite 3 extension -we will use the class-based approach because it will provide users with an -object that handles opening and closing database connections. +The best ways to learn about extensions are to look at how other +extensions you use are written, and discuss with others. Discuss your +design ideas with others on our `Discord Chat`_ or +`GitHub Discussions`_. -When designing your classes, it's important to make them easily reusable -at the module level. This means the object itself must not under any -circumstances store any application specific state and must be shareable -between different applications. +The best extensions share common patterns, so that anyone familiar with +using one extension won't feel completely lost with another. This can +only work if collaboration happens early. -The Extension Code ------------------- - -Here's the contents of the `flask_sqlite3.py` for copy/paste:: - - import sqlite3 - from flask import current_app, _app_ctx_stack +Naming +------ - class SQLite3(object): - def __init__(self, app=None): - self.app = app - if app is not None: - self.init_app(app) - - def init_app(self, app): - app.config.setdefault('SQLITE3_DATABASE', ':memory:') - app.teardown_appcontext(self.teardown) - - def connect(self): - return sqlite3.connect(current_app.config['SQLITE3_DATABASE']) - - def teardown(self, exception): - ctx = _app_ctx_stack.top - if hasattr(ctx, 'sqlite3_db'): - ctx.sqlite3_db.close() - - @property - def connection(self): - ctx = _app_ctx_stack.top - if ctx is not None: - if not hasattr(ctx, 'sqlite3_db'): - ctx.sqlite3_db = self.connect() - return ctx.sqlite3_db - - -So here's what these lines of code do: - -1. The ``__init__`` method takes an optional app object and, if supplied, will - call ``init_app``. -2. The ``init_app`` method exists so that the ``SQLite3`` object can be - instantiated without requiring an app object. This method supports the - factory pattern for creating applications. The ``init_app`` will set the - configuration for the database, defaulting to an in memory database if - no configuration is supplied. In addition, the ``init_app`` method - attaches the ``teardown`` handler. -3. Next, we define a ``connect`` method that opens a database connection. -4. Finally, we add a ``connection`` property that on first access opens - the database connection and stores it on the context. This is also - the recommended way to handling resources: fetch resources lazily the - first time they are used. - - Note here that we're attaching our database connection to the top - application context via ``_app_ctx_stack.top``. Extensions should use - the top context for storing their own information with a sufficiently - complex name. - -So why did we decide on a class-based approach here? Because using our -extension looks something like this:: - - from flask import Flask - from flask_sqlite3 import SQLite3 - - app = Flask(__name__) - app.config.from_pyfile('the-config.cfg') - db = SQLite3(app) - -You can then use the database from views like this:: - - @app.route('/') - def show_all(): - cur = db.connection.cursor() - cur.execute(...) - -Likewise if you are outside of a request you can use the database by -pushing an app context:: - - with app.app_context(): - cur = db.connection.cursor() - cur.execute(...) - -At the end of the ``with`` block the teardown handles will be executed -automatically. - -Additionally, the ``init_app`` method is used to support the factory pattern -for creating apps:: - - db = SQLite3() - # Then later on. - app = create_app('the-config.cfg') - db.init_app(app) - -Keep in mind that supporting this factory pattern for creating apps is required -for approved flask extensions (described below). - -.. admonition:: Note on ``init_app`` +A Flask extension typically has ``flask`` in its name as a prefix or +suffix. If it wraps another library, it should include the library name +as well. This makes it easy to search for extensions, and makes their +purpose clearer. - As you noticed, ``init_app`` does not assign ``app`` to ``self``. This - is intentional! Class based Flask extensions must only store the - application on the object when the application was passed to the - constructor. This tells the extension: I am not interested in using - multiple applications. +A general Python packaging recommendation is that the install name from +the package index and the name used in ``import`` statements should be +related. The import name is lowercase, with words separated by +underscores (``_``). The install name is either lower case or title +case, with words separated by dashes (``-``). If it wraps another +library, prefer using the same case as that library's name. - When the extension needs to find the current application and it does - not have a reference to it, it must either use the - :data:`~flask.current_app` context local or change the API in a way - that you can pass the application explicitly. +Here are some example install and import names: +- ``Flask-Name`` imported as ``flask_name`` +- ``flask-name-lower`` imported as ``flask_name_lower`` +- ``Flask-ComboName`` imported as ``flask_comboname`` +- ``Name-Flask`` imported as ``name_flask`` -Using _app_ctx_stack --------------------- -In the example above, before every request, a ``sqlite3_db`` variable is -assigned to ``_app_ctx_stack.top``. In a view function, this variable is -accessible using the ``connection`` property of ``SQLite3``. During the -teardown of a request, the ``sqlite3_db`` connection is closed. By using -this pattern, the *same* connection to the sqlite3 database is accessible -to anything that needs it for the duration of the request. +The Extension Class and Initialization +-------------------------------------- +All extensions will need some entry point that initializes the +extension with the application. The most common pattern is to create a +class that represents the extension's configuration and behavior, with +an ``init_app`` method to apply the extension instance to the given +application instance. -Learn from Others ------------------ +.. code-block:: python -This documentation only touches the bare minimum for extension development. -If you want to learn more, it's a very good idea to check out existing extensions -on `PyPI`_. If you feel lost there is `Discord Chat`_ or -`GitHub Discussions`_ to get some ideas for nice looking APIs. Especially if you do -something nobody before you did, it might be a very good idea to get some more -input. This not only generates useful feedback on what people might want from -an extension, but also avoids having multiple developers working in isolation -on pretty much the same problem. - -Remember: good API design is hard, so introduce your project on -`Discord Chat`_ or `GitHub Discussions`_, and let other developers give -you a helping hand with designing the API. + class HelloExtension: + def __init__(self, app=None): + if app is not None: + self.init_app(app) -The best Flask extensions are extensions that share common idioms for the -API. And this can only work if collaboration happens early. + def init_app(self, app): + app.before_request(...) + +It is important that the app is not stored on the extension, don't do +``self.app = app``. The only time the extension should have direct +access to an app is during ``init_app``, otherwise it should use +:data:`current_app`. + +This allows the extension to support the application factory pattern, +avoids circular import issues when importing the extension instance +elsewhere in a user's code, and makes testing with different +configurations easier. + +.. code-block:: python + + hello = HelloExtension() + + def create_app(): + app = Flask(__name__) + hello.init_app(app) + return app + +Above, the ``hello`` extension instance exists independently of the +application. This means that other modules in a user's project can do +``from project import hello`` and use the extension in blueprints before +the app exists. + +The :attr:`Flask.extensions` dict can be used to store a reference to +the extension on the application, or some other state specific to the +application. Be aware that this is a single namespace, so use a name +unique to your extension, such as the extension's name without the +"flask" prefix. + + +Adding Behavior +--------------- + +There are many ways that an extension can add behavior. Any setup +methods that are available on the :class:`Flask` object can be used +during an extension's ``init_app`` method. + +A common pattern is to use :meth:`~Flask.before_request` to initialize +some data or a connection at the beginning of each request, then +:meth:`~Flask.teardown_request` to clean it up at the end. This can be +stored on :data:`g`, discussed more below. + +A more lazy approach is to provide a method that initializes and caches +the data or connection. For example, a ``ext.get_db`` method could +create a database connection the first time it's called, so that a view +that doesn't use the database doesn't create a connection. + +Besides doing something before and after every view, your extension +might want to add some specific views as well. In this case, you could +define a :class:`Blueprint`, then call :meth:`~Flask.register_blueprint` +during ``init_app`` to add the blueprint to the app. + + +Configuration Techniques +------------------------ + +There can be multiple levels and sources of configuration for an +extension. You should consider what parts of your extension fall into +each one. + +- Configuration per application instance, through ``app.config`` + values. This is configuration that could reasonably change for each + deployment of an application. A common example is a URL to an + external resource, such as a database. Configuration keys should + start with the extension's name so that they don't interfere with + other extensions. +- Configuration per extension instance, through ``__init__`` + arguments. This configuration usually affects how the extension + is used, such that it wouldn't make sense to change it per + deployment. +- Configuration per extension instance, through instance attributes + and decorator methods. It might be more ergonomic to assign to + ``ext.value``, or use a ``@ext.register`` decorator to register a + function, after the extension instance has been created. +- Global configuration through class attributes. Changing a class + attribute like ``Ext.connection_class`` can customize default + behavior without making a subclass. This could be combined + per-extension configuration to override defaults. +- Subclassing and overriding methods and attributes. Making the API of + the extension itself something that can be overridden provides a + very powerful tool for advanced customization. + +The :class:`~flask.Flask` object itself uses all of these techniques. + +It's up to you to decide what configuration is appropriate for your +extension, based on what you need and what you want to support. + +Configuration should not be changed after the application setup phase is +complete and the server begins handling requests. Configuration is +global, any changes to it are not guaranteed to be visible to other +workers. + + +Data During a Request +--------------------- + +When writing a Flask application, the :data:`~flask.g` object is used to +store information during a request. For example the +:doc:`tutorial ` stores a connection to a SQLite +database as ``g.db``. Extensions can also use this, with some care. +Since ``g`` is a single global namespace, extensions must use unique +names that won't collide with user data. For example, use the extension +name as a prefix, or as a namespace. + +.. code-block:: python + + # an internal prefix with the extension name + g._hello_user_id = 2 + + # or an internal prefix as a namespace + from types import SimpleNamespace + g._hello = SimpleNamespace() + g._hello.user_id = 2 + +The data in ``g`` lasts for an application context. An application +context is active when a request context is, or when a CLI command is +run. If you're storing something that should be closed, use +:meth:`~flask.Flask.teardown_appcontext` to ensure that it gets closed +when the application context ends. If it should only be valid during a +request, or would not be used in the CLI outside a reqeust, use +:meth:`~flask.Flask.teardown_request`. + +An older technique for storing context data was to store it on +``_app_ctx_stack.top`` or ``_request_ctx_stack.top``. However, this just +moves the same namespace collision problem elsewhere (although less +likely) and modifies objects that are very internal to Flask's +operations. Prefer storing data under a unique name in ``g``. + + +Views and Models +---------------- + +Your extension views might want to interact with specific models in your +database, or some other extension or data connected to your application. +For example, let's consider a ``Flask-SimpleBlog`` extension that works +with Flask-SQLAlchemy to provide a ``Post`` model and views to write +and read posts. + +The ``Post`` model needs to subclass the Flask-SQLAlchemy ``db.Model`` +object, but that's only available once you've created an instance of +that extension, not when your extension is defining its views. So how +can the view code, defined before the model exists, access the model? + +One method could be to use :doc:`views`. During ``__init__``, create +the model, then create the views by passing the model to the view +class's :meth:`~views.View.as_view` method. + +.. code-block:: python + + class PostAPI(MethodView): + def __init__(self, model): + self.model = model + + def get(id): + post = self.model.query.get(id) + return jsonify(post.to_json()) + + class BlogExtension: + def __init__(self, db): + class Post(db.Model): + id = db.Column(primary_key=True) + title = db.Column(db.String, nullable=False) + + self.post_model = Post -Approved Extensions -------------------- + def init_app(self, app): + api_view = PostAPI.as_view(model=self.post_model) -Flask previously had the concept of approved extensions. These came with -some vetting of support and compatibility. While this list became too -difficult to maintain over time, the guidelines are still relevant to -all extensions maintained and developed today, as they help the Flask + db = SQLAlchemy() + blog = BlogExtension(db) + db.init_app(app) + blog.init_app(app) + +Another technique could be to use an attribute on the extension, such as +``self.post_model`` from above. Add the extension to ``app.extensions`` +in ``init_app``, then access +``current_app.extensions["simple_blog"].post_model`` from views. + +You may also want to provide base classes so that users can provide +their own ``Post`` model that conforms to the API your extension +expects. So they could implement ``class Post(blog.BasePost)``, then +set it as ``blog.post_model``. + +As you can see, this can get a bit complex. Unfortunately, there's no +perfect solution here, only different strategies and tradeoffs depending +on your needs and how much customization you want to offer. Luckily, +this sort of resource dependency is not a common need for most +extensions. Remember, if you need help with design, ask on our +`Discord Chat`_ or `GitHub Discussions`_. + + +Recommended Extension Guidelines +-------------------------------- + +Flask previously had the concept of "approved extensions", where the +Flask maintainers evaluated the quality, support, and compatibility of +the extensions before listing them. While the list became too difficult +to maintain over time, the guidelines are still relevant to all +extensions maintained and developed today, as they help the Flask ecosystem remain consistent and compatible. -0. An approved Flask extension requires a maintainer. In the event an - extension author would like to move beyond the project, the project - should find a new maintainer and transfer access to the repository, - documentation, PyPI, and any other services. If no maintainer - is available, give access to the Pallets core team. -1. The naming scheme is *Flask-ExtensionName* or *ExtensionName-Flask*. +1. An extension requires a maintainer. In the event an extension author + would like to move beyond the project, the project should find a new + maintainer and transfer access to the repository, documentation, + PyPI, and any other services. The `Pallets-Eco`_ organization on + GitHub allows for community maintenance with oversight from the + Pallets maintainers. +2. The naming scheme is *Flask-ExtensionName* or *ExtensionName-Flask*. It must provide exactly one package or module named ``flask_extension_name``. -2. The extension must be BSD or MIT licensed. It must be open source - and publicly available. -3. The extension's API must have the following characteristics: +3. The extension must use an open source license. The Python web + ecosystem tends to prefer BSD or MIT. It must be open source and + publicly available. +4. The extension's API must have the following characteristics: - It must support multiple applications running in the same Python process. Use ``current_app`` instead of ``self.app``, store @@ -312,21 +286,25 @@ ecosystem remain consistent and compatible. - It must be possible to use the factory pattern for creating applications. Use the ``ext.init_app()`` pattern. -4. From a clone of the repository, an extension with its dependencies - must be installable with ``pip install -e .``. -5. It must ship a testing suite that can be invoked with ``tox -e py`` - or ``pytest``. If not using ``tox``, the test dependencies should be - specified in a ``requirements.txt`` file. The tests must be part of - the sdist distribution. -6. The documentation must use the ``flask`` theme from the - `Official Pallets Themes`_. A link to the documentation or project - website must be in the PyPI metadata or the readme. -7. For maximum compatibility, the extension should support the same - versions of Python that Flask supports. 3.7+ is recommended as of - December 2021. Use ``python_requires=">= 3.7"`` in ``setup.py`` to - indicate supported versions. +5. From a clone of the repository, an extension with its dependencies + must be installable in editable mode with ``pip install -e .``. +6. It must ship tests that can be invoked with a common tool like + ``tox -e py``, ``nox -s test`` or ``pytest``. If not using ``tox``, + the test dependencies should be specified in a requirements file. + The tests must be part of the sdist distribution. +7. A link to the documentation or project website must be in the PyPI + metadata or the readme. The documentation should use the Flask theme + from the `Official Pallets Themes`_. +8. The extension's dependencies should not use upper bounds or assume + any particular version scheme, but should use lower bounds to + indicate minimum compatibility support. For example, + ``sqlalchemy>=1.4``. +9. Indicate the versions of Python supported using + ``python_requires=">=version"``. Flask itself supports Python >=3.7 + as of December 2021, but this will update over time. .. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask .. _Discord Chat: https://discord.gg/pallets .. _GitHub Discussions: https://github.com/pallets/flask/discussions .. _Official Pallets Themes: https://pypi.org/project/Pallets-Sphinx-Themes/ +.. _Pallets-Eco: https://github.com/pallets-eco diff --git a/docs/extensions.rst b/docs/extensions.rst index 784fd807a7..4713ec8e2d 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -39,10 +39,10 @@ an extension called "Flask-Foo" might be used like this:: Building Extensions ------------------- -While the `PyPI `_ contains many Flask extensions, you may -not find an extension that fits your need. If this is the case, you can -create your own. Read :doc:`/extensiondev` to develop your own Flask -extension. +While `PyPI `_ contains many Flask extensions, you may not find +an extension that fits your need. If this is the case, you can create +your own, and publish it for others to use as well. Read +:doc:`extensiondev` to develop your own Flask extension. .. _pypi: https://pypi.org/search/?c=Framework+%3A%3A+Flask From 5544d09477af3221d988ec18b086930c2d9e67cd Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 29 Jun 2022 21:11:58 -0700 Subject: [PATCH 058/229] re-add flag to skip unguarded app.run from CLI --- src/flask/app.py | 4 ++-- src/flask/cli.py | 5 +++++ tests/test_cli.py | 7 ++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 236a47a8de..22dd395398 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -916,8 +916,8 @@ def run( if os.environ.get("FLASK_RUN_FROM_CLI") == "true": if not is_running_from_reloader(): click.secho( - " * Ignoring a call to 'app.run()', the server is" - " already being run with the 'flask run' command.\n" + " * Ignoring a call to 'app.run()' that would block" + " the current 'flask' CLI command.\n" " Only call 'app.run()' in an 'if __name__ ==" ' "__main__"\' guard.', fg="red", diff --git a/src/flask/cli.py b/src/flask/cli.py index 321794b69b..af29b2c1a7 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -715,6 +715,11 @@ def make_context( parent: click.Context | None = None, **extra: t.Any, ) -> click.Context: + # Set a flag to tell app.run to become a no-op. If app.run was + # not in a __name__ == __main__ guard, it would start the server + # when importing, blocking whatever command is being called. + os.environ["FLASK_RUN_FROM_CLI"] = "true" + # Attempt to load .env and .flask env files. The --env-file # option can cause another file to be loaded. if get_load_dotenv(self.load_dotenv): diff --git a/tests/test_cli.py b/tests/test_cli.py index 7d83d86527..fdd2212b61 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -13,7 +13,6 @@ from _pytest.monkeypatch import notset from click.testing import CliRunner -from flask import _app_ctx_stack from flask import Blueprint from flask import current_app from flask import Flask @@ -322,13 +321,11 @@ def check(value): app = click.get_current_context().obj.load_app() # the loaded app should be the same as current_app same_app = current_app._get_current_object() is app - # only one app context should be pushed - stack_size = len(_app_ctx_stack._local.stack) - return same_app, stack_size, value + return same_app, value cli = FlaskGroup(create_app=lambda: app) result = runner.invoke(cli, ["check", "x"], standalone_mode=False) - assert result.return_value == (True, 1, True) + assert result.return_value == (True, True) def test_with_appcontext(runner): From 2589328485f51ad17afda682dd84a83da4e4e38e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Jul 2022 16:05:52 +0000 Subject: [PATCH 059/229] Bump actions/setup-python from 3 to 4 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 4. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7ad21db9ce..674fb8b73b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -37,7 +37,7 @@ jobs: - {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing} steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} cache: 'pip' From 84c722044ad03f7f5384f5314c8350b3a13d8dfa Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 29 Jun 2022 21:02:44 -0700 Subject: [PATCH 060/229] new debug/test preserve context implementation --- CHANGES.rst | 12 ++++++++ docs/config.rst | 11 ++----- docs/reqcontext.rst | 19 ------------ src/flask/app.py | 23 +++++---------- src/flask/ctx.py | 69 ++++++++++++------------------------------- src/flask/scaffold.py | 7 ----- src/flask/testing.py | 43 +++++++++++++++------------ tests/test_basic.py | 60 ++----------------------------------- tests/test_signals.py | 26 +++++++--------- tests/test_testing.py | 34 ++++----------------- 10 files changed, 84 insertions(+), 220 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 64b4ef0b2a..cc96f441bf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -42,6 +42,18 @@ Unreleased them in a ``Response``. :pr:`4629` - Add ``stream_template`` and ``stream_template_string`` functions to render a template as a stream of pieces. :pr:`4629` +- A new implementation of context preservation during debugging and + testing. :pr:`4666` + + - ``request``, ``g``, and other context-locals point to the + correct data when running code in the interactive debugger + console. :issue:`2836` + - Teardown functions are always run at the end of the request, + even if the context is preserved. They are also run after the + preserved context is popped. + - ``stream_with_context`` preserves context separately from a + ``with client`` block. It will be cleaned up when + ``response.get_data()`` or ``response.close()`` is called. Version 2.1.3 diff --git a/docs/config.rst b/docs/config.rst index 12170e9041..ebe29d0500 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -126,14 +126,6 @@ The following configuration values are used internally by Flask: Default: ``None`` -.. py:data:: PRESERVE_CONTEXT_ON_EXCEPTION - - Don't pop the request context when an exception occurs. If not set, this - is true if ``DEBUG`` is true. This allows debuggers to introspect the - request data on errors, and should normally not need to be set directly. - - Default: ``None`` - .. py:data:: TRAP_HTTP_EXCEPTIONS If there is no handler for an ``HTTPException``-type exception, re-raise it @@ -392,6 +384,9 @@ The following configuration values are used internally by Flask: Added :data:`MAX_COOKIE_SIZE` to control a warning from Werkzeug. +.. versionchanged:: 2.2 + Removed ``PRESERVE_CONTEXT_ON_EXCEPTION``. + Configuring from Python Files ----------------------------- diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index f395e844b0..96e17e3b9a 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -219,25 +219,6 @@ sent: :meth:`~Flask.teardown_request` functions are called. -Context Preservation on Error ------------------------------ - -At the end of a request, the request context is popped and all data -associated with it is destroyed. If an error occurs during development, -it is useful to delay destroying the data for debugging purposes. - -When the development server is running in development mode (the -``--env`` option is set to ``'development'``), the error and data will -be preserved and shown in the interactive debugger. - -This behavior can be controlled with the -:data:`PRESERVE_CONTEXT_ON_EXCEPTION` config. As described above, it -defaults to ``True`` in the development environment. - -Do not enable :data:`PRESERVE_CONTEXT_ON_EXCEPTION` in production, as it -will cause your application to leak memory on exceptions. - - .. _notes-on-proxies: Notes On Proxies diff --git a/src/flask/app.py b/src/flask/app.py index 22dd395398..5a8223e58e 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -331,7 +331,6 @@ class Flask(Scaffold): "DEBUG": None, "TESTING": False, "PROPAGATE_EXCEPTIONS": None, - "PRESERVE_CONTEXT_ON_EXCEPTION": None, "SECRET_KEY": None, "PERMANENT_SESSION_LIFETIME": timedelta(days=31), "USE_X_SENDFILE": False, @@ -583,19 +582,6 @@ def propagate_exceptions(self) -> bool: return rv return self.testing or self.debug - @property - def preserve_context_on_exception(self) -> bool: - """Returns the value of the ``PRESERVE_CONTEXT_ON_EXCEPTION`` - configuration value in case it's set, otherwise a sensible default - is returned. - - .. versionadded:: 0.7 - """ - rv = self.config["PRESERVE_CONTEXT_ON_EXCEPTION"] - if rv is not None: - return rv - return self.debug - @locked_cached_property def logger(self) -> logging.Logger: """A standard Python :class:`~logging.Logger` for the app, with @@ -2301,9 +2287,14 @@ def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any: raise return response(environ, start_response) finally: - if self.should_ignore_error(error): + if "werkzeug.debug.preserve_context" in environ: + environ["werkzeug.debug.preserve_context"](_app_ctx_stack.top) + environ["werkzeug.debug.preserve_context"](_request_ctx_stack.top) + + if error is not None and self.should_ignore_error(error): error = None - ctx.auto_pop(error) + + ctx.pop(error) def __call__(self, environ: dict, start_response: t.Callable) -> t.Any: """The WSGI server calls the Flask application object as the diff --git a/src/flask/ctx.py b/src/flask/ctx.py index 2b2ebb758f..caecbcc2b2 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -289,20 +289,12 @@ class RequestContext: functions registered on the application for teardown execution (:meth:`~flask.Flask.teardown_request`). - The request context is automatically popped at the end of the request - for you. In debug mode the request context is kept around if - exceptions happen so that interactive debuggers have a chance to - introspect the data. With 0.4 this can also be forced for requests - that did not fail and outside of ``DEBUG`` mode. By setting - ``'flask._preserve_context'`` to ``True`` on the WSGI environment the - context will not pop itself at the end of the request. This is used by - the :meth:`~flask.Flask.test_client` for example to implement the - deferred cleanup functionality. - - You might find this helpful for unittests where you need the - information from the context local around for a little longer. Make - sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in - that situation, otherwise your unittests will leak memory. + The request context is automatically popped at the end of the + request. When using the interactive debugger, the context will be + restored so ``request`` is still accessible. Similarly, the test + client can preserve the context after the request ends. However, + teardown functions may already have closed some resources such as + database connections. """ def __init__( @@ -330,14 +322,6 @@ def __init__( # one is created implicitly so for each level we add this information self._implicit_app_ctx_stack: t.List[t.Optional["AppContext"]] = [] - # indicator if the context was preserved. Next time another context - # is pushed the preserved context is popped. - self.preserved = False - - # remembers the exception for pop if there is one in case the context - # preservation kicks in. - self._preserved_exc = None - # Functions that should be executed after the request on the response # object. These will be called before the regular "after_request" # functions. @@ -400,19 +384,6 @@ def match_request(self) -> None: self.request.routing_exception = e def push(self) -> None: - """Binds the request context to the current context.""" - # If an exception occurs in debug mode or if context preservation is - # activated under exception situations exactly one context stays - # on the stack. The rationale is that you want to access that - # information under debug situations. However if someone forgets to - # pop that context again we want to make sure that on the next push - # it's invalidated, otherwise we run at risk that something leaks - # memory. This is usually only a problem in test suite since this - # functionality is not active in production environments. - top = _request_ctx_stack.top - if top is not None and top.preserved: - top.pop(top._preserved_exc) - # Before we push the request context we have to ensure that there # is an application context. app_ctx = _app_ctx_stack.top @@ -454,8 +425,6 @@ def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: igno try: if not self._implicit_app_ctx_stack: - self.preserved = False - self._preserved_exc = None if exc is _sentinel: exc = sys.exc_info()[1] self.app.do_teardown_request(exc) @@ -481,13 +450,18 @@ def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: igno ), f"Popped wrong request context. ({rv!r} instead of {self!r})" def auto_pop(self, exc: t.Optional[BaseException]) -> None: - if self.request.environ.get("flask._preserve_context") or ( - exc is not None and self.app.preserve_context_on_exception - ): - self.preserved = True - self._preserved_exc = exc # type: ignore - else: - self.pop(exc) + """ + .. deprecated:: 2.2 + Will be removed in Flask 2.3. + """ + import warnings + + warnings.warn( + "'ctx.auto_pop' is deprecated and will be removed in Flask 2.3.", + DeprecationWarning, + stacklevel=2, + ) + self.pop(exc) def __enter__(self) -> "RequestContext": self.push() @@ -499,12 +473,7 @@ def __exit__( exc_value: t.Optional[BaseException], tb: t.Optional[TracebackType], ) -> None: - # do not pop the request stack if we are in debug mode and an - # exception happened. This will allow the debugger to still - # access the request object in the interactive shell. Furthermore - # the context can be force kept alive for the test client. - # See flask.testing for how this works. - self.auto_pop(exc_value) + self.pop(exc_value) def __repr__(self) -> str: return ( diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 54d42d1de8..418b24ae66 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -600,13 +600,6 @@ def teardown_request(self, f: ft.TeardownCallable) -> ft.TeardownCallable: be passed an error object. The return values of teardown functions are ignored. - - .. admonition:: Debug Note - - In debug mode Flask will not tear down a request on an exception - immediately. Instead it will keep it alive so that the interactive - debugger can still access it. This behavior can be controlled - by the ``PRESERVE_CONTEXT_ON_EXCEPTION`` configuration variable. """ self.teardown_request_funcs.setdefault(None, []).append(f) return f diff --git a/src/flask/testing.py b/src/flask/testing.py index df69d90303..f652c23a02 100644 --- a/src/flask/testing.py +++ b/src/flask/testing.py @@ -1,5 +1,6 @@ import typing as t from contextlib import contextmanager +from contextlib import ExitStack from copy import copy from types import TracebackType @@ -108,10 +109,12 @@ class FlaskClient(Client): """ application: "Flask" - preserve_context = False def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: super().__init__(*args, **kwargs) + self.preserve_context = False + self._new_contexts: t.List[t.ContextManager[t.Any]] = [] + self._context_stack = ExitStack() self.environ_base = { "REMOTE_ADDR": "127.0.0.1", "HTTP_USER_AGENT": f"werkzeug/{werkzeug.__version__}", @@ -173,11 +176,12 @@ def session_transaction( self.cookie_jar.extract_wsgi(c.request.environ, headers) def _copy_environ(self, other): - return { - **self.environ_base, - **other, - "flask._preserve_context": self.preserve_context, - } + out = {**self.environ_base, **other} + + if self.preserve_context: + out["werkzeug.debug.preserve_context"] = self._new_contexts.append + + return out def _request_from_builder_args(self, args, kwargs): kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {})) @@ -214,12 +218,24 @@ def open( # request is None request = self._request_from_builder_args(args, kwargs) - return super().open( + # Pop any previously preserved contexts. This prevents contexts + # from being preserved across redirects or multiple requests + # within a single block. + self._context_stack.close() + + response = super().open( request, buffered=buffered, follow_redirects=follow_redirects, ) + # Re-push contexts that were preserved during the request. + while self._new_contexts: + cm = self._new_contexts.pop() + self._context_stack.enter_context(cm) + + return response + def __enter__(self) -> "FlaskClient": if self.preserve_context: raise RuntimeError("Cannot nest client invocations") @@ -233,18 +249,7 @@ def __exit__( tb: t.Optional[TracebackType], ) -> None: self.preserve_context = False - - # Normally the request context is preserved until the next - # request in the same thread comes. When the client exits we - # want to clean up earlier. Pop request contexts until the stack - # is empty or a non-preserved one is found. - while True: - top = _request_ctx_stack.top - - if top is not None and top.preserved: - top.pop() - else: - break + self._context_stack.close() class FlaskCliRunner(CliRunner): diff --git a/tests/test_basic.py b/tests/test_basic.py index 916b7038b7..91ba042f3b 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -928,13 +928,8 @@ def test_baseexception_error_handling(app, client): def broken_func(): raise KeyboardInterrupt() - with client: - with pytest.raises(KeyboardInterrupt): - client.get("/") - - ctx = flask._request_ctx_stack.top - assert ctx.preserved - assert type(ctx._preserved_exc) is KeyboardInterrupt + with pytest.raises(KeyboardInterrupt): + client.get("/") def test_before_request_and_routing_errors(app, client): @@ -1769,57 +1764,6 @@ def for_bar_foo(): assert client.get("/bar/123").data == b"123" -def test_preserve_only_once(app, client): - app.debug = True - - @app.route("/fail") - def fail_func(): - 1 // 0 - - for _x in range(3): - with pytest.raises(ZeroDivisionError): - client.get("/fail") - - assert flask._request_ctx_stack.top is not None - assert flask._app_ctx_stack.top is not None - # implicit appctx disappears too - flask._request_ctx_stack.top.pop() - assert flask._request_ctx_stack.top is None - assert flask._app_ctx_stack.top is None - - -def test_preserve_remembers_exception(app, client): - app.debug = True - errors = [] - - @app.route("/fail") - def fail_func(): - 1 // 0 - - @app.route("/success") - def success_func(): - return "Okay" - - @app.teardown_request - def teardown_handler(exc): - errors.append(exc) - - # After this failure we did not yet call the teardown handler - with pytest.raises(ZeroDivisionError): - client.get("/fail") - assert errors == [] - - # But this request triggers it, and it's an error - client.get("/success") - assert len(errors) == 2 - assert isinstance(errors[0], ZeroDivisionError) - - # At this point another request does nothing. - client.get("/success") - assert len(errors) == 3 - assert errors[1] is None - - def test_get_method_on_g(app_ctx): assert flask.g.get("x") is None assert flask.g.get("x", 11) == 11 diff --git a/tests/test_signals.py b/tests/test_signals.py index 719eb3efa1..8aa6983653 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -123,8 +123,7 @@ def record(sender, exception): flask.got_request_exception.disconnect(record, app) -def test_appcontext_signals(): - app = flask.Flask(__name__) +def test_appcontext_signals(app, client): recorded = [] def record_push(sender, **kwargs): @@ -140,10 +139,8 @@ def index(): flask.appcontext_pushed.connect(record_push, app) flask.appcontext_popped.connect(record_pop, app) try: - with app.test_client() as c: - rv = c.get("/") - assert rv.data == b"Hello" - assert recorded == ["push"] + rv = client.get("/") + assert rv.data == b"Hello" assert recorded == ["push", "pop"] finally: flask.appcontext_pushed.disconnect(record_push, app) @@ -174,12 +171,12 @@ def record(sender, message, category): flask.message_flashed.disconnect(record, app) -def test_appcontext_tearing_down_signal(): - app = flask.Flask(__name__) +def test_appcontext_tearing_down_signal(app, client): + app.testing = False recorded = [] - def record_teardown(sender, **kwargs): - recorded.append(("tear_down", kwargs)) + def record_teardown(sender, exc): + recorded.append(exc) @app.route("/") def index(): @@ -187,10 +184,9 @@ def index(): flask.appcontext_tearing_down.connect(record_teardown, app) try: - with app.test_client() as c: - rv = c.get("/") - assert rv.status_code == 500 - assert recorded == [] - assert recorded == [("tear_down", {"exc": None})] + rv = client.get("/") + assert rv.status_code == 500 + assert len(recorded) == 1 + assert isinstance(recorded[0], ZeroDivisionError) finally: flask.appcontext_tearing_down.disconnect(record_teardown, app) diff --git a/tests/test_testing.py b/tests/test_testing.py index a78502f468..dd6347e5f3 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -187,7 +187,6 @@ def index(): def test_session_transactions_no_null_sessions(): app = flask.Flask(__name__) - app.testing = True with app.test_client() as c: with pytest.raises(RuntimeError) as e: @@ -254,29 +253,6 @@ def test_reuse_client(client): assert client.get("/").status_code == 404 -def test_test_client_calls_teardown_handlers(app, client): - called = [] - - @app.teardown_request - def remember(error): - called.append(error) - - with client: - assert called == [] - client.get("/") - assert called == [] - assert called == [None] - - del called[:] - with client: - assert called == [] - client.get("/") - assert called == [] - client.get("/") - assert called == [None] - assert called == [None, None] - - def test_full_url_request(app, client): @app.route("/action", methods=["POST"]) def action(): @@ -412,13 +388,15 @@ def hello_command(): def test_client_pop_all_preserved(app, req_ctx, client): @app.route("/") def index(): - # stream_with_context pushes a third context, preserved by client - return flask.Response(flask.stream_with_context("hello")) + # stream_with_context pushes a third context, preserved by response + return flask.stream_with_context("hello") - # req_ctx fixture pushed an initial context, not marked preserved + # req_ctx fixture pushed an initial context with client: # request pushes a second request context, preserved by client - client.get("/") + rv = client.get("/") + # close the response, releasing the context held by stream_with_context + rv.close() # only req_ctx fixture should still be pushed assert flask._request_ctx_stack.top is req_ctx From 9e686d93b69f0e73f801cc11a57839f4dfe2ca1a Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 1 Jul 2022 13:44:31 -0700 Subject: [PATCH 061/229] remove deprecated RequestContext.g --- CHANGES.rst | 4 ++++ src/flask/ctx.py | 26 -------------------------- 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index cc96f441bf..c4d6b0443f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,10 @@ Version 2.2.0 Unreleased +- Remove previously deprecated code. :pr:`4337` + + - The ``RequestContext.g`` proxy to ``AppContext.g`` is removed. + - Add new customization points to the ``Flask`` app object for many previously global behaviors. diff --git a/src/flask/ctx.py b/src/flask/ctx.py index caecbcc2b2..dc1f23b340 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -327,32 +327,6 @@ def __init__( # functions. self._after_request_functions: t.List[ft.AfterRequestCallable] = [] - @property - def g(self) -> _AppCtxGlobals: - import warnings - - warnings.warn( - "Accessing 'g' on the request context is deprecated and" - " will be removed in Flask 2.2. Access `g` directly or from" - "the application context instead.", - DeprecationWarning, - stacklevel=2, - ) - return _app_ctx_stack.top.g - - @g.setter - def g(self, value: _AppCtxGlobals) -> None: - import warnings - - warnings.warn( - "Setting 'g' on the request context is deprecated and" - " will be removed in Flask 2.2. Set it on the application" - " context instead.", - DeprecationWarning, - stacklevel=2, - ) - _app_ctx_stack.top.g = value - def copy(self) -> "RequestContext": """Creates a copy of this request context with the same request object. This can be used to move a request context to a different greenlet. From c2810ffdd2f2becbc37d35f63b69034b8b9ed5b8 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 1 Jul 2022 13:59:44 -0700 Subject: [PATCH 062/229] remove deprecated send_file argument names --- CHANGES.rst | 8 ++++- src/flask/helpers.py | 70 ++++---------------------------------------- 2 files changed, 13 insertions(+), 65 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index c4d6b0443f..825b2e2733 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,7 +7,13 @@ Unreleased - Remove previously deprecated code. :pr:`4337` - - The ``RequestContext.g`` proxy to ``AppContext.g`` is removed. + - Old names for some ``send_file`` parameters have been removed. + ``download_name`` replaces ``attachment_filename``, ``max_age`` + replaces ``cache_timeout``, and ``etag`` replaces ``add_etags``. + Additionally, ``path`` replaces ``filename`` in + ``send_from_directory``. + - The ``RequestContext.g`` property returning ``AppContext.g`` is + removed. - Add new customization points to the ``Flask`` app object for many previously global behaviors. diff --git a/src/flask/helpers.py b/src/flask/helpers.py index d1a84b9cf9..79c562c019 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -3,7 +3,6 @@ import socket import sys import typing as t -import warnings from datetime import datetime from functools import lru_cache from functools import update_wrapper @@ -390,53 +389,12 @@ def get_flashed_messages( return flashes -def _prepare_send_file_kwargs( - download_name: t.Optional[str] = None, - attachment_filename: t.Optional[str] = None, - etag: t.Optional[t.Union[bool, str]] = None, - add_etags: t.Optional[t.Union[bool]] = None, - max_age: t.Optional[ - t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]] - ] = None, - cache_timeout: t.Optional[int] = None, - **kwargs: t.Any, -) -> t.Dict[str, t.Any]: - if attachment_filename is not None: - warnings.warn( - "The 'attachment_filename' parameter has been renamed to" - " 'download_name'. The old name will be removed in Flask" - " 2.2.", - DeprecationWarning, - stacklevel=3, - ) - download_name = attachment_filename - - if cache_timeout is not None: - warnings.warn( - "The 'cache_timeout' parameter has been renamed to" - " 'max_age'. The old name will be removed in Flask 2.2.", - DeprecationWarning, - stacklevel=3, - ) - max_age = cache_timeout - - if add_etags is not None: - warnings.warn( - "The 'add_etags' parameter has been renamed to 'etag'. The" - " old name will be removed in Flask 2.2.", - DeprecationWarning, - stacklevel=3, - ) - etag = add_etags - - if max_age is None: - max_age = current_app.get_send_file_max_age +def _prepare_send_file_kwargs(**kwargs: t.Any) -> t.Dict[str, t.Any]: + if kwargs.get("max_age") is None: + kwargs["max_age"] = current_app.get_send_file_max_age kwargs.update( environ=request.environ, - download_name=download_name, - etag=etag, - max_age=max_age, use_x_sendfile=current_app.use_x_sendfile, response_class=current_app.response_class, _root_path=current_app.root_path, # type: ignore @@ -449,16 +407,13 @@ def send_file( mimetype: t.Optional[str] = None, as_attachment: bool = False, download_name: t.Optional[str] = None, - attachment_filename: t.Optional[str] = None, conditional: bool = True, etag: t.Union[bool, str] = True, - add_etags: t.Optional[bool] = None, last_modified: t.Optional[t.Union[datetime, int, float]] = None, max_age: t.Optional[ t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]] ] = None, - cache_timeout: t.Optional[int] = None, -): +) -> "Response": """Send the contents of a file to the client. The first argument can be a file path or a file-like object. Paths @@ -560,20 +515,17 @@ def send_file( .. versionadded:: 0.2 """ - return werkzeug.utils.send_file( + return werkzeug.utils.send_file( # type: ignore[return-value] **_prepare_send_file_kwargs( path_or_file=path_or_file, environ=request.environ, mimetype=mimetype, as_attachment=as_attachment, download_name=download_name, - attachment_filename=attachment_filename, conditional=conditional, etag=etag, - add_etags=add_etags, last_modified=last_modified, max_age=max_age, - cache_timeout=cache_timeout, ) ) @@ -581,7 +533,6 @@ def send_file( def send_from_directory( directory: t.Union[os.PathLike, str], path: t.Union[os.PathLike, str], - filename: t.Optional[str] = None, **kwargs: t.Any, ) -> "Response": """Send a file from within a directory using :func:`send_file`. @@ -617,16 +568,7 @@ def download_file(name): .. versionadded:: 0.5 """ - if filename is not None: - warnings.warn( - "The 'filename' parameter has been renamed to 'path'. The" - " old name will be removed in Flask 2.2.", - DeprecationWarning, - stacklevel=2, - ) - path = filename - - return werkzeug.utils.send_from_directory( # type: ignore + return werkzeug.utils.send_from_directory( # type: ignore[return-value] directory, path, **_prepare_send_file_kwargs(**kwargs) ) From ab6a8b0330845c80d661a748314691302ea7d8f7 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 2 Jul 2022 21:02:00 -0700 Subject: [PATCH 063/229] relax routes cli match order --- tests/test_cli.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index c9dd5ade06..676e07439b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -422,22 +422,23 @@ def create_app(): class TestRoutes: @pytest.fixture - def invoke(self, runner): - def create_app(): - app = Flask(__name__) - app.testing = True + def app(self): + app = Flask(__name__) + app.testing = True - @app.route("/get_post//", methods=["GET", "POST"]) - def yyy_get_post(x, y): - pass + @app.route("/get_post//", methods=["GET", "POST"]) + def yyy_get_post(x, y): + pass - @app.route("/zzz_post", methods=["POST"]) - def aaa_post(): - pass + @app.route("/zzz_post", methods=["POST"]) + def aaa_post(): + pass - return app + return app - cli = FlaskGroup(create_app=create_app) + @pytest.fixture + def invoke(self, app, runner): + cli = FlaskGroup(create_app=lambda: app) return partial(runner.invoke, cli) @pytest.fixture @@ -462,7 +463,7 @@ def test_simple(self, invoke): assert result.exit_code == 0 self.expect_order(["aaa_post", "static", "yyy_get_post"], result.output) - def test_sort(self, invoke): + def test_sort(self, app, invoke): default_output = invoke(["routes"]).output endpoint_output = invoke(["routes", "-s", "endpoint"]).output assert default_output == endpoint_output @@ -474,10 +475,8 @@ def test_sort(self, invoke): ["yyy_get_post", "static", "aaa_post"], invoke(["routes", "-s", "rule"]).output, ) - self.expect_order( - ["aaa_post", "yyy_get_post", "static"], - invoke(["routes", "-s", "match"]).output, - ) + match_order = [r.endpoint for r in app.url_map.iter_rules()] + self.expect_order(match_order, invoke(["routes", "-s", "match"]).output) def test_all_methods(self, invoke): output = invoke(["routes"]).output From ca2bfbb0ac66fbbeeedbf6bbe317bee45cb23d29 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sat, 2 Jul 2022 10:39:18 +0800 Subject: [PATCH 064/229] Support returning list as JSON --- src/flask/app.py | 10 +++++++--- src/flask/typing.py | 2 +- tests/test_basic.py | 8 ++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 5a8223e58e..f9db3b005d 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1830,6 +1830,9 @@ def make_response(self, rv: ft.ResponseReturnValue) -> Response: ``dict`` A dictionary that will be jsonify'd before being returned. + ``list`` + A list that will be jsonify'd before being returned. + ``generator`` or ``iterator`` A generator that returns ``str`` or ``bytes`` to be streamed as the response. @@ -1855,6 +1858,7 @@ def make_response(self, rv: ft.ResponseReturnValue) -> Response: .. versionchanged:: 2.2 A generator will be converted to a streaming response. + A list will be converted to a JSON response. .. versionchanged:: 1.1 A dict will be converted to a JSON response. @@ -1907,7 +1911,7 @@ def make_response(self, rv: ft.ResponseReturnValue) -> Response: headers=headers, # type: ignore[arg-type] ) status = headers = None - elif isinstance(rv, dict): + elif isinstance(rv, (dict, list)): rv = jsonify(rv) elif isinstance(rv, BaseResponse) or callable(rv): # evaluate a WSGI callable, or coerce a different response @@ -1920,14 +1924,14 @@ def make_response(self, rv: ft.ResponseReturnValue) -> Response: raise TypeError( f"{e}\nThe view function did not return a valid" " response. The return type must be a string," - " dict, tuple, Response instance, or WSGI" + " dict, list, tuple, Response instance, or WSGI" f" callable, but it was a {type(rv).__name__}." ).with_traceback(sys.exc_info()[2]) from None else: raise TypeError( "The view function did not return a valid" " response. The return type must be a string," - " dict, tuple, Response instance, or WSGI" + " dict, list, tuple, Response instance, or WSGI" f" callable, but it was a {type(rv).__name__}." ) diff --git a/src/flask/typing.py b/src/flask/typing.py index 4fb96545dd..30c9398c4c 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -7,7 +7,7 @@ # The possible types that are directly convertible or are a Response object. ResponseValue = t.Union[ - "Response", str, bytes, t.Dict[str, t.Any], t.Iterator[str], t.Iterator[bytes] + "Response", str, bytes, list, t.Dict[str, t.Any], t.Iterator[str], t.Iterator[bytes] ] # the possible types for an individual HTTP header diff --git a/tests/test_basic.py b/tests/test_basic.py index 91ba042f3b..dca48e2d38 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1166,6 +1166,10 @@ def from_wsgi(): def from_dict(): return {"foo": "bar"}, 201 + @app.route("/list") + def from_list(): + return ["foo", "bar"], 201 + assert client.get("/text").data == "Hällo Wörld".encode() assert client.get("/bytes").data == "Hällo Wörld".encode() @@ -1205,6 +1209,10 @@ def from_dict(): assert rv.json == {"foo": "bar"} assert rv.status_code == 201 + rv = client.get("/list") + assert rv.json == ["foo", "bar"] + assert rv.status_code == 201 + def test_response_type_errors(): app = flask.Flask(__name__) From f8cb0b0dd5ea30729db6b131aeeb30915ea4860b Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 2 Jul 2022 21:32:55 -0700 Subject: [PATCH 065/229] update docs about json --- CHANGES.rst | 3 +++ docs/quickstart.rst | 41 ++++++++++++++++++++++------------------- src/flask/app.py | 10 ++++++---- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 825b2e2733..504e319fe0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -65,6 +65,9 @@ Unreleased ``with client`` block. It will be cleaned up when ``response.get_data()`` or ``response.close()`` is called. +- Allow returning a list from a view function, to convert it to a + JSON response like a dict is. :issue:`4672` + Version 2.1.3 ------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 7c6c594cb1..ed5f455778 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -681,22 +681,25 @@ The return value from a view function is automatically converted into a response object for you. If the return value is a string it's converted into a response object with the string as response body, a ``200 OK`` status code and a :mimetype:`text/html` mimetype. If the -return value is a dict, :func:`jsonify` is called to produce a response. -The logic that Flask applies to converting return values into response -objects is as follows: +return value is a dict or list, :func:`jsonify` is called to produce a +response. The logic that Flask applies to converting return values into +response objects is as follows: 1. If a response object of the correct type is returned it's directly returned from the view. 2. If it's a string, a response object is created with that data and the default parameters. -3. If it's a dict, a response object is created using ``jsonify``. -4. If a tuple is returned the items in the tuple can provide extra +3. If it's an iterator or generator returning strings or bytes, it is + treated as a streaming response. +4. If it's a dict or list, a response object is created using + :func:`~flask.json.jsonify`. +5. If a tuple is returned the items in the tuple can provide extra information. Such tuples have to be in the form ``(response, status)``, ``(response, headers)``, or ``(response, status, headers)``. The ``status`` value will override the status code and ``headers`` can be a list or dictionary of additional header values. -5. If none of that works, Flask will assume the return value is a +6. If none of that works, Flask will assume the return value is a valid WSGI application and convert that into a response object. If you want to get hold of the resulting response object inside the view @@ -727,8 +730,8 @@ APIs with JSON `````````````` A common response format when writing an API is JSON. It's easy to get -started writing such an API with Flask. If you return a ``dict`` from a -view, it will be converted to a JSON response. +started writing such an API with Flask. If you return a ``dict`` or +``list`` from a view, it will be converted to a JSON response. .. code-block:: python @@ -741,20 +744,20 @@ view, it will be converted to a JSON response. "image": url_for("user_image", filename=user.image), } -Depending on your API design, you may want to create JSON responses for -types other than ``dict``. In that case, use the -:func:`~flask.json.jsonify` function, which will serialize any supported -JSON data type. Or look into Flask community extensions that support -more complex applications. - -.. code-block:: python - - from flask import jsonify - @app.route("/users") def users_api(): users = get_all_users() - return jsonify([user.to_json() for user in users]) + return [user.to_json() for user in users] + +This is a shortcut to passing the data to the +:func:`~flask.json.jsonify` function, which will serialize any supported +JSON data type. That means that all the data in the dict or list must be +JSON serializable. + +For complex types such as database models, you'll want to use a +serialization library to convert the data to valid JSON types first. +There are many serialization libraries and Flask API extensions +maintained by the community that support more complex applications. .. _sessions: diff --git a/src/flask/app.py b/src/flask/app.py index f9db3b005d..8726a6e814 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1924,15 +1924,17 @@ def make_response(self, rv: ft.ResponseReturnValue) -> Response: raise TypeError( f"{e}\nThe view function did not return a valid" " response. The return type must be a string," - " dict, list, tuple, Response instance, or WSGI" - f" callable, but it was a {type(rv).__name__}." + " dict, list, tuple with headers or status," + " Response instance, or WSGI callable, but it" + f" was a {type(rv).__name__}." ).with_traceback(sys.exc_info()[2]) from None else: raise TypeError( "The view function did not return a valid" " response. The return type must be a string," - " dict, list, tuple, Response instance, or WSGI" - f" callable, but it was a {type(rv).__name__}." + " dict, list, tuple with headers or status," + " Response instance, or WSGI callable, but it was a" + f" {type(rv).__name__}." ) rv = t.cast(Response, rv) From 60b845ebabeb77e401186f4b83de21d3f9c6a9d7 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 2 Jul 2022 21:40:20 -0700 Subject: [PATCH 066/229] update typing tests for json --- src/flask/typing.py | 8 +++++++- tests/typing/typing_route.py | 12 +++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/flask/typing.py b/src/flask/typing.py index 30c9398c4c..6bbdb1ddf9 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -7,7 +7,13 @@ # The possible types that are directly convertible or are a Response object. ResponseValue = t.Union[ - "Response", str, bytes, list, t.Dict[str, t.Any], t.Iterator[str], t.Iterator[bytes] + "Response", + str, + bytes, + t.List[t.Any], + t.Dict[str, t.Any], + t.Iterator[str], + t.Iterator[bytes], ] # the possible types for an individual HTTP header diff --git a/tests/typing/typing_route.py b/tests/typing/typing_route.py index 9c518938fb..41973c264b 100644 --- a/tests/typing/typing_route.py +++ b/tests/typing/typing_route.py @@ -25,7 +25,17 @@ def hello_bytes() -> bytes: @app.route("/json") def hello_json() -> Response: - return jsonify({"response": "Hello, World!"}) + return jsonify("Hello, World!") + + +@app.route("/json/dict") +def hello_json_dict() -> t.Dict[str, t.Any]: + return {"response": "Hello, World!"} + + +@app.route("/json/dict") +def hello_json_list() -> t.List[t.Any]: + return [{"message": "Hello"}, {"message": "World"}] @app.route("/generator") From 50df54e4c7fb06df51a89d8f363e2997d2310449 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 4 Jul 2022 08:35:36 -0700 Subject: [PATCH 067/229] explain workflow --- .github/workflows/lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml index b4f7633870..cd89f67c9d 100644 --- a/.github/workflows/lock.yaml +++ b/.github/workflows/lock.yaml @@ -1,3 +1,6 @@ +# This does not automatically close "stale" issues. Instead, it locks closed issues after 2 weeks of no activity. +# If there's a new issue related to an old one, we've found it's much easier to work on as a new issue. + name: 'Lock threads' on: From e9d0000fc152108948144def06b2ac84524bffcc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 22:37:19 +0000 Subject: [PATCH 068/229] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.32.1 → v2.34.0](https://github.com/asottile/pyupgrade/compare/v2.32.1...v2.34.0) - [github.com/asottile/reorder_python_imports: v3.1.0 → v3.3.0](https://github.com/asottile/reorder_python_imports/compare/v3.1.0...v3.3.0) - [github.com/psf/black: 22.3.0 → 22.6.0](https://github.com/psf/black/compare/22.3.0...22.6.0) - [github.com/pre-commit/pre-commit-hooks: v4.2.0 → v4.3.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.2.0...v4.3.0) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e5a89e660c..703962e88c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,12 +3,12 @@ ci: autoupdate_schedule: monthly repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.32.1 + rev: v2.34.0 hooks: - id: pyupgrade args: ["--py36-plus"] - repo: https://github.com/asottile/reorder_python_imports - rev: v3.1.0 + rev: v3.3.0 hooks: - id: reorder-python-imports name: Reorder Python imports (src, tests) @@ -16,7 +16,7 @@ repos: args: ["--application-directories", "src"] additional_dependencies: ["setuptools>60.9"] - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.6.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 @@ -31,7 +31,7 @@ repos: hooks: - id: pip-compile-multi-verify - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.3.0 hooks: - id: fix-byte-order-marker - id: trailing-whitespace From 9dfcb90c92e13f2d92cb723cb834b5e0d666a2f5 Mon Sep 17 00:00:00 2001 From: Ties Jan Hefting Date: Wed, 6 Jul 2022 11:38:33 +0200 Subject: [PATCH 069/229] Document serialization of Decimal in JSONEncoder The Flask JSONEncoder serializes Decimal types to strings, but this behavior is missing from the docs. The docs are updated accordingly. --- docs/api.rst | 1 + src/flask/json/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index b3cffde26f..6b02494bff 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -242,6 +242,7 @@ for easier customization. By default it handles some extra data types: - :class:`datetime.datetime` and :class:`datetime.date` are serialized to :rfc:`822` strings. This is the same as the HTTP date format. +- :class:`decimal.Decimal` is serialized to a string. - :class:`uuid.UUID` is serialized to a string. - :class:`dataclasses.dataclass` is passed to :func:`dataclasses.asdict`. diff --git a/src/flask/json/__init__.py b/src/flask/json/__init__.py index edc9793df4..adefe02dcd 100644 --- a/src/flask/json/__init__.py +++ b/src/flask/json/__init__.py @@ -23,6 +23,7 @@ class JSONEncoder(_json.JSONEncoder): - :class:`datetime.datetime` and :class:`datetime.date` are serialized to :rfc:`822` strings. This is the same as the HTTP date format. + - :class:`decimal.Decimal` is serialized to a string. - :class:`uuid.UUID` is serialized to a string. - :class:`dataclasses.dataclass` is passed to :func:`dataclasses.asdict`. From 9b44bf2818d8e3cde422ad7f43fb33dfc6737289 Mon Sep 17 00:00:00 2001 From: Phil Jones Date: Wed, 6 Jul 2022 22:05:20 +0100 Subject: [PATCH 070/229] Improve decorator typing (#4676) * Add a missing setupmethod decorator * Improve the decorator typing This will allow type checkers to understand that the decorators return the same function signature as passed as an argument. This follows the guidelines from https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators. I've chosen to keep a TypeVar per module and usage as I think encouraged by PEP 695, which I hope is accepted as the syntax is much nicer. --- src/flask/app.py | 37 +++++++++++++------- src/flask/blueprints.py | 60 +++++++++++++++++++------------ src/flask/scaffold.py | 68 +++++++++++++++++++----------------- src/flask/typing.py | 27 ++++++++++---- src/flask/views.py | 2 +- tests/typing/typing_route.py | 5 +++ 6 files changed, 123 insertions(+), 76 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 8726a6e814..736061b944 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -74,6 +74,17 @@ from .testing import FlaskClient from .testing import FlaskCliRunner +T_before_first_request = t.TypeVar( + "T_before_first_request", bound=ft.BeforeFirstRequestCallable +) +T_shell_context_processor = t.TypeVar( + "T_shell_context_processor", bound=ft.ShellContextProcessorCallable +) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) +T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) +T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) + if sys.version_info >= (3, 8): iscoroutinefunction = inspect.iscoroutinefunction else: @@ -472,7 +483,7 @@ def __init__( #: when a shell context is created. #: #: .. versionadded:: 0.11 - self.shell_context_processors: t.List[t.Callable[[], t.Dict[str, t.Any]]] = [] + self.shell_context_processors: t.List[ft.ShellContextProcessorCallable] = [] #: Maps registered blueprint names to blueprint objects. The #: dict retains the order the blueprints were registered in. @@ -1075,7 +1086,7 @@ def add_url_rule( self, rule: str, endpoint: t.Optional[str] = None, - view_func: t.Optional[ft.ViewCallable] = None, + view_func: t.Optional[ft.RouteCallable] = None, provide_automatic_options: t.Optional[bool] = None, **options: t.Any, ) -> None: @@ -1132,7 +1143,7 @@ def add_url_rule( @setupmethod def template_filter( self, name: t.Optional[str] = None - ) -> t.Callable[[ft.TemplateFilterCallable], ft.TemplateFilterCallable]: + ) -> t.Callable[[T_template_filter], T_template_filter]: """A decorator that is used to register custom template filter. You can specify a name for the filter, otherwise the function name will be used. Example:: @@ -1145,7 +1156,7 @@ def reverse(s): function name will be used. """ - def decorator(f: ft.TemplateFilterCallable) -> ft.TemplateFilterCallable: + def decorator(f: T_template_filter) -> T_template_filter: self.add_template_filter(f, name=name) return f @@ -1166,7 +1177,7 @@ def add_template_filter( @setupmethod def template_test( self, name: t.Optional[str] = None - ) -> t.Callable[[ft.TemplateTestCallable], ft.TemplateTestCallable]: + ) -> t.Callable[[T_template_test], T_template_test]: """A decorator that is used to register custom template test. You can specify a name for the test, otherwise the function name will be used. Example:: @@ -1186,7 +1197,7 @@ def is_prime(n): function name will be used. """ - def decorator(f: ft.TemplateTestCallable) -> ft.TemplateTestCallable: + def decorator(f: T_template_test) -> T_template_test: self.add_template_test(f, name=name) return f @@ -1209,7 +1220,7 @@ def add_template_test( @setupmethod def template_global( self, name: t.Optional[str] = None - ) -> t.Callable[[ft.TemplateGlobalCallable], ft.TemplateGlobalCallable]: + ) -> t.Callable[[T_template_global], T_template_global]: """A decorator that is used to register a custom template global function. You can specify a name for the global function, otherwise the function name will be used. Example:: @@ -1224,7 +1235,7 @@ def double(n): function name will be used. """ - def decorator(f: ft.TemplateGlobalCallable) -> ft.TemplateGlobalCallable: + def decorator(f: T_template_global) -> T_template_global: self.add_template_global(f, name=name) return f @@ -1245,9 +1256,7 @@ def add_template_global( self.jinja_env.globals[name or f.__name__] = f @setupmethod - def before_first_request( - self, f: ft.BeforeFirstRequestCallable - ) -> ft.BeforeFirstRequestCallable: + def before_first_request(self, f: T_before_first_request) -> T_before_first_request: """Registers a function to be run before the first request to this instance of the application. @@ -1273,7 +1282,7 @@ def before_first_request( return f @setupmethod - def teardown_appcontext(self, f: ft.TeardownCallable) -> ft.TeardownCallable: + def teardown_appcontext(self, f: T_teardown) -> T_teardown: """Registers a function to be called when the application context ends. These functions are typically also called when the request context is popped. @@ -1306,7 +1315,9 @@ def teardown_appcontext(self, f: ft.TeardownCallable) -> ft.TeardownCallable: return f @setupmethod - def shell_context_processor(self, f: t.Callable) -> t.Callable: + def shell_context_processor( + self, f: T_shell_context_processor + ) -> T_shell_context_processor: """Registers a shell context processor function. .. versionadded:: 0.11 diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 76b3606746..6deda47e79 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -13,6 +13,23 @@ from .app import Flask DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable] +T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable) +T_before_first_request = t.TypeVar( + "T_before_first_request", bound=ft.BeforeFirstRequestCallable +) +T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) +T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_context_processor = t.TypeVar( + "T_template_context_processor", bound=ft.TemplateContextProcessorCallable +) +T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) +T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) +T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) +T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) +T_url_value_preprocessor = t.TypeVar( + "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable +) class BlueprintSetupState: @@ -236,7 +253,7 @@ def wrapper(state: BlueprintSetupState) -> None: if state.first_registration: func(state) - return self.record(update_wrapper(wrapper, func)) + self.record(update_wrapper(wrapper, func)) def make_setup_state( self, app: "Flask", options: dict, first_registration: bool = False @@ -392,7 +409,7 @@ def add_url_rule( self, rule: str, endpoint: t.Optional[str] = None, - view_func: t.Optional[ft.ViewCallable] = None, + view_func: t.Optional[ft.RouteCallable] = None, provide_automatic_options: t.Optional[bool] = None, **options: t.Any, ) -> None: @@ -418,7 +435,7 @@ def add_url_rule( @setupmethod def app_template_filter( self, name: t.Optional[str] = None - ) -> t.Callable[[ft.TemplateFilterCallable], ft.TemplateFilterCallable]: + ) -> t.Callable[[T_template_filter], T_template_filter]: """Register a custom template filter, available application wide. Like :meth:`Flask.template_filter` but for a blueprint. @@ -426,7 +443,7 @@ def app_template_filter( function name will be used. """ - def decorator(f: ft.TemplateFilterCallable) -> ft.TemplateFilterCallable: + def decorator(f: T_template_filter) -> T_template_filter: self.add_app_template_filter(f, name=name) return f @@ -452,7 +469,7 @@ def register_template(state: BlueprintSetupState) -> None: @setupmethod def app_template_test( self, name: t.Optional[str] = None - ) -> t.Callable[[ft.TemplateTestCallable], ft.TemplateTestCallable]: + ) -> t.Callable[[T_template_test], T_template_test]: """Register a custom template test, available application wide. Like :meth:`Flask.template_test` but for a blueprint. @@ -462,7 +479,7 @@ def app_template_test( function name will be used. """ - def decorator(f: ft.TemplateTestCallable) -> ft.TemplateTestCallable: + def decorator(f: T_template_test) -> T_template_test: self.add_app_template_test(f, name=name) return f @@ -490,7 +507,7 @@ def register_template(state: BlueprintSetupState) -> None: @setupmethod def app_template_global( self, name: t.Optional[str] = None - ) -> t.Callable[[ft.TemplateGlobalCallable], ft.TemplateGlobalCallable]: + ) -> t.Callable[[T_template_global], T_template_global]: """Register a custom template global, available application wide. Like :meth:`Flask.template_global` but for a blueprint. @@ -500,7 +517,7 @@ def app_template_global( function name will be used. """ - def decorator(f: ft.TemplateGlobalCallable) -> ft.TemplateGlobalCallable: + def decorator(f: T_template_global) -> T_template_global: self.add_app_template_global(f, name=name) return f @@ -526,9 +543,7 @@ def register_template(state: BlueprintSetupState) -> None: self.record_once(register_template) @setupmethod - def before_app_request( - self, f: ft.BeforeRequestCallable - ) -> ft.BeforeRequestCallable: + def before_app_request(self, f: T_before_request) -> T_before_request: """Like :meth:`Flask.before_request`. Such a function is executed before each request, even if outside of a blueprint. """ @@ -539,8 +554,8 @@ def before_app_request( @setupmethod def before_app_first_request( - self, f: ft.BeforeFirstRequestCallable - ) -> ft.BeforeFirstRequestCallable: + self, f: T_before_first_request + ) -> T_before_first_request: """Like :meth:`Flask.before_first_request`. Such a function is executed before the first request to the application. @@ -560,7 +575,8 @@ def before_app_first_request( self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) return f - def after_app_request(self, f: ft.AfterRequestCallable) -> ft.AfterRequestCallable: + @setupmethod + def after_app_request(self, f: T_after_request) -> T_after_request: """Like :meth:`Flask.after_request` but for a blueprint. Such a function is executed after each request, even if outside of the blueprint. """ @@ -570,7 +586,7 @@ def after_app_request(self, f: ft.AfterRequestCallable) -> ft.AfterRequestCallab return f @setupmethod - def teardown_app_request(self, f: ft.TeardownCallable) -> ft.TeardownCallable: + def teardown_app_request(self, f: T_teardown) -> T_teardown: """Like :meth:`Flask.teardown_request` but for a blueprint. Such a function is executed when tearing down each request, even if outside of the blueprint. @@ -582,8 +598,8 @@ def teardown_app_request(self, f: ft.TeardownCallable) -> ft.TeardownCallable: @setupmethod def app_context_processor( - self, f: ft.TemplateContextProcessorCallable - ) -> ft.TemplateContextProcessorCallable: + self, f: T_template_context_processor + ) -> T_template_context_processor: """Like :meth:`Flask.context_processor` but for a blueprint. Such a function is executed each request, even if outside of the blueprint. """ @@ -595,12 +611,12 @@ def app_context_processor( @setupmethod def app_errorhandler( self, code: t.Union[t.Type[Exception], int] - ) -> t.Callable[[ft.ErrorHandlerDecorator], ft.ErrorHandlerDecorator]: + ) -> t.Callable[[T_error_handler], T_error_handler]: """Like :meth:`Flask.errorhandler` but for a blueprint. This handler is used for all requests, even if outside of the blueprint. """ - def decorator(f: ft.ErrorHandlerDecorator) -> ft.ErrorHandlerDecorator: + def decorator(f: T_error_handler) -> T_error_handler: self.record_once(lambda s: s.app.errorhandler(code)(f)) return f @@ -608,8 +624,8 @@ def decorator(f: ft.ErrorHandlerDecorator) -> ft.ErrorHandlerDecorator: @setupmethod def app_url_value_preprocessor( - self, f: ft.URLValuePreprocessorCallable - ) -> ft.URLValuePreprocessorCallable: + self, f: T_url_value_preprocessor + ) -> T_url_value_preprocessor: """Same as :meth:`url_value_preprocessor` but application wide.""" self.record_once( lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) @@ -617,7 +633,7 @@ def app_url_value_preprocessor( return f @setupmethod - def app_url_defaults(self, f: ft.URLDefaultCallable) -> ft.URLDefaultCallable: + def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults: """Same as :meth:`url_defaults` but application wide.""" self.record_once( lambda s: s.app.url_default_functions.setdefault(None, []).append(f) diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 418b24ae66..7f099f40c4 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -28,6 +28,18 @@ _sentinel = object() F = t.TypeVar("F", bound=t.Callable[..., t.Any]) +T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable) +T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) +T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_context_processor = t.TypeVar( + "T_template_context_processor", bound=ft.TemplateContextProcessorCallable +) +T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) +T_url_value_preprocessor = t.TypeVar( + "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable +) +T_route = t.TypeVar("T_route", bound=ft.RouteCallable) def setupmethod(f: F) -> F: @@ -352,16 +364,14 @@ def _method_route( method: str, rule: str, options: dict, - ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: + ) -> t.Callable[[T_route], T_route]: if "methods" in options: raise TypeError("Use the 'route' decorator to use the 'methods' argument.") return self.route(rule, methods=[method], **options) @setupmethod - def get( - self, rule: str, **options: t.Any - ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: + def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """Shortcut for :meth:`route` with ``methods=["GET"]``. .. versionadded:: 2.0 @@ -369,9 +379,7 @@ def get( return self._method_route("GET", rule, options) @setupmethod - def post( - self, rule: str, **options: t.Any - ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: + def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """Shortcut for :meth:`route` with ``methods=["POST"]``. .. versionadded:: 2.0 @@ -379,9 +387,7 @@ def post( return self._method_route("POST", rule, options) @setupmethod - def put( - self, rule: str, **options: t.Any - ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: + def put(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """Shortcut for :meth:`route` with ``methods=["PUT"]``. .. versionadded:: 2.0 @@ -389,9 +395,7 @@ def put( return self._method_route("PUT", rule, options) @setupmethod - def delete( - self, rule: str, **options: t.Any - ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: + def delete(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """Shortcut for :meth:`route` with ``methods=["DELETE"]``. .. versionadded:: 2.0 @@ -399,9 +403,7 @@ def delete( return self._method_route("DELETE", rule, options) @setupmethod - def patch( - self, rule: str, **options: t.Any - ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: + def patch(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """Shortcut for :meth:`route` with ``methods=["PATCH"]``. .. versionadded:: 2.0 @@ -409,9 +411,7 @@ def patch( return self._method_route("PATCH", rule, options) @setupmethod - def route( - self, rule: str, **options: t.Any - ) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]: + def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: """Decorate a view function to register it with the given URL rule and options. Calls :meth:`add_url_rule`, which has more details about the implementation. @@ -435,7 +435,7 @@ def index(): :class:`~werkzeug.routing.Rule` object. """ - def decorator(f: ft.RouteDecorator) -> ft.RouteDecorator: + def decorator(f: T_route) -> T_route: endpoint = options.pop("endpoint", None) self.add_url_rule(rule, endpoint, f, **options) return f @@ -447,7 +447,7 @@ def add_url_rule( self, rule: str, endpoint: t.Optional[str] = None, - view_func: t.Optional[ft.ViewCallable] = None, + view_func: t.Optional[ft.RouteCallable] = None, provide_automatic_options: t.Optional[bool] = None, **options: t.Any, ) -> None: @@ -511,7 +511,7 @@ def index(): raise NotImplementedError @setupmethod - def endpoint(self, endpoint: str) -> t.Callable: + def endpoint(self, endpoint: str) -> t.Callable[[F], F]: """Decorate a view function to register it for the given endpoint. Used if a rule is added without a ``view_func`` with :meth:`add_url_rule`. @@ -528,14 +528,14 @@ def example(): function. """ - def decorator(f): + def decorator(f: F) -> F: self.view_functions[endpoint] = f return f return decorator @setupmethod - def before_request(self, f: ft.BeforeRequestCallable) -> ft.BeforeRequestCallable: + def before_request(self, f: T_before_request) -> T_before_request: """Register a function to run before each request. For example, this can be used to open a database connection, or @@ -557,7 +557,7 @@ def load_user(): return f @setupmethod - def after_request(self, f: ft.AfterRequestCallable) -> ft.AfterRequestCallable: + def after_request(self, f: T_after_request) -> T_after_request: """Register a function to run after each request to this object. The function is called with the response object, and must return @@ -573,7 +573,7 @@ def after_request(self, f: ft.AfterRequestCallable) -> ft.AfterRequestCallable: return f @setupmethod - def teardown_request(self, f: ft.TeardownCallable) -> ft.TeardownCallable: + def teardown_request(self, f: T_teardown) -> T_teardown: """Register a function to be run at the end of each request, regardless of whether there was an exception or not. These functions are executed when the request context is popped, even if not an @@ -606,16 +606,18 @@ def teardown_request(self, f: ft.TeardownCallable) -> ft.TeardownCallable: @setupmethod def context_processor( - self, f: ft.TemplateContextProcessorCallable - ) -> ft.TemplateContextProcessorCallable: + self, + f: T_template_context_processor, + ) -> T_template_context_processor: """Registers a template context processor function.""" self.template_context_processors[None].append(f) return f @setupmethod def url_value_preprocessor( - self, f: ft.URLValuePreprocessorCallable - ) -> ft.URLValuePreprocessorCallable: + self, + f: T_url_value_preprocessor, + ) -> T_url_value_preprocessor: """Register a URL value preprocessor function for all view functions in the application. These functions will be called before the :meth:`before_request` functions. @@ -632,7 +634,7 @@ def url_value_preprocessor( return f @setupmethod - def url_defaults(self, f: ft.URLDefaultCallable) -> ft.URLDefaultCallable: + def url_defaults(self, f: T_url_defaults) -> T_url_defaults: """Callback function for URL defaults for all view functions of the application. It's called with the endpoint and values and should update the values passed in place. @@ -643,7 +645,7 @@ def url_defaults(self, f: ft.URLDefaultCallable) -> ft.URLDefaultCallable: @setupmethod def errorhandler( self, code_or_exception: t.Union[t.Type[Exception], int] - ) -> t.Callable[[ft.ErrorHandlerDecorator], ft.ErrorHandlerDecorator]: + ) -> t.Callable[[T_error_handler], T_error_handler]: """Register a function to handle errors by code or exception class. A decorator that is used to register a function given an @@ -673,7 +675,7 @@ def special_exception_handler(error): an arbitrary exception """ - def decorator(f: ft.ErrorHandlerDecorator) -> ft.ErrorHandlerDecorator: + def decorator(f: T_error_handler) -> T_error_handler: self.register_error_handler(code_or_exception, f) return f diff --git a/src/flask/typing.py b/src/flask/typing.py index 6bbdb1ddf9..89bdc71eb4 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -42,10 +42,22 @@ ResponseClass = t.TypeVar("ResponseClass", bound="Response") AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named -AfterRequestCallable = t.Callable[[ResponseClass], ResponseClass] -BeforeFirstRequestCallable = t.Callable[[], None] -BeforeRequestCallable = t.Callable[[], t.Optional[ResponseReturnValue]] -TeardownCallable = t.Callable[[t.Optional[BaseException]], None] +AfterRequestCallable = t.Union[ + t.Callable[[ResponseClass], ResponseClass], + t.Callable[[ResponseClass], t.Awaitable[ResponseClass]], +] +BeforeFirstRequestCallable = t.Union[ + t.Callable[[], None], t.Callable[[], t.Awaitable[None]] +] +BeforeRequestCallable = t.Union[ + t.Callable[[], t.Optional[ResponseReturnValue]], + t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]], +] +ShellContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]] +TeardownCallable = t.Union[ + t.Callable[[t.Optional[BaseException]], None], + t.Callable[[t.Optional[BaseException]], t.Awaitable[None]], +] TemplateContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]] TemplateFilterCallable = t.Callable[..., t.Any] TemplateGlobalCallable = t.Callable[..., t.Any] @@ -60,7 +72,8 @@ # https://github.com/pallets/flask/issues/4295 # https://github.com/pallets/flask/issues/4297 ErrorHandlerCallable = t.Callable[[t.Any], ResponseReturnValue] -ErrorHandlerDecorator = t.TypeVar("ErrorHandlerDecorator", bound=ErrorHandlerCallable) -ViewCallable = t.Callable[..., ResponseReturnValue] -RouteDecorator = t.TypeVar("RouteDecorator", bound=ViewCallable) +RouteCallable = t.Union[ + t.Callable[..., ResponseReturnValue], + t.Callable[..., t.Awaitable[ResponseReturnValue]], +] diff --git a/src/flask/views.py b/src/flask/views.py index 7aac3dd5a5..a82f191238 100644 --- a/src/flask/views.py +++ b/src/flask/views.py @@ -82,7 +82,7 @@ def dispatch_request(self) -> ft.ResponseReturnValue: @classmethod def as_view( cls, name: str, *class_args: t.Any, **class_kwargs: t.Any - ) -> ft.ViewCallable: + ) -> ft.RouteCallable: """Convert the class into a view function that can be registered for a route. diff --git a/tests/typing/typing_route.py b/tests/typing/typing_route.py index 41973c264b..5f2ddbfd45 100644 --- a/tests/typing/typing_route.py +++ b/tests/typing/typing_route.py @@ -84,6 +84,11 @@ 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 From 2f1d1d625697b9eca82dc8b2f46fff24dace63b1 Mon Sep 17 00:00:00 2001 From: pgjones Date: Fri, 8 Jul 2022 14:33:20 +0100 Subject: [PATCH 071/229] Add further typing tests This should help ensure the app decorators are correctly typed. --- tests/typing/typing_app_decorators.py | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/typing/typing_app_decorators.py diff --git a/tests/typing/typing_app_decorators.py b/tests/typing/typing_app_decorators.py new file mode 100644 index 0000000000..3df3e716fb --- /dev/null +++ b/tests/typing/typing_app_decorators.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import typing as t + +from flask import Flask +from flask import Response + +app = Flask(__name__) + + +@app.after_request +def after_sync(response: Response) -> Response: + ... + + +@app.after_request +async def after_async(response: Response) -> Response: + ... + + +@app.before_request +def before_sync() -> None: + ... + + +@app.before_request +async def before_async() -> None: + ... + + +@app.teardown_appcontext +def teardown_sync(exc: t.Optional[BaseException]) -> None: + ... + + +@app.teardown_appcontext +async def teardown_async(exc: t.Optional[BaseException]) -> None: + ... From 6751f68560bfa7c51133d2712830645ed551ace8 Mon Sep 17 00:00:00 2001 From: "Earl C. Ruby III" Date: Fri, 8 Jul 2022 10:21:42 -0700 Subject: [PATCH 072/229] New URL for the Quart project The Quart project moved from Gitlab (https://gitlab.com/pgjones/quart) to Github (https://github.com/pallets/quart). There is a message at the top of the Gitlab page announcing the move. --- docs/async-await.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/async-await.rst b/docs/async-await.rst index cea8460231..06a29fccc1 100644 --- a/docs/async-await.rst +++ b/docs/async-await.rst @@ -91,7 +91,7 @@ patch low-level Python functions to accomplish this, whereas ``async``/ whether you should use Flask, Quart, or something else is ultimately up to understanding the specific needs of your project. -.. _Quart: https://gitlab.com/pgjones/quart +.. _Quart: https://github.com/pallets/quart .. _ASGI: https://asgi.readthedocs.io/en/latest/ From 89463cb77c7f3830c308cdfcc250255fd7377d01 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 8 Jul 2022 10:58:29 -0700 Subject: [PATCH 073/229] require Werkzeug 2.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a28763b56c..bd31435c6c 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="Flask", install_requires=[ - "Werkzeug >= 2.0", + "Werkzeug >= 2.2.0a1", "Jinja2 >= 3.0", "itsdangerous >= 2.0", "click >= 8.0", From 0b2f809f9b56861dae3ae5154d73b4afaf632a0a Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 27 Jun 2022 07:57:03 -0700 Subject: [PATCH 074/229] access names as proxies directly --- src/flask/globals.py | 75 +++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/src/flask/globals.py b/src/flask/globals.py index 7824204fe9..f4781f6cc1 100644 --- a/src/flask/globals.py +++ b/src/flask/globals.py @@ -1,59 +1,50 @@ import typing as t -from functools import partial +from contextvars import ContextVar -from werkzeug.local import LocalProxy from werkzeug.local import LocalStack if t.TYPE_CHECKING: # pragma: no cover from .app import Flask from .ctx import _AppCtxGlobals + from .ctx import AppContext + from .ctx import RequestContext from .sessions import SessionMixin from .wrappers import Request -_request_ctx_err_msg = """\ -Working outside of request context. - -This typically means that you attempted to use functionality that needed -an active HTTP request. Consult the documentation on testing for -information about how to avoid this problem.\ -""" -_app_ctx_err_msg = """\ +_no_app_msg = """\ Working outside of application context. This typically means that you attempted to use functionality that needed -to interface with the current application object in some way. To solve -this, set up an application context with app.app_context(). See the -documentation for more information.\ +the current application. To solve this, set up an application context +with app.app_context(). See the documentation for more information.\ """ +_cv_app: ContextVar[t.List["AppContext"]] = ContextVar("flask.app_ctx") +_app_ctx_stack: LocalStack["AppContext"] = LocalStack(_cv_app) +app_ctx: "AppContext" = _app_ctx_stack( # type: ignore[assignment] + unbound_message=_no_app_msg +) +current_app: "Flask" = _app_ctx_stack( # type: ignore[assignment] + "app", unbound_message=_no_app_msg +) +g: "_AppCtxGlobals" = _app_ctx_stack( # type: ignore[assignment] + "g", unbound_message=_no_app_msg +) +_no_req_msg = """\ +Working outside of request context. -def _lookup_req_object(name): - top = _request_ctx_stack.top - if top is None: - raise RuntimeError(_request_ctx_err_msg) - return getattr(top, name) - - -def _lookup_app_object(name): - top = _app_ctx_stack.top - if top is None: - raise RuntimeError(_app_ctx_err_msg) - return getattr(top, name) - - -def _find_app(): - top = _app_ctx_stack.top - if top is None: - raise RuntimeError(_app_ctx_err_msg) - return top.app - - -# context locals -_request_ctx_stack = LocalStack() -_app_ctx_stack = LocalStack() -current_app: "Flask" = LocalProxy(_find_app) # type: ignore -request: "Request" = LocalProxy(partial(_lookup_req_object, "request")) # type: ignore -session: "SessionMixin" = LocalProxy( # type: ignore - partial(_lookup_req_object, "session") +This typically means that you attempted to use functionality that needed +an active HTTP request. Consult the documentation on testing for +information about how to avoid this problem.\ +""" +_cv_req: ContextVar[t.List["RequestContext"]] = ContextVar("flask.request_ctx") +_request_ctx_stack: LocalStack["RequestContext"] = LocalStack(_cv_req) +request_ctx: "RequestContext" = _request_ctx_stack( # type: ignore[assignment] + unbound_message=_no_req_msg +) +request: "Request" = _request_ctx_stack( # type: ignore[assignment] + "request", unbound_message=_no_req_msg +) +session: "SessionMixin" = _request_ctx_stack( # type: ignore[assignment] + "session", unbound_message=_no_req_msg ) -g: "_AppCtxGlobals" = LocalProxy(partial(_lookup_app_object, "g")) # type: ignore From d597db67ded68d4aedf42b9d1d4570573fb79cd6 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 4 Jul 2022 22:50:51 -0700 Subject: [PATCH 075/229] contexts no longer use LocalStack --- CHANGES.rst | 9 +++++ src/flask/__init__.py | 28 ++++++++++++- src/flask/ctx.py | 81 ++++++++++++++++---------------------- src/flask/globals.py | 91 +++++++++++++++++++++++++++++++++++-------- src/flask/testing.py | 8 ++-- 5 files changed, 146 insertions(+), 71 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 504e319fe0..5fda08e3a0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,15 @@ Unreleased - The ``RequestContext.g`` property returning ``AppContext.g`` is removed. +- The app and request contexts are managed using Python context vars + directly rather than Werkzeug's ``LocalStack``. This should result + in better performance and memory use. :pr:`4672` + + - Extension maintainers, be aware that ``_app_ctx_stack.top`` + and ``_request_ctx_stack.top`` are deprecated. Store data on + ``g`` instead using a unique prefix, like + ``g._extension_name_attr``. + - Add new customization points to the ``Flask`` app object for many previously global behaviors. diff --git a/src/flask/__init__.py b/src/flask/__init__.py index 8ca1dbad0b..7eb8c24b14 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -11,8 +11,6 @@ from .ctx import copy_current_request_context as copy_current_request_context from .ctx import has_app_context as has_app_context from .ctx import has_request_context as has_request_context -from .globals import _app_ctx_stack as _app_ctx_stack -from .globals import _request_ctx_stack as _request_ctx_stack from .globals import current_app as current_app from .globals import g as g from .globals import request as request @@ -45,3 +43,29 @@ from .templating import stream_template_string as stream_template_string __version__ = "2.2.0.dev0" + + +def __getattr__(name): + if name == "_app_ctx_stack": + import warnings + from .globals import __app_ctx_stack + + warnings.warn( + "'_app_ctx_stack' is deprecated and will be removed in Flask 2.3.", + DeprecationWarning, + stacklevel=2, + ) + return __app_ctx_stack + + if name == "_request_ctx_stack": + import warnings + from .globals import __request_ctx_stack + + warnings.warn( + "'_request_ctx_stack' is deprecated and will be removed in Flask 2.3.", + DeprecationWarning, + stacklevel=2, + ) + return __request_ctx_stack + + raise AttributeError(name) diff --git a/src/flask/ctx.py b/src/flask/ctx.py index dc1f23b340..758127ea78 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -1,3 +1,4 @@ +import contextvars import sys import typing as t from functools import update_wrapper @@ -7,6 +8,8 @@ from . import typing as ft from .globals import _app_ctx_stack +from .globals import _cv_app +from .globals import _cv_req from .globals import _request_ctx_stack from .signals import appcontext_popped from .signals import appcontext_pushed @@ -212,7 +215,7 @@ def __init__(self, username, remote_addr=None): .. versionadded:: 0.7 """ - return _request_ctx_stack.top is not None + return _cv_app.get(None) is not None def has_app_context() -> bool: @@ -222,7 +225,7 @@ def has_app_context() -> bool: .. versionadded:: 0.9 """ - return _app_ctx_stack.top is not None + return _cv_req.get(None) is not None class AppContext: @@ -238,28 +241,29 @@ def __init__(self, app: "Flask") -> None: self.app = app self.url_adapter = app.create_url_adapter(None) self.g = app.app_ctx_globals_class() - - # Like request context, app contexts can be pushed multiple times - # but there a basic "refcount" is enough to track them. - self._refcnt = 0 + self._cv_tokens: t.List[contextvars.Token] = [] def push(self) -> None: """Binds the app context to the current context.""" - self._refcnt += 1 - _app_ctx_stack.push(self) + self._cv_tokens.append(_cv_app.set(self)) appcontext_pushed.send(self.app) def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore """Pops the app context.""" try: - self._refcnt -= 1 - if self._refcnt <= 0: + if len(self._cv_tokens) == 1: if exc is _sentinel: exc = sys.exc_info()[1] self.app.do_teardown_appcontext(exc) finally: - rv = _app_ctx_stack.pop() - assert rv is self, f"Popped wrong app context. ({rv!r} instead of {self!r})" + ctx = _cv_app.get() + _cv_app.reset(self._cv_tokens.pop()) + + if ctx is not self: + raise AssertionError( + f"Popped wrong app context. ({ctx!r} instead of {self!r})" + ) + appcontext_popped.send(self.app) def __enter__(self) -> "AppContext": @@ -315,18 +319,13 @@ def __init__( self.request.routing_exception = e self.flashes = None self.session = session - - # Request contexts can be pushed multiple times and interleaved with - # other request contexts. Now only if the last level is popped we - # get rid of them. Additionally if an application context is missing - # one is created implicitly so for each level we add this information - self._implicit_app_ctx_stack: t.List[t.Optional["AppContext"]] = [] - # Functions that should be executed after the request on the response # object. These will be called before the regular "after_request" # functions. self._after_request_functions: t.List[ft.AfterRequestCallable] = [] + self._cv_tokens: t.List[t.Tuple[contextvars.Token, t.Optional[AppContext]]] = [] + def copy(self) -> "RequestContext": """Creates a copy of this request context with the same request object. This can be used to move a request context to a different greenlet. @@ -360,15 +359,15 @@ def match_request(self) -> None: def push(self) -> None: # Before we push the request context we have to ensure that there # is an application context. - app_ctx = _app_ctx_stack.top - if app_ctx is None or app_ctx.app != self.app: + app_ctx = _cv_app.get(None) + + if app_ctx is None or app_ctx.app is not self.app: app_ctx = self.app.app_context() app_ctx.push() - self._implicit_app_ctx_stack.append(app_ctx) else: - self._implicit_app_ctx_stack.append(None) + app_ctx = None - _request_ctx_stack.push(self) + self._cv_tokens.append((_cv_req.set(self), app_ctx)) # Open the session at the moment that the request context is available. # This allows a custom open_session method to use the request context. @@ -394,11 +393,10 @@ def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: igno .. versionchanged:: 0.9 Added the `exc` argument. """ - app_ctx = self._implicit_app_ctx_stack.pop() - clear_request = False + clear_request = len(self._cv_tokens) == 1 try: - if not self._implicit_app_ctx_stack: + if clear_request: if exc is _sentinel: exc = sys.exc_info()[1] self.app.do_teardown_request(exc) @@ -406,36 +404,23 @@ def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: igno request_close = getattr(self.request, "close", None) if request_close is not None: request_close() - clear_request = True finally: - rv = _request_ctx_stack.pop() + ctx = _cv_req.get() + token, app_ctx = self._cv_tokens.pop() + _cv_req.reset(token) # get rid of circular dependencies at the end of the request # so that we don't require the GC to be active. if clear_request: - rv.request.environ["werkzeug.request"] = None + ctx.request.environ["werkzeug.request"] = None - # Get rid of the app as well if necessary. if app_ctx is not None: app_ctx.pop(exc) - assert ( - rv is self - ), f"Popped wrong request context. ({rv!r} instead of {self!r})" - - def auto_pop(self, exc: t.Optional[BaseException]) -> None: - """ - .. deprecated:: 2.2 - Will be removed in Flask 2.3. - """ - import warnings - - warnings.warn( - "'ctx.auto_pop' is deprecated and will be removed in Flask 2.3.", - DeprecationWarning, - stacklevel=2, - ) - self.pop(exc) + if ctx is not self: + raise AssertionError( + f"Popped wrong request context. ({ctx!r} instead of {self!r})" + ) def __enter__(self) -> "RequestContext": self.push() diff --git a/src/flask/globals.py b/src/flask/globals.py index f4781f6cc1..af079877d1 100644 --- a/src/flask/globals.py +++ b/src/flask/globals.py @@ -1,7 +1,7 @@ import typing as t from contextvars import ContextVar -from werkzeug.local import LocalStack +from werkzeug.local import LocalProxy if t.TYPE_CHECKING: # pragma: no cover from .app import Flask @@ -11,6 +11,39 @@ from .sessions import SessionMixin from .wrappers import Request + +class _FakeStack: + def __init__(self, name: str, cv: ContextVar[t.Any]) -> None: + self.name = name + self.cv = cv + + def _warn(self): + import warnings + + warnings.warn( + f"'_{self.name}_ctx_stack' is deprecated and will be" + " removed in Flask 2.3. Use 'g' to store data, or" + f" '{self.name}_ctx' to access the current context.", + DeprecationWarning, + stacklevel=3, + ) + + def push(self, obj: t.Any) -> None: + self._warn() + self.cv.set(obj) + + def pop(self) -> t.Any: + self._warn() + ctx = self.cv.get(None) + self.cv.set(None) + return ctx + + @property + def top(self) -> t.Optional[t.Any]: + self._warn() + return self.cv.get(None) + + _no_app_msg = """\ Working outside of application context. @@ -18,16 +51,16 @@ the current application. To solve this, set up an application context with app.app_context(). See the documentation for more information.\ """ -_cv_app: ContextVar[t.List["AppContext"]] = ContextVar("flask.app_ctx") -_app_ctx_stack: LocalStack["AppContext"] = LocalStack(_cv_app) -app_ctx: "AppContext" = _app_ctx_stack( # type: ignore[assignment] - unbound_message=_no_app_msg +_cv_app: ContextVar["AppContext"] = ContextVar("flask.app_ctx") +__app_ctx_stack = _FakeStack("app", _cv_app) +app_ctx: "AppContext" = LocalProxy( # type: ignore[assignment] + _cv_app, unbound_message=_no_app_msg ) -current_app: "Flask" = _app_ctx_stack( # type: ignore[assignment] - "app", unbound_message=_no_app_msg +current_app: "Flask" = LocalProxy( # type: ignore[assignment] + _cv_app, "app", unbound_message=_no_app_msg ) -g: "_AppCtxGlobals" = _app_ctx_stack( # type: ignore[assignment] - "g", unbound_message=_no_app_msg +g: "_AppCtxGlobals" = LocalProxy( # type: ignore[assignment] + _cv_app, "g", unbound_message=_no_app_msg ) _no_req_msg = """\ @@ -37,14 +70,38 @@ an active HTTP request. Consult the documentation on testing for information about how to avoid this problem.\ """ -_cv_req: ContextVar[t.List["RequestContext"]] = ContextVar("flask.request_ctx") -_request_ctx_stack: LocalStack["RequestContext"] = LocalStack(_cv_req) -request_ctx: "RequestContext" = _request_ctx_stack( # type: ignore[assignment] - unbound_message=_no_req_msg +_cv_req: ContextVar["RequestContext"] = ContextVar("flask.request_ctx") +__request_ctx_stack = _FakeStack("request", _cv_req) +request_ctx: "RequestContext" = LocalProxy( # type: ignore[assignment] + _cv_req, unbound_message=_no_req_msg ) -request: "Request" = _request_ctx_stack( # type: ignore[assignment] - "request", unbound_message=_no_req_msg +request: "Request" = LocalProxy( # type: ignore[assignment] + _cv_req, "request", unbound_message=_no_req_msg ) -session: "SessionMixin" = _request_ctx_stack( # type: ignore[assignment] - "session", unbound_message=_no_req_msg +session: "SessionMixin" = LocalProxy( # type: ignore[assignment] + _cv_req, "session", unbound_message=_no_req_msg ) + + +def __getattr__(name: str) -> t.Any: + if name == "_app_ctx_stack": + import warnings + + warnings.warn( + "'_app_ctx_stack' is deprecated and will be remoevd in Flask 2.3.", + DeprecationWarning, + stacklevel=2, + ) + return __app_ctx_stack + + if name == "_request_ctx_stack": + import warnings + + warnings.warn( + "'_request_ctx_stack' is deprecated and will be remoevd in Flask 2.3.", + DeprecationWarning, + stacklevel=2, + ) + return __request_ctx_stack + + raise AttributeError(name) diff --git a/src/flask/testing.py b/src/flask/testing.py index f652c23a02..0e1189c9f0 100644 --- a/src/flask/testing.py +++ b/src/flask/testing.py @@ -11,7 +11,7 @@ from werkzeug.wrappers import Request as BaseRequest from .cli import ScriptInfo -from .globals import _request_ctx_stack +from .globals import _cv_req from .json import dumps as json_dumps from .sessions import SessionMixin @@ -147,7 +147,7 @@ def session_transaction( app = self.application environ_overrides = kwargs.setdefault("environ_overrides", {}) self.cookie_jar.inject_wsgi(environ_overrides) - outer_reqctx = _request_ctx_stack.top + outer_reqctx = _cv_req.get(None) with app.test_request_context(*args, **kwargs) as c: session_interface = app.session_interface sess = session_interface.open_session(app, c.request) @@ -163,11 +163,11 @@ def session_transaction( # behavior. It's important to not use the push and pop # methods of the actual request context object since that would # mean that cleanup handlers are called - _request_ctx_stack.push(outer_reqctx) + token = _cv_req.set(outer_reqctx) try: yield sess finally: - _request_ctx_stack.pop() + _cv_req.reset(token) resp = app.response_class() if not session_interface.is_null_session(sess): From 82c2e0366ce6b74a3786a64631ce58b85b3a7d4e Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 5 Jul 2022 06:33:03 -0700 Subject: [PATCH 076/229] remove uses of LocalStack --- src/flask/app.py | 28 +++++++++--------- src/flask/cli.py | 8 ++---- src/flask/ctx.py | 40 +++++++++++++------------- src/flask/debughelpers.py | 7 ++--- src/flask/helpers.py | 16 +++++------ src/flask/templating.py | 50 ++++++++++++++------------------- src/flask/testing.py | 2 +- tests/conftest.py | 19 +++++++------ tests/test_appctx.py | 18 ++++++------ tests/test_basic.py | 48 ++++++++++++------------------- tests/test_reqctx.py | 5 ++-- tests/test_session_interface.py | 3 +- tests/test_testing.py | 3 +- 13 files changed, 115 insertions(+), 132 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 736061b944..084429aa92 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -38,10 +38,11 @@ from .ctx import _AppCtxGlobals from .ctx import AppContext from .ctx import RequestContext -from .globals import _app_ctx_stack -from .globals import _request_ctx_stack +from .globals import _cv_app +from .globals import _cv_req from .globals import g from .globals import request +from .globals import request_ctx from .globals import session from .helpers import _split_blueprint_path from .helpers import get_debug_flag @@ -1554,10 +1555,10 @@ def dispatch_request(self) -> ft.ResponseReturnValue: This no longer does the exception handling, this code was moved to the new :meth:`full_dispatch_request`. """ - req = _request_ctx_stack.top.request + req = request_ctx.request if req.routing_exception is not None: self.raise_routing_exception(req) - rule = req.url_rule + rule: Rule = req.url_rule # type: ignore[assignment] # if we provide automatic options for this URL and the # request came with the OPTIONS method, reply automatically if ( @@ -1566,7 +1567,8 @@ def dispatch_request(self) -> ft.ResponseReturnValue: ): return self.make_default_options_response() # otherwise dispatch to the handler for that endpoint - return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args) + view_args: t.Dict[str, t.Any] = req.view_args # type: ignore[assignment] + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) def full_dispatch_request(self) -> Response: """Dispatches the request and on top of that performs request @@ -1631,8 +1633,8 @@ def make_default_options_response(self) -> Response: .. versionadded:: 0.7 """ - adapter = _request_ctx_stack.top.url_adapter - methods = adapter.allowed_methods() + adapter = request_ctx.url_adapter + methods = adapter.allowed_methods() # type: ignore[union-attr] rv = self.response_class() rv.allow.update(methods) return rv @@ -1740,7 +1742,7 @@ def url_for( .. versionadded:: 2.2 Moved from ``flask.url_for``, which calls this method. """ - req_ctx = _request_ctx_stack.top + req_ctx = _cv_req.get(None) if req_ctx is not None: url_adapter = req_ctx.url_adapter @@ -1759,7 +1761,7 @@ def url_for( if _external is None: _external = _scheme is not None else: - app_ctx = _app_ctx_stack.top + app_ctx = _cv_app.get(None) # If called by helpers.url_for, an app context is active, # use its url_adapter. Otherwise, app.url_for was called @@ -1790,7 +1792,7 @@ def url_for( self.inject_url_defaults(endpoint, values) try: - rv = url_adapter.build( + rv = url_adapter.build( # type: ignore[union-attr] endpoint, values, method=_method, @@ -2099,7 +2101,7 @@ def process_response(self, response: Response) -> Response: :return: a new response object or the same, has to be an instance of :attr:`response_class`. """ - ctx = _request_ctx_stack.top + ctx = request_ctx._get_current_object() # type: ignore[attr-defined] for func in ctx._after_request_functions: response = self.ensure_sync(func)(response) @@ -2305,8 +2307,8 @@ def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any: return response(environ, start_response) finally: if "werkzeug.debug.preserve_context" in environ: - environ["werkzeug.debug.preserve_context"](_app_ctx_stack.top) - environ["werkzeug.debug.preserve_context"](_request_ctx_stack.top) + environ["werkzeug.debug.preserve_context"](_cv_app.get()) + environ["werkzeug.debug.preserve_context"](_cv_req.get()) if error is not None and self.should_ignore_error(error): error = None diff --git a/src/flask/cli.py b/src/flask/cli.py index af29b2c1a7..ba0db467ac 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -1051,13 +1051,11 @@ def shell_command() -> None: without having to manually configure the application. """ import code - from .globals import _app_ctx_stack - app = _app_ctx_stack.top.app banner = ( f"Python {sys.version} on {sys.platform}\n" - f"App: {app.import_name} [{app.env}]\n" - f"Instance: {app.instance_path}" + f"App: {current_app.import_name} [{current_app.env}]\n" + f"Instance: {current_app.instance_path}" ) ctx: dict = {} @@ -1068,7 +1066,7 @@ def shell_command() -> None: with open(startup) as f: eval(compile(f.read(), startup, "exec"), ctx) - ctx.update(app.make_shell_context()) + ctx.update(current_app.make_shell_context()) # Site, customize, or startup script can set a hook to call when # entering interactive mode. The default one sets up readline with diff --git a/src/flask/ctx.py b/src/flask/ctx.py index 758127ea78..62242e804c 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -7,10 +7,8 @@ from werkzeug.exceptions import HTTPException from . import typing as ft -from .globals import _app_ctx_stack from .globals import _cv_app from .globals import _cv_req -from .globals import _request_ctx_stack from .signals import appcontext_popped from .signals import appcontext_pushed @@ -106,9 +104,9 @@ def __iter__(self) -> t.Iterator[str]: return iter(self.__dict__) def __repr__(self) -> str: - top = _app_ctx_stack.top - if top is not None: - return f"" + ctx = _cv_app.get(None) + if ctx is not None: + return f"" return object.__repr__(self) @@ -133,15 +131,15 @@ def add_header(response): .. versionadded:: 0.9 """ - top = _request_ctx_stack.top + ctx = _cv_req.get(None) - if top is None: + if ctx is None: raise RuntimeError( - "This decorator can only be used when a request context is" - " active, such as within a view function." + "'after_this_request' can only be used when a request" + " context is active, such as in a view function." ) - top._after_request_functions.append(f) + ctx._after_request_functions.append(f) return f @@ -169,19 +167,19 @@ def do_some_work(): .. versionadded:: 0.10 """ - top = _request_ctx_stack.top + ctx = _cv_req.get(None) - if top is None: + if ctx is None: raise RuntimeError( - "This decorator can only be used when a request context is" - " active, such as within a view function." + "'copy_current_request_context' can only be used when a" + " request context is active, such as in a view function." ) - reqctx = top.copy() + ctx = ctx.copy() def wrapper(*args, **kwargs): - with reqctx: - return reqctx.app.ensure_sync(f)(*args, **kwargs) + with ctx: + return ctx.app.ensure_sync(f)(*args, **kwargs) return update_wrapper(wrapper, f) @@ -240,7 +238,7 @@ class AppContext: def __init__(self, app: "Flask") -> None: self.app = app self.url_adapter = app.create_url_adapter(None) - self.g = app.app_ctx_globals_class() + self.g: _AppCtxGlobals = app.app_ctx_globals_class() self._cv_tokens: t.List[contextvars.Token] = [] def push(self) -> None: @@ -311,14 +309,14 @@ def __init__( self.app = app if request is None: request = app.request_class(environ) - self.request = request + self.request: Request = request self.url_adapter = None try: self.url_adapter = app.create_url_adapter(self.request) except HTTPException as e: self.request.routing_exception = e - self.flashes = None - self.session = session + self.flashes: t.Optional[t.List[t.Tuple[str, str]]] = None + self.session: t.Optional["SessionMixin"] = session # Functions that should be executed after the request on the response # object. These will be called before the regular "after_request" # functions. diff --git a/src/flask/debughelpers.py b/src/flask/debughelpers.py index b1e3ce1bc7..b0639892c4 100644 --- a/src/flask/debughelpers.py +++ b/src/flask/debughelpers.py @@ -2,7 +2,7 @@ from .app import Flask from .blueprints import Blueprint -from .globals import _request_ctx_stack +from .globals import request_ctx class UnexpectedUnicodeError(AssertionError, UnicodeError): @@ -116,9 +116,8 @@ def explain_template_loading_attempts(app: Flask, template, attempts) -> None: info = [f"Locating template {template!r}:"] total_found = 0 blueprint = None - reqctx = _request_ctx_stack.top - if reqctx is not None and reqctx.request.blueprint is not None: - blueprint = reqctx.request.blueprint + if request_ctx and request_ctx.request.blueprint is not None: + blueprint = request_ctx.request.blueprint for idx, (loader, srcobj, triple) in enumerate(attempts): if isinstance(srcobj, Flask): diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 79c562c019..bc074b6af0 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -12,9 +12,10 @@ from werkzeug.exceptions import abort as _wz_abort from werkzeug.utils import redirect as _wz_redirect -from .globals import _request_ctx_stack +from .globals import _cv_req from .globals import current_app from .globals import request +from .globals import request_ctx from .globals import session from .signals import message_flashed @@ -110,11 +111,11 @@ def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: return update_wrapper(decorator, generator_or_function) # type: ignore def generator() -> t.Generator: - ctx = _request_ctx_stack.top + ctx = _cv_req.get(None) if ctx is None: raise RuntimeError( - "Attempted to stream with context but " - "there was no context in the first place to keep around." + "'stream_with_context' can only be used when a request" + " context is active, such as in a view function." ) with ctx: # Dummy sentinel. Has to be inside the context block or we're @@ -377,11 +378,10 @@ def get_flashed_messages( :param category_filter: filter of categories to limit return values. Only categories in the list will be returned. """ - flashes = _request_ctx_stack.top.flashes + flashes = request_ctx.flashes if flashes is None: - _request_ctx_stack.top.flashes = flashes = ( - session.pop("_flashes") if "_flashes" in session else [] - ) + flashes = session.pop("_flashes") if "_flashes" in session else [] + request_ctx.flashes = flashes if category_filter: flashes = list(filter(lambda f: f[0] in category_filter, flashes)) if not with_categories: diff --git a/src/flask/templating.py b/src/flask/templating.py index 7d92cf1e94..24a672b749 100644 --- a/src/flask/templating.py +++ b/src/flask/templating.py @@ -5,8 +5,8 @@ from jinja2 import Template from jinja2 import TemplateNotFound -from .globals import _app_ctx_stack -from .globals import _request_ctx_stack +from .globals import _cv_app +from .globals import _cv_req from .globals import current_app from .globals import request from .helpers import stream_with_context @@ -22,9 +22,9 @@ def _default_template_ctx_processor() -> t.Dict[str, t.Any]: """Default template context processor. Injects `request`, `session` and `g`. """ - reqctx = _request_ctx_stack.top - appctx = _app_ctx_stack.top - rv = {} + appctx = _cv_app.get(None) + reqctx = _cv_req.get(None) + rv: t.Dict[str, t.Any] = {} if appctx is not None: rv["g"] = appctx.g if reqctx is not None: @@ -124,7 +124,8 @@ def list_templates(self) -> t.List[str]: return list(result) -def _render(template: Template, context: dict, app: "Flask") -> str: +def _render(app: "Flask", template: Template, context: t.Dict[str, t.Any]) -> str: + app.update_template_context(context) before_render_template.send(app, template=template, context=context) rv = template.render(context) template_rendered.send(app, template=template, context=context) @@ -135,36 +136,27 @@ def render_template( template_name_or_list: t.Union[str, Template, t.List[t.Union[str, Template]]], **context: t.Any ) -> str: - """Renders a template from the template folder with the given - context. + """Render a template by name with the given context. - :param template_name_or_list: the name of the template to be - rendered, or an iterable with template names - the first one existing will be rendered - :param context: the variables that should be available in the - context of the template. + :param template_name_or_list: The name of the template to render. If + a list is given, the first name to exist will be rendered. + :param context: The variables to make available in the template. """ - ctx = _app_ctx_stack.top - ctx.app.update_template_context(context) - return _render( - ctx.app.jinja_env.get_or_select_template(template_name_or_list), - context, - ctx.app, - ) + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.get_or_select_template(template_name_or_list) + return _render(app, template, context) def render_template_string(source: str, **context: t.Any) -> str: - """Renders a template from the given template source string - with the given context. Template variables will be autoescaped. + """Render a template from the given source string with the given + context. - :param source: the source code of the template to be - rendered - :param context: the variables that should be available in the - context of the template. + :param source: The source code of the template to render. + :param context: The variables to make available in the template. """ - ctx = _app_ctx_stack.top - ctx.app.update_template_context(context) - return _render(ctx.app.jinja_env.from_string(source), context, ctx.app) + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.from_string(source) + return _render(app, template, context) def _stream( diff --git a/src/flask/testing.py b/src/flask/testing.py index 0e1189c9f0..e188439b18 100644 --- a/src/flask/testing.py +++ b/src/flask/testing.py @@ -163,7 +163,7 @@ def session_transaction( # behavior. It's important to not use the push and pop # methods of the actual request context object since that would # mean that cleanup handlers are called - token = _cv_req.set(outer_reqctx) + token = _cv_req.set(outer_reqctx) # type: ignore[arg-type] try: yield sess finally: diff --git a/tests/conftest.py b/tests/conftest.py index 1e1ba0d449..670acc8876 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,8 +6,8 @@ import pytest from _pytest import monkeypatch -import flask -from flask import Flask as _Flask +from flask import Flask +from flask.globals import request_ctx @pytest.fixture(scope="session", autouse=True) @@ -44,14 +44,13 @@ def _reset_os_environ(monkeypatch, _standard_os_environ): monkeypatch._setitem.extend(_standard_os_environ) -class Flask(_Flask): - testing = True - secret_key = "test key" - - @pytest.fixture def app(): app = Flask("flask_test", root_path=os.path.dirname(__file__)) + app.config.update( + TESTING=True, + SECRET_KEY="test key", + ) return app @@ -92,8 +91,10 @@ def leak_detector(): # make sure we're not leaking a request context since we are # testing flask internally in debug mode in a few cases leaks = [] - while flask._request_ctx_stack.top is not None: - leaks.append(flask._request_ctx_stack.pop()) + while request_ctx: + leaks.append(request_ctx._get_current_object()) + request_ctx.pop() + assert leaks == [] diff --git a/tests/test_appctx.py b/tests/test_appctx.py index aeb75a55c0..f5ca0bde42 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -1,6 +1,8 @@ import pytest import flask +from flask.globals import app_ctx +from flask.globals import request_ctx def test_basic_url_generation(app): @@ -29,14 +31,14 @@ def test_url_generation_without_context_fails(): def test_request_context_means_app_context(app): with app.test_request_context(): - assert flask.current_app._get_current_object() == app - assert flask._app_ctx_stack.top is None + assert flask.current_app._get_current_object() is app + assert not flask.current_app def test_app_context_provides_current_app(app): with app.app_context(): - assert flask.current_app._get_current_object() == app - assert flask._app_ctx_stack.top is None + assert flask.current_app._get_current_object() is app + assert not flask.current_app def test_app_tearing_down(app): @@ -175,11 +177,11 @@ def teardown_app(error=None): @app.route("/") def index(): - with flask._app_ctx_stack.top: - with flask._request_ctx_stack.top: + with app_ctx: + with request_ctx: pass - env = flask._request_ctx_stack.top.request.environ - assert env["werkzeug.request"] is not None + + assert flask.request.environ["werkzeug.request"] is not None return "" res = client.get("/") diff --git a/tests/test_basic.py b/tests/test_basic.py index dca48e2d38..3e98fdac46 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1110,14 +1110,10 @@ def test_enctype_debug_helper(app, client): def index(): return flask.request.files["foo"].filename - # with statement is important because we leave an exception on the - # stack otherwise and we want to ensure that this is not the case - # to not negatively affect other tests. - with client: - with pytest.raises(DebugFilesKeyError) as e: - client.post("/fail", data={"foo": "index.txt"}) - assert "no file contents were transmitted" in str(e.value) - assert "This was submitted: 'index.txt'" in str(e.value) + with pytest.raises(DebugFilesKeyError) as e: + client.post("/fail", data={"foo": "index.txt"}) + assert "no file contents were transmitted" in str(e.value) + assert "This was submitted: 'index.txt'" in str(e.value) def test_response_types(app, client): @@ -1548,29 +1544,21 @@ def subdomain(): assert rv.data == b"subdomain" -@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") -@pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning") -def test_exception_propagation(app, client): - def apprunner(config_key): - @app.route("/") - def index(): - 1 // 0 +@pytest.mark.parametrize("key", ["TESTING", "PROPAGATE_EXCEPTIONS", "DEBUG", None]) +def test_exception_propagation(app, client, key): + app.testing = False - if config_key is not None: - app.config[config_key] = True - with pytest.raises(Exception): - client.get("/") - else: - assert client.get("/").status_code == 500 - - # we have to run this test in an isolated thread because if the - # debug flag is set to true and an exception happens the context is - # not torn down. This causes other tests that run after this fail - # when they expect no exception on the stack. - for config_key in "TESTING", "PROPAGATE_EXCEPTIONS", "DEBUG", None: - t = Thread(target=apprunner, args=(config_key,)) - t.start() - t.join() + @app.route("/") + def index(): + 1 // 0 + + if key is not None: + app.config[key] = True + + with pytest.raises(ZeroDivisionError): + client.get("/") + else: + assert client.get("/").status_code == 500 @pytest.mark.parametrize("debug", [True, False]) diff --git a/tests/test_reqctx.py b/tests/test_reqctx.py index 1a478917be..abfacb98bf 100644 --- a/tests/test_reqctx.py +++ b/tests/test_reqctx.py @@ -3,6 +3,7 @@ import pytest import flask +from flask.globals import request_ctx from flask.sessions import SecureCookieSessionInterface from flask.sessions import SessionInterface @@ -116,7 +117,7 @@ def meh(): assert index() == "Hello World!" with app.test_request_context("/meh"): assert meh() == "http://localhost/meh" - assert flask._request_ctx_stack.top is None + assert not flask.request def test_context_test(app): @@ -152,7 +153,7 @@ def test_greenlet_context_copying(self, app, client): @app.route("/") def index(): flask.session["fizz"] = "buzz" - reqctx = flask._request_ctx_stack.top.copy() + reqctx = request_ctx.copy() def g(): assert not flask.request diff --git a/tests/test_session_interface.py b/tests/test_session_interface.py index 39562f5ad1..613da37fd2 100644 --- a/tests/test_session_interface.py +++ b/tests/test_session_interface.py @@ -1,4 +1,5 @@ import flask +from flask.globals import request_ctx from flask.sessions import SessionInterface @@ -13,7 +14,7 @@ def save_session(self, app, session, response): pass def open_session(self, app, request): - flask._request_ctx_stack.top.match_request() + request_ctx.match_request() assert request.endpoint is not None app = flask.Flask(__name__) diff --git a/tests/test_testing.py b/tests/test_testing.py index dd6347e5f3..0b37a2e5bd 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -5,6 +5,7 @@ import flask from flask import appcontext_popped from flask.cli import ScriptInfo +from flask.globals import _cv_req from flask.json import jsonify from flask.testing import EnvironBuilder from flask.testing import FlaskCliRunner @@ -399,4 +400,4 @@ def index(): # close the response, releasing the context held by stream_with_context rv.close() # only req_ctx fixture should still be pushed - assert flask._request_ctx_stack.top is req_ctx + assert _cv_req.get(None) is req_ctx From e0dad454810dd081947d3ca2ff376c5096185698 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 8 Jul 2022 07:08:54 -0700 Subject: [PATCH 077/229] update docs about contexts --- docs/api.rst | 56 ++++++++++----------------------------- docs/appcontext.rst | 8 ------ docs/extensiondev.rst | 6 ----- docs/patterns/sqlite3.rst | 4 --- docs/reqcontext.rst | 25 ++++++++--------- src/flask/app.py | 43 +++++++++++++++--------------- src/flask/ctx.py | 18 ++++++------- src/flask/scaffold.py | 45 +++++++++++++++---------------- src/flask/testing.py | 9 +++---- 9 files changed, 82 insertions(+), 132 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 217473bc39..67772a77b2 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -311,56 +311,28 @@ Useful Internals .. autoclass:: flask.ctx.RequestContext :members: -.. data:: _request_ctx_stack +.. data:: flask.globals.request_ctx - The internal :class:`~werkzeug.local.LocalStack` that holds - :class:`~flask.ctx.RequestContext` instances. Typically, the - :data:`request` and :data:`session` proxies should be accessed - instead of the stack. It may be useful to access the stack in - extension code. + The current :class:`~flask.ctx.RequestContext`. If a request context + is not active, accessing attributes on this proxy will raise a + ``RuntimeError``. - The following attributes are always present on each layer of the - stack: - - `app` - the active Flask application. - - `url_adapter` - the URL adapter that was used to match the request. - - `request` - the current request object. - - `session` - the active session object. - - `g` - an object with all the attributes of the :data:`flask.g` object. - - `flashes` - an internal cache for the flashed messages. - - Example usage:: - - from flask import _request_ctx_stack - - def get_session(): - ctx = _request_ctx_stack.top - if ctx is not None: - return ctx.session + This is an internal object that is essential to how Flask handles + requests. Accessing this should not be needed in most cases. Most + likely you want :data:`request` and :data:`session` instead. .. autoclass:: flask.ctx.AppContext :members: -.. data:: _app_ctx_stack +.. data:: flask.globals.app_ctx - The internal :class:`~werkzeug.local.LocalStack` that holds - :class:`~flask.ctx.AppContext` instances. Typically, the - :data:`current_app` and :data:`g` proxies should be accessed instead - of the stack. Extensions can access the contexts on the stack as a - namespace to store data. + The current :class:`~flask.ctx.AppContext`. If an app context is not + active, accessing attributes on this proxy will raise a + ``RuntimeError``. - .. versionadded:: 0.9 + This is an internal object that is essential to how Flask handles + requests. Accessing this should not be needed in most cases. Most + likely you want :data:`current_app` and :data:`g` instead. .. autoclass:: flask.blueprints.BlueprintSetupState :members: diff --git a/docs/appcontext.rst b/docs/appcontext.rst index b214f254e6..a4ae38617c 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -136,14 +136,6 @@ local from ``get_db()``:: Accessing ``db`` will call ``get_db`` internally, in the same way that :data:`current_app` works. ----- - -If you're writing an extension, :data:`g` should be reserved for user -code. You may store internal data on the context itself, but be sure to -use a sufficiently unique name. The current context is accessed with -:data:`_app_ctx_stack.top <_app_ctx_stack>`. For more information see -:doc:`/extensiondev`. - Events and Signals ------------------ diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 25ced1870c..95745119d1 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -187,12 +187,6 @@ when the application context ends. If it should only be valid during a request, or would not be used in the CLI outside a reqeust, use :meth:`~flask.Flask.teardown_request`. -An older technique for storing context data was to store it on -``_app_ctx_stack.top`` or ``_request_ctx_stack.top``. However, this just -moves the same namespace collision problem elsewhere (although less -likely) and modifies objects that are very internal to Flask's -operations. Prefer storing data under a unique name in ``g``. - Views and Models ---------------- diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index 12336fb1b4..5932589ffa 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -30,10 +30,6 @@ or create an application context itself. At that point the ``get_db`` function can be used to get the current database connection. Whenever the context is destroyed the database connection will be terminated. -Note: if you use Flask 0.9 or older you need to use -``flask._app_ctx_stack.top`` instead of ``g`` as the :data:`flask.g` -object was bound to the request and not application context. - Example:: @app.route('/') diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 96e17e3b9a..2b109770e5 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -37,12 +37,14 @@ context, which also pushes an :doc:`app context `. When the request ends it pops the request context then the application context. The context is unique to each thread (or other worker type). -:data:`request` cannot be passed to another thread, the other thread -will have a different context stack and will not know about the request -the parent thread was pointing to. +:data:`request` cannot be passed to another thread, the other thread has +a different context space and will not know about the request the parent +thread was pointing to. -Context locals are implemented in Werkzeug. See :doc:`werkzeug:local` -for more information on how this works internally. +Context locals are implemented using Python's :mod:`contextvars` and +Werkzeug's :class:`~werkzeug.local.LocalProxy`. Python manages the +lifetime of context vars automatically, and local proxy wraps that +low-level interface to make the data easier to work with. Manually Push a Context @@ -87,10 +89,9 @@ How the Context Works The :meth:`Flask.wsgi_app` method is called to handle each request. It manages the contexts during the request. Internally, the request and -application contexts work as stacks, :data:`_request_ctx_stack` and -:data:`_app_ctx_stack`. When contexts are pushed onto the stack, the +application contexts work like stacks. When contexts are pushed, the proxies that depend on them are available and point at information from -the top context on the stack. +the top item. When the request starts, a :class:`~ctx.RequestContext` is created and pushed, which creates and pushes an :class:`~ctx.AppContext` first if @@ -99,10 +100,10 @@ these contexts are pushed, the :data:`current_app`, :data:`g`, :data:`request`, and :data:`session` proxies are available to the original thread handling the request. -Because the contexts are stacks, other contexts may be pushed to change -the proxies during a request. While this is not a common pattern, it -can be used in advanced applications to, for example, do internal -redirects or chain different applications together. +Other contexts may be pushed to change the proxies during a request. +While this is not a common pattern, it can be used in advanced +applications to, for example, do internal redirects or chain different +applications together. After the request is dispatched and a response is generated and sent, the request context is popped, which then pops the application context. diff --git a/src/flask/app.py b/src/flask/app.py index 084429aa92..19d7735103 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1284,29 +1284,30 @@ def before_first_request(self, f: T_before_first_request) -> T_before_first_requ @setupmethod def teardown_appcontext(self, f: T_teardown) -> T_teardown: - """Registers a function to be called when the application context - ends. These functions are typically also called when the request - context is popped. + """Registers a function to be called when the application + context is popped. The application context is typically popped + after the request context for each request, at the end of CLI + commands, or after a manually pushed context ends. - Example:: - - ctx = app.app_context() - ctx.push() - ... - ctx.pop() - - When ``ctx.pop()`` is executed in the above example, the teardown - functions are called just before the app context moves from the - stack of active contexts. This becomes relevant if you are using - such constructs in tests. - - Since a request context typically also manages an application - context it would also be called when you pop a request context. + .. code-block:: python - When a teardown function was called because of an unhandled exception - it will be passed an error object. If an :meth:`errorhandler` is - registered, it will handle the exception and the teardown will not - receive it. + with app.app_context(): + ... + + When the ``with`` block exits (or ``ctx.pop()`` is called), the + teardown functions are called just before the app context is + made inactive. Since a request context typically also manages an + application context it would also be called when you pop a + request context. + + When a teardown function was called because of an unhandled + exception it will be passed an error object. If an + :meth:`errorhandler` is registered, it will handle the exception + and the teardown will not receive it. + + Teardown functions must avoid raising exceptions. If they + execute code that might fail they must surround that code with a + ``try``/``except`` block and log any errors. The return values of teardown functions are ignored. diff --git a/src/flask/ctx.py b/src/flask/ctx.py index 62242e804c..1ea2719bda 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -227,12 +227,10 @@ def has_app_context() -> bool: class AppContext: - """The application context binds an application object implicitly - to the current thread or greenlet, similar to how the - :class:`RequestContext` binds request information. The application - context is also implicitly created if a request context is created - but the application is not on top of the individual application - context. + """The app context contains application-specific information. An app + context is created and pushed at the beginning of each request if + one is not already active. An app context is also pushed when + running CLI commands. """ def __init__(self, app: "Flask") -> None: @@ -278,10 +276,10 @@ def __exit__( class RequestContext: - """The request context contains all request relevant information. It is - created at the beginning of the request and pushed to the - `_request_ctx_stack` and removed at the end of it. It will create the - URL adapter and request object for the WSGI environment provided. + """The request context contains per-request information. The Flask + app creates and pushes it at the beginning of the request, then pops + it at the end of the request. It will create the URL adapter and + request object for the WSGI environment provided. Do not attempt to use this class directly, instead use :meth:`~flask.Flask.test_request_context` and diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 7f099f40c4..1f6882ac43 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -574,30 +574,27 @@ def after_request(self, f: T_after_request) -> T_after_request: @setupmethod def teardown_request(self, f: T_teardown) -> T_teardown: - """Register a function to be run at the end of each request, - regardless of whether there was an exception or not. These functions - are executed when the request context is popped, even if not an - actual request was performed. - - Example:: - - ctx = app.test_request_context() - ctx.push() - ... - ctx.pop() - - When ``ctx.pop()`` is executed in the above example, the teardown - functions are called just before the request context moves from the - stack of active contexts. This becomes relevant if you are using - such constructs in tests. - - Teardown functions must avoid raising exceptions. If - they execute code that might fail they - will have to surround the execution of that code with try/except - statements and log any errors. - - When a teardown function was called because of an exception it will - be passed an error object. + """Register a function to be called when the request context is + popped. Typically this happens at the end of each request, but + contexts may be pushed manually as well during testing. + + .. code-block:: python + + with app.test_request_context(): + ... + + When the ``with`` block exits (or ``ctx.pop()`` is called), the + teardown functions are called just before the request context is + made inactive. + + When a teardown function was called because of an unhandled + exception it will be passed an error object. If an + :meth:`errorhandler` is registered, it will handle the exception + and the teardown will not receive it. + + Teardown functions must avoid raising exceptions. If they + execute code that might fail they must surround that code with a + ``try``/``except`` block and log any errors. The return values of teardown functions are ignored. """ diff --git a/src/flask/testing.py b/src/flask/testing.py index e188439b18..bf84f8484f 100644 --- a/src/flask/testing.py +++ b/src/flask/testing.py @@ -94,11 +94,10 @@ def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore class FlaskClient(Client): - """Works like a regular Werkzeug test client but has some knowledge about - how Flask works to defer the cleanup of the request context stack to the - end of a ``with`` body when used in a ``with`` statement. For general - information about how to use this class refer to - :class:`werkzeug.test.Client`. + """Works like a regular Werkzeug test client but has knowledge about + Flask's contexts to defer the cleanup of the request context until + the end of a ``with`` block. For general information about how to + use this class refer to :class:`werkzeug.test.Client`. .. versionchanged:: 0.12 `app.test_client()` includes preset default environment, which can be From 979e0adbacfb3c5cc1ac678b4ef5373dadde83d3 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 8 Jul 2022 12:02:18 -0700 Subject: [PATCH 078/229] fix pr link --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5fda08e3a0..55c5c28170 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,7 +17,7 @@ Unreleased - The app and request contexts are managed using Python context vars directly rather than Werkzeug's ``LocalStack``. This should result - in better performance and memory use. :pr:`4672` + in better performance and memory use. :pr:`4682` - Extension maintainers, be aware that ``_app_ctx_stack.top`` and ``_request_ctx_stack.top`` are deprecated. Store data on From 91044c4d769efa7f20455e1b70457a7da95c26d8 Mon Sep 17 00:00:00 2001 From: pgjones Date: Sat, 9 Jul 2022 13:41:35 +0100 Subject: [PATCH 079/229] Change _cv_req -> _cv_request This is a clearer name for the variable. --- src/flask/app.py | 6 +++--- src/flask/ctx.py | 14 +++++++------- src/flask/globals.py | 10 +++++----- src/flask/helpers.py | 4 ++-- src/flask/templating.py | 4 ++-- src/flask/testing.py | 8 ++++---- tests/test_testing.py | 4 ++-- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 19d7735103..ffa66c547f 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -39,7 +39,7 @@ from .ctx import AppContext from .ctx import RequestContext from .globals import _cv_app -from .globals import _cv_req +from .globals import _cv_request from .globals import g from .globals import request from .globals import request_ctx @@ -1743,7 +1743,7 @@ def url_for( .. versionadded:: 2.2 Moved from ``flask.url_for``, which calls this method. """ - req_ctx = _cv_req.get(None) + req_ctx = _cv_request.get(None) if req_ctx is not None: url_adapter = req_ctx.url_adapter @@ -2309,7 +2309,7 @@ def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any: finally: if "werkzeug.debug.preserve_context" in environ: environ["werkzeug.debug.preserve_context"](_cv_app.get()) - environ["werkzeug.debug.preserve_context"](_cv_req.get()) + environ["werkzeug.debug.preserve_context"](_cv_request.get()) if error is not None and self.should_ignore_error(error): error = None diff --git a/src/flask/ctx.py b/src/flask/ctx.py index 1ea2719bda..d0e7e1e45f 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -8,7 +8,7 @@ from . import typing as ft from .globals import _cv_app -from .globals import _cv_req +from .globals import _cv_request from .signals import appcontext_popped from .signals import appcontext_pushed @@ -131,7 +131,7 @@ def add_header(response): .. versionadded:: 0.9 """ - ctx = _cv_req.get(None) + ctx = _cv_request.get(None) if ctx is None: raise RuntimeError( @@ -167,7 +167,7 @@ def do_some_work(): .. versionadded:: 0.10 """ - ctx = _cv_req.get(None) + ctx = _cv_request.get(None) if ctx is None: raise RuntimeError( @@ -223,7 +223,7 @@ def has_app_context() -> bool: .. versionadded:: 0.9 """ - return _cv_req.get(None) is not None + return _cv_request.get(None) is not None class AppContext: @@ -363,7 +363,7 @@ def push(self) -> None: else: app_ctx = None - self._cv_tokens.append((_cv_req.set(self), app_ctx)) + self._cv_tokens.append((_cv_request.set(self), app_ctx)) # Open the session at the moment that the request context is available. # This allows a custom open_session method to use the request context. @@ -401,9 +401,9 @@ def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: igno if request_close is not None: request_close() finally: - ctx = _cv_req.get() + ctx = _cv_request.get() token, app_ctx = self._cv_tokens.pop() - _cv_req.reset(token) + _cv_request.reset(token) # get rid of circular dependencies at the end of the request # so that we don't require the GC to be active. diff --git a/src/flask/globals.py b/src/flask/globals.py index af079877d1..b230ef7e06 100644 --- a/src/flask/globals.py +++ b/src/flask/globals.py @@ -70,16 +70,16 @@ def top(self) -> t.Optional[t.Any]: an active HTTP request. Consult the documentation on testing for information about how to avoid this problem.\ """ -_cv_req: ContextVar["RequestContext"] = ContextVar("flask.request_ctx") -__request_ctx_stack = _FakeStack("request", _cv_req) +_cv_request: ContextVar["RequestContext"] = ContextVar("flask.request_ctx") +__request_ctx_stack = _FakeStack("request", _cv_request) request_ctx: "RequestContext" = LocalProxy( # type: ignore[assignment] - _cv_req, unbound_message=_no_req_msg + _cv_request, unbound_message=_no_req_msg ) request: "Request" = LocalProxy( # type: ignore[assignment] - _cv_req, "request", unbound_message=_no_req_msg + _cv_request, "request", unbound_message=_no_req_msg ) session: "SessionMixin" = LocalProxy( # type: ignore[assignment] - _cv_req, "session", unbound_message=_no_req_msg + _cv_request, "session", unbound_message=_no_req_msg ) diff --git a/src/flask/helpers.py b/src/flask/helpers.py index bc074b6af0..fb054466b3 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -12,7 +12,7 @@ from werkzeug.exceptions import abort as _wz_abort from werkzeug.utils import redirect as _wz_redirect -from .globals import _cv_req +from .globals import _cv_request from .globals import current_app from .globals import request from .globals import request_ctx @@ -111,7 +111,7 @@ def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: return update_wrapper(decorator, generator_or_function) # type: ignore def generator() -> t.Generator: - ctx = _cv_req.get(None) + ctx = _cv_request.get(None) if ctx is None: raise RuntimeError( "'stream_with_context' can only be used when a request" diff --git a/src/flask/templating.py b/src/flask/templating.py index 24a672b749..25cc3f95e6 100644 --- a/src/flask/templating.py +++ b/src/flask/templating.py @@ -6,7 +6,7 @@ from jinja2 import TemplateNotFound from .globals import _cv_app -from .globals import _cv_req +from .globals import _cv_request from .globals import current_app from .globals import request from .helpers import stream_with_context @@ -23,7 +23,7 @@ def _default_template_ctx_processor() -> t.Dict[str, t.Any]: `session` and `g`. """ appctx = _cv_app.get(None) - reqctx = _cv_req.get(None) + reqctx = _cv_request.get(None) rv: t.Dict[str, t.Any] = {} if appctx is not None: rv["g"] = appctx.g diff --git a/src/flask/testing.py b/src/flask/testing.py index bf84f8484f..6db11416df 100644 --- a/src/flask/testing.py +++ b/src/flask/testing.py @@ -11,7 +11,7 @@ from werkzeug.wrappers import Request as BaseRequest from .cli import ScriptInfo -from .globals import _cv_req +from .globals import _cv_request from .json import dumps as json_dumps from .sessions import SessionMixin @@ -146,7 +146,7 @@ def session_transaction( app = self.application environ_overrides = kwargs.setdefault("environ_overrides", {}) self.cookie_jar.inject_wsgi(environ_overrides) - outer_reqctx = _cv_req.get(None) + outer_reqctx = _cv_request.get(None) with app.test_request_context(*args, **kwargs) as c: session_interface = app.session_interface sess = session_interface.open_session(app, c.request) @@ -162,11 +162,11 @@ def session_transaction( # behavior. It's important to not use the push and pop # methods of the actual request context object since that would # mean that cleanup handlers are called - token = _cv_req.set(outer_reqctx) # type: ignore[arg-type] + token = _cv_request.set(outer_reqctx) # type: ignore[arg-type] try: yield sess finally: - _cv_req.reset(token) + _cv_request.reset(token) resp = app.response_class() if not session_interface.is_null_session(sess): diff --git a/tests/test_testing.py b/tests/test_testing.py index 0b37a2e5bd..b494ded641 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -5,7 +5,7 @@ import flask from flask import appcontext_popped from flask.cli import ScriptInfo -from flask.globals import _cv_req +from flask.globals import _cv_request from flask.json import jsonify from flask.testing import EnvironBuilder from flask.testing import FlaskCliRunner @@ -400,4 +400,4 @@ def index(): # close the response, releasing the context held by stream_with_context rv.close() # only req_ctx fixture should still be pushed - assert _cv_req.get(None) is req_ctx + assert _cv_request.get(None) is req_ctx From 58ecacd271c2a07018cb6f65da164d4bed1e14ea Mon Sep 17 00:00:00 2001 From: Pedro Torcatt Date: Sun, 10 Jul 2022 07:22:56 -0400 Subject: [PATCH 080/229] Change Roll back by Rever --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 55c5c28170..8847ef1386 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -236,7 +236,7 @@ Released 2021-05-21 the endpoint name. :issue:`4041` - Combine URL prefixes when nesting blueprints that were created with a ``url_prefix`` value. :issue:`4037` -- Roll back a change to the order that URL matching was done. The +- Revert a change to the order that URL matching was done. The URL is again matched after the session is loaded, so the session is available in custom URL converters. :issue:`4053` - Re-add deprecated ``Config.from_json``, which was accidentally From 03b410066b96f63ba2a215750f881d5bebb78a29 Mon Sep 17 00:00:00 2001 From: Pedro Torcatt Date: Mon, 11 Jul 2022 18:02:00 -0400 Subject: [PATCH 081/229] Fix docs/CHANGES.rst typo --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8847ef1386..e0e15802c1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -914,7 +914,7 @@ Released 2013-06-13, codename Limoncello - Set the content-length header for x-sendfile. - ``tojson`` filter now does not escape script blocks in HTML5 parsers. -- ``tojson`` used in templates is now safe by default due. This was +- ``tojson`` used in templates is now safe by default. This was allowed due to the different escaping behavior. - Flask will now raise an error if you attempt to register a new function on an already used endpoint. From 69f9845ef2da3051d74d4dade3e88ccf5b2ee3de Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 13 Jul 2022 07:41:43 -0700 Subject: [PATCH 082/229] add json provider interface --- CHANGES.rst | 17 +- docs/api.rst | 32 ++-- docs/config.rst | 25 ++- src/flask/app.py | 62 +++++-- src/flask/blueprints.py | 10 +- src/flask/ctx.py | 1 + src/flask/json/__init__.py | 362 ++++++++++++++++++++----------------- src/flask/json/provider.py | 310 +++++++++++++++++++++++++++++++ src/flask/scaffold.py | 12 +- src/flask/testing.py | 5 +- tests/test_basic.py | 33 ++-- tests/test_json.py | 77 ++------ tests/test_testing.py | 2 +- 13 files changed, 664 insertions(+), 284 deletions(-) create mode 100644 src/flask/json/provider.py diff --git a/CHANGES.rst b/CHANGES.rst index e0e15802c1..1c0ce74f04 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -32,7 +32,22 @@ Unreleased ``Flask.aborter_class`` and ``Flask.make_aborter`` can be used to customize this aborter. :issue:`4567` - ``flask.redirect`` will call ``app.redirect``. :issue:`4569` - + - ``flask.json`` is an instance of ``JSONProvider``. A different + provider can be set to use a different JSON library. + ``flask.jsonify`` will call ``app.json.response``, other + functions in ``flask.json`` will call corresponding functions in + ``app.json``. :pr:`4688` + +- JSON configuration is moved to attributes on the default + ``app.json`` provider. ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, + ``JSONIFY_MIMETYPE``, and ``JSONIFY_PRETTYPRINT_REGULAR`` are + deprecated. :pr:`4688` +- Setting custom ``json_encoder`` and ``json_decoder`` classes on the + app or a blueprint, and the corresponding ``json.JSONEncoder`` and + ``JSONDecoder`` classes, are deprecated. JSON behavior can now be + overridden using the ``app.json`` provider interface. :pr:`4688` +- ``json.htmlsafe_dumps`` and ``json.htmlsafe_dump`` are deprecated, + the function is built-in to Jinja now. :pr:`4688` - Refactor ``register_error_handler`` to consolidate error checking. Rewrite some error messages to be more consistent. :issue:`4559` - Use Blueprint decorators and functions intended for setup after diff --git a/docs/api.rst b/docs/api.rst index 67772a77b2..5359b37044 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -236,21 +236,15 @@ JSON Support .. module:: flask.json -Flask uses the built-in :mod:`json` module for handling JSON. It will -use the current blueprint's or application's JSON encoder and decoder -for easier customization. By default it handles some extra data types: - -- :class:`datetime.datetime` and :class:`datetime.date` are serialized - to :rfc:`822` strings. This is the same as the HTTP date format. -- :class:`uuid.UUID` is serialized to a string. -- :class:`dataclasses.dataclass` is passed to - :func:`dataclasses.asdict`. -- :class:`~markupsafe.Markup` (or any object with a ``__html__`` - method) will call the ``__html__`` method to get a string. - -Jinja's ``|tojson`` filter is configured to use Flask's :func:`dumps` -function. The filter marks the output with ``|safe`` automatically. Use -the filter to render data inside `` From 43ef559de3ecb1915aabe8156f357d7ac4cd22b3 Mon Sep 17 00:00:00 2001 From: "Maxim G. Ivanov" Date: Wed, 21 Dec 2022 10:41:11 +0700 Subject: [PATCH 141/229] Fix varname in docs --- docs/views.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/views.rst b/docs/views.rst index 3eebbfaacf..8937d7b55c 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -256,7 +256,7 @@ provide get (list) and post (create) methods. return self.model.query.get_or_404(id) def get(self, id): - user = self._get_item(id) + item = self._get_item(id) return jsonify(item.to_json()) def patch(self, id): From 43bc7330cece1dbe75e7d6c326a0dea8d5652699 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 22 Dec 2022 09:23:48 -0800 Subject: [PATCH 142/229] update requirements --- .pre-commit-config.yaml | 4 ++-- requirements/dev.txt | 26 ++++++++++++++++---------- requirements/docs.txt | 10 ++++------ requirements/tests.txt | 8 +++----- requirements/typing.txt | 4 ++-- 5 files changed, 27 insertions(+), 25 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45b3e0f2fa..2b1dee57df 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ ci: autoupdate_schedule: monthly repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.3.0 + rev: v3.3.1 hooks: - id: pyupgrade args: ["--py37-plus"] @@ -15,7 +15,7 @@ repos: files: "^(?!examples/)" args: ["--application-directories", "src"] - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 22.12.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 diff --git a/requirements/dev.txt b/requirements/dev.txt index 7597d9c9c2..41b2619ce3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -10,19 +10,25 @@ -r typing.txt build==0.9.0 # via pip-tools +cachetools==5.2.0 + # via tox cfgv==3.3.1 # via pre-commit +chardet==5.1.0 + # via tox click==8.1.3 # via # pip-compile-multi # pip-tools +colorama==0.4.6 + # via tox distlib==0.3.6 # via virtualenv -filelock==3.8.0 +filelock==3.8.2 # via # tox # virtualenv -identify==2.5.9 +identify==2.5.11 # via pre-commit nodeenv==1.7.0 # via pre-commit @@ -30,25 +36,25 @@ pep517==0.13.0 # via build pip-compile-multi==2.6.1 # via -r requirements/dev.in -pip-tools==6.10.0 +pip-tools==6.12.1 # via pip-compile-multi -platformdirs==2.5.4 - # via virtualenv +platformdirs==2.6.0 + # via + # tox + # virtualenv pre-commit==2.20.0 # via -r requirements/dev.in -py==1.11.0 +pyproject-api==1.2.1 # via tox pyyaml==6.0 # via pre-commit -six==1.16.0 - # via tox toml==0.10.2 # via pre-commit toposort==1.7 # via pip-compile-multi -tox==3.27.1 +tox==4.0.16 # via -r requirements/dev.in -virtualenv==20.16.7 +virtualenv==20.17.1 # via # pre-commit # tox diff --git a/requirements/docs.txt b/requirements/docs.txt index 7c7e586987..b1e46bde86 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -9,7 +9,7 @@ alabaster==0.7.12 # via sphinx babel==2.11.0 # via sphinx -certifi==2022.9.24 +certifi==2022.12.7 # via requests charset-normalizer==2.1.1 # via requests @@ -25,19 +25,17 @@ jinja2==3.1.2 # via sphinx markupsafe==2.1.1 # via jinja2 -packaging==21.3 +packaging==22.0 # via # pallets-sphinx-themes # sphinx -pallets-sphinx-themes==2.0.2 +pallets-sphinx-themes==2.0.3 # via -r requirements/docs.in pygments==2.13.0 # via # sphinx # sphinx-tabs -pyparsing==3.0.9 - # via packaging -pytz==2022.6 +pytz==2022.7 # via babel requests==2.28.1 # via sphinx diff --git a/requirements/tests.txt b/requirements/tests.txt index b6bc00fc01..aff42de283 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -5,20 +5,18 @@ # # pip-compile-multi # -asgiref==3.5.2 +asgiref==3.6.0 # via -r requirements/tests.in -attrs==22.1.0 +attrs==22.2.0 # via pytest blinker==1.5 # via -r requirements/tests.in iniconfig==1.1.1 # via pytest -packaging==21.3 +packaging==22.0 # via pytest pluggy==1.0.0 # via pytest -pyparsing==3.0.9 - # via packaging pytest==7.2.0 # via -r requirements/tests.in python-dotenv==0.21.0 diff --git a/requirements/typing.txt b/requirements/typing.txt index 491a09fb3c..ad8dd594df 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -7,7 +7,7 @@ # cffi==1.15.1 # via cryptography -cryptography==38.0.3 +cryptography==38.0.4 # via -r requirements/typing.in mypy==0.991 # via -r requirements/typing.in @@ -19,7 +19,7 @@ types-contextvars==2.4.7 # via -r requirements/typing.in types-dataclasses==0.6.6 # via -r requirements/typing.in -types-setuptools==65.6.0.1 +types-setuptools==65.6.0.2 # via -r requirements/typing.in typing-extensions==4.4.0 # via mypy From 8e3128b9893ba4ec8d120e1c2f479f61bd10a476 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 22 Dec 2022 09:30:31 -0800 Subject: [PATCH 143/229] ignore flake8 b905 zip(strict=True) until python 3.10 --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index e858d13a20..ea7f66e20a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -80,6 +80,8 @@ ignore = E722 # bin op line break, invalid W503 + # requires Python 3.10 + B905 # up to 88 allowed by bugbear B950 max-line-length = 80 per-file-ignores = From 1a68768e6b0aa19bd670ca050ad81adff55ba479 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 1 Dec 2022 18:57:08 +0600 Subject: [PATCH 144/229] python 2 style inheritance clean up from docs --- docs/patterns/appdispatch.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst index 0c5e846efd..efa470a78e 100644 --- a/docs/patterns/appdispatch.rst +++ b/docs/patterns/appdispatch.rst @@ -93,7 +93,7 @@ exist yet, it is dynamically created and remembered:: from threading import Lock - class SubdomainDispatcher(object): + class SubdomainDispatcher: def __init__(self, domain, create_app): self.domain = domain @@ -148,7 +148,7 @@ request path up to the first slash:: from threading import Lock from werkzeug.wsgi import pop_path_info, peek_path_info - class PathDispatcher(object): + class PathDispatcher: def __init__(self, default_app, create_app): self.default_app = default_app From 6fcf6d00bd36f7c3c51115199615dc93c4a3007a Mon Sep 17 00:00:00 2001 From: "Maxim G. Ivanov" Date: Thu, 22 Dec 2022 23:23:23 +0700 Subject: [PATCH 145/229] Fix URL "committing as you go" --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d5e3a3f776..8d209048b8 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -171,7 +171,7 @@ Start coding $ git push --set-upstream fork your-branch-name -.. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes +.. _committing as you go: https://afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes .. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request From 677a0468481a0f620bdb217e8f1a24ebe94681e5 Mon Sep 17 00:00:00 2001 From: "Maxim G. Ivanov" Date: Thu, 22 Dec 2022 23:23:23 +0700 Subject: [PATCH 146/229] Fix URL "committing as you go" --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d5e3a3f776..8d209048b8 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -171,7 +171,7 @@ Start coding $ git push --set-upstream fork your-branch-name -.. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes +.. _committing as you go: https://afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes .. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request From 09112cfc477a270f4b2990e1daf39db1dbe30e98 Mon Sep 17 00:00:00 2001 From: "Maxim G. Ivanov" Date: Fri, 23 Dec 2022 00:19:18 +0700 Subject: [PATCH 147/229] template_folder type allows pathlib --- CHANGES.rst | 2 ++ src/flask/app.py | 2 +- src/flask/blueprints.py | 2 +- src/flask/scaffold.py | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 94257a8b3b..fa1a626908 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,8 @@ Version 2.2.3 Unreleased +- Fix the type of ``template_folder`` to accept ``pathlib.Path``. :issue:`4892` + Version 2.2.2 ------------- diff --git a/src/flask/app.py b/src/flask/app.py index fed76e3058..5315b71cb5 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -558,7 +558,7 @@ def __init__( static_host: t.Optional[str] = None, host_matching: bool = False, subdomain_matching: bool = False, - template_folder: t.Optional[str] = "templates", + template_folder: t.Optional[t.Union[str, os.PathLike]] = "templates", instance_path: t.Optional[str] = None, instance_relative_config: bool = False, root_path: t.Optional[str] = None, diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index c2595512db..f6d62ba83f 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -250,7 +250,7 @@ def __init__( import_name: str, static_folder: t.Optional[t.Union[str, os.PathLike]] = None, static_url_path: t.Optional[str] = None, - template_folder: t.Optional[str] = None, + template_folder: t.Optional[t.Union[str, os.PathLike]] = None, url_prefix: t.Optional[str] = None, subdomain: t.Optional[str] = None, url_defaults: t.Optional[dict] = None, diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 1530a11ec8..ebfc741f1a 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -93,7 +93,7 @@ def __init__( import_name: str, static_folder: t.Optional[t.Union[str, os.PathLike]] = None, static_url_path: t.Optional[str] = None, - template_folder: t.Optional[str] = None, + template_folder: t.Optional[t.Union[str, os.PathLike]] = None, root_path: t.Optional[str] = None, ): #: The name of the package or module that this object belongs From 79032ca5f1c4747e5aeaa193bdeb2e4eae410ea6 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Sun, 30 Oct 2022 07:55:51 -0700 Subject: [PATCH 148/229] Add .svg to select_jinja_autoescape (#4840) As SVG files are a type of XML file and are similar in nearly all aspects to XML, .svg should also be autoescaped. --- CHANGES.rst | 3 +++ docs/templating.rst | 2 +- src/flask/app.py | 5 ++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index fa1a626908..82d2da6d8d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,9 @@ Version 2.2.3 Unreleased +- Autoescaping is now enabled by default for ``.svg`` files. Inside + templates this behavior can be changed with the ``autoescape`` tag. + :issue:`4831` - Fix the type of ``template_folder`` to accept ``pathlib.Path``. :issue:`4892` diff --git a/docs/templating.rst b/docs/templating.rst index 3cda995e44..f497de7333 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -18,7 +18,7 @@ Jinja Setup Unless customized, Jinja2 is configured by Flask as follows: - autoescaping is enabled for all templates ending in ``.html``, - ``.htm``, ``.xml`` as well as ``.xhtml`` when using + ``.htm``, ``.xml``, ``.xhtml``, as well as ``.svg`` when using :func:`~flask.templating.render_template`. - autoescaping is enabled for all strings when using :func:`~flask.templating.render_template_string`. diff --git a/src/flask/app.py b/src/flask/app.py index 5315b71cb5..0ac4bbb5ae 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -961,11 +961,14 @@ def select_jinja_autoescape(self, filename: str) -> bool: """Returns ``True`` if autoescaping should be active for the given template name. If no template name is given, returns `True`. + .. versionchanged:: 2.2 + Autoescaping is now enabled by default for ``.svg`` files. + .. versionadded:: 0.5 """ if filename is None: return True - return filename.endswith((".html", ".htm", ".xml", ".xhtml")) + return filename.endswith((".html", ".htm", ".xml", ".xhtml", ".svg")) def update_template_context(self, context: dict) -> None: """Update the template context with some commonly used variables. From 4bc0e4943dfa637361aec2bb18dc9e1fabeaad12 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sun, 21 Aug 2022 17:36:30 +0800 Subject: [PATCH 149/229] Add --debug option to flask run --- CHANGES.rst | 1 + src/flask/cli.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 82d2da6d8d..79e66e956d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,7 @@ Unreleased templates this behavior can be changed with the ``autoescape`` tag. :issue:`4831` - Fix the type of ``template_folder`` to accept ``pathlib.Path``. :issue:`4892` +- Add ``--debug`` option to the ``flask run`` command. :issue:`4777` Version 2.2.2 diff --git a/src/flask/cli.py b/src/flask/cli.py index 82fe8194eb..10e9c1e99c 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -837,6 +837,11 @@ def convert(self, value, param, ctx): expose_value=False, help="The key file to use when specifying a certificate.", ) +@click.option( + "--debug/--no-debug", + default=None, + help="Enable or disable the debug mode.", +) @click.option( "--reload/--no-reload", default=None, @@ -878,6 +883,7 @@ def run_command( info, host, port, + debug, reload, debugger, with_threads, @@ -910,7 +916,8 @@ def app(environ, start_response): # command fails. raise e from None - debug = get_debug_flag() + if debug is None: + debug = get_debug_flag() if reload is None: reload = debug From bd26928fdb2476fca62f0e621e8f2870250ac2bc Mon Sep 17 00:00:00 2001 From: Grey Li Date: Tue, 23 Aug 2022 12:44:50 +0800 Subject: [PATCH 150/229] Prefer flask run --debug in docs --- docs/_static/pycharm-run-config.png | Bin 77462 -> 99654 bytes docs/cli.rst | 4 ++-- docs/config.rst | 6 +++--- docs/debugging.rst | 4 ++-- docs/quickstart.rst | 2 +- docs/server.rst | 2 +- docs/tutorial/factory.rst | 2 +- examples/tutorial/README.rst | 2 +- src/flask/cli.py | 12 ++++-------- 9 files changed, 15 insertions(+), 19 deletions(-) diff --git a/docs/_static/pycharm-run-config.png b/docs/_static/pycharm-run-config.png index 3f78915796e34c54a90008782caa61df47ea248a..ad025545a7ff00c6366a6146d808d875c5eb6a0a 100644 GIT binary patch literal 99654 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 literal 77462 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

YIiEQ5^T7%brmC|dy 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 diff --git a/docs/cli.rst b/docs/cli.rst index 22484f1731..a3ea268eb3 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -95,7 +95,7 @@ the ``--debug`` option. .. code-block:: console - $ flask --app hello --debug run + $ flask --app hello run --debug * Serving Flask app "hello" * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) @@ -550,7 +550,7 @@ a name such as "flask run". Click the *Script path* dropdown and change it to *Module name*, then input ``flask``. The *Parameters* field is set to the CLI command to execute along with any arguments. -This example uses ``--app hello --debug run``, which will run the development server in +This example uses ``--app hello run --debug``, which will run the development server in debug mode. ``--app hello`` should be the import or file with your Flask app. If you installed your project as a package in your virtualenv, you may uncheck the diff --git a/docs/config.rst b/docs/config.rst index aa966e5c0d..9446e4565f 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -47,15 +47,15 @@ Debug Mode The :data:`DEBUG` config value is special because it may behave inconsistently if changed after the app has begun setting up. In order to set debug mode reliably, use the -``--debug`` option on the ``flask`` command. ``flask run`` will use the interactive +``--debug`` option on the ``flask run`` command. ``flask run`` will use the interactive debugger and reloader by default in debug mode. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug Using the option is recommended. While it is possible to set :data:`DEBUG` in your -config or code, this is strongly discouraged. It can't be read early by the ``flask`` +config or code, this is strongly discouraged. It can't be read early by the ``flask run`` command, and some systems or extensions may have already configured themselves based on a previous value. diff --git a/docs/debugging.rst b/docs/debugging.rst index fb3604b036..18f4286758 100644 --- a/docs/debugging.rst +++ b/docs/debugging.rst @@ -43,7 +43,7 @@ The debugger is enabled by default when the development server is run in debug m .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug When running from Python code, passing ``debug=True`` enables debug mode, which is mostly equivalent. @@ -72,7 +72,7 @@ which can interfere. .. code-block:: text - $ flask --app hello --debug run --no-debugger --no-reload + $ flask --app hello run --debug --no-debugger --no-reload When running from Python: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 02dbc97835..ad9e3bc4e8 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -108,7 +108,7 @@ To enable debug mode, use the ``--debug`` option. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug * Serving Flask app 'hello' * Debug mode: on * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) diff --git a/docs/server.rst b/docs/server.rst index a34dfab5dd..d38aa12089 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -24,7 +24,7 @@ debug mode. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug This enables debug mode, including the interactive debugger and reloader, and then starts the server on http://localhost:5000/. Use ``flask run --help`` to see the diff --git a/docs/tutorial/factory.rst b/docs/tutorial/factory.rst index c8e2c5f4e0..39febd135f 100644 --- a/docs/tutorial/factory.rst +++ b/docs/tutorial/factory.rst @@ -137,7 +137,7 @@ follow the tutorial. .. code-block:: text - $ flask --app flaskr --debug run + $ flask --app flaskr run --debug You'll see output similar to this: diff --git a/examples/tutorial/README.rst b/examples/tutorial/README.rst index a7e12ca250..1c745078bc 100644 --- a/examples/tutorial/README.rst +++ b/examples/tutorial/README.rst @@ -48,7 +48,7 @@ Run .. code-block:: text $ flask --app flaskr init-db - $ flask --app flaskr --debug run + $ flask --app flaskr run --debug Open http://127.0.0.1:5000 in a browser. diff --git a/src/flask/cli.py b/src/flask/cli.py index 10e9c1e99c..37a15ff2d8 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -837,11 +837,6 @@ def convert(self, value, param, ctx): expose_value=False, help="The key file to use when specifying a certificate.", ) -@click.option( - "--debug/--no-debug", - default=None, - help="Enable or disable the debug mode.", -) @click.option( "--reload/--no-reload", default=None, @@ -883,7 +878,6 @@ def run_command( info, host, port, - debug, reload, debugger, with_threads, @@ -916,8 +910,7 @@ def app(environ, start_response): # command fails. raise e from None - if debug is None: - debug = get_debug_flag() + debug = get_debug_flag() if reload is None: reload = debug @@ -940,6 +933,9 @@ def app(environ, start_response): ) +run_command.params.insert(0, _debug_option) + + @click.command("shell", short_help="Run a shell in the app context.") @with_appcontext def shell_command() -> None: From 4d69165ab6e17fa754139d348cdfd9edacbcb999 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 29 Dec 2022 09:51:34 -0800 Subject: [PATCH 151/229] revert run debug docs --- docs/cli.rst | 4 ++-- docs/config.rst | 4 ++-- docs/debugging.rst | 4 ++-- docs/quickstart.rst | 2 +- docs/server.rst | 2 +- docs/tutorial/factory.rst | 2 +- examples/tutorial/README.rst | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index a3ea268eb3..22484f1731 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -95,7 +95,7 @@ the ``--debug`` option. .. code-block:: console - $ flask --app hello run --debug + $ flask --app hello --debug run * Serving Flask app "hello" * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) @@ -550,7 +550,7 @@ a name such as "flask run". Click the *Script path* dropdown and change it to *Module name*, then input ``flask``. The *Parameters* field is set to the CLI command to execute along with any arguments. -This example uses ``--app hello run --debug``, which will run the development server in +This example uses ``--app hello --debug run``, which will run the development server in debug mode. ``--app hello`` should be the import or file with your Flask app. If you installed your project as a package in your virtualenv, you may uncheck the diff --git a/docs/config.rst b/docs/config.rst index 9446e4565f..7cffc44bb2 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -47,12 +47,12 @@ Debug Mode The :data:`DEBUG` config value is special because it may behave inconsistently if changed after the app has begun setting up. In order to set debug mode reliably, use the -``--debug`` option on the ``flask run`` command. ``flask run`` will use the interactive +``--debug`` option on the ``flask`` command. ``flask run`` will use the interactive debugger and reloader by default in debug mode. .. code-block:: text - $ flask --app hello run --debug + $ flask --app hello --debug run Using the option is recommended. While it is possible to set :data:`DEBUG` in your config or code, this is strongly discouraged. It can't be read early by the ``flask run`` diff --git a/docs/debugging.rst b/docs/debugging.rst index 18f4286758..fb3604b036 100644 --- a/docs/debugging.rst +++ b/docs/debugging.rst @@ -43,7 +43,7 @@ The debugger is enabled by default when the development server is run in debug m .. code-block:: text - $ flask --app hello run --debug + $ flask --app hello --debug run When running from Python code, passing ``debug=True`` enables debug mode, which is mostly equivalent. @@ -72,7 +72,7 @@ which can interfere. .. code-block:: text - $ flask --app hello run --debug --no-debugger --no-reload + $ flask --app hello --debug run --no-debugger --no-reload When running from Python: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index ad9e3bc4e8..02dbc97835 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -108,7 +108,7 @@ To enable debug mode, use the ``--debug`` option. .. code-block:: text - $ flask --app hello run --debug + $ flask --app hello --debug run * Serving Flask app 'hello' * Debug mode: on * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) diff --git a/docs/server.rst b/docs/server.rst index d38aa12089..a34dfab5dd 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -24,7 +24,7 @@ debug mode. .. code-block:: text - $ flask --app hello run --debug + $ flask --app hello --debug run This enables debug mode, including the interactive debugger and reloader, and then starts the server on http://localhost:5000/. Use ``flask run --help`` to see the diff --git a/docs/tutorial/factory.rst b/docs/tutorial/factory.rst index 39febd135f..c8e2c5f4e0 100644 --- a/docs/tutorial/factory.rst +++ b/docs/tutorial/factory.rst @@ -137,7 +137,7 @@ follow the tutorial. .. code-block:: text - $ flask --app flaskr run --debug + $ flask --app flaskr --debug run You'll see output similar to this: diff --git a/examples/tutorial/README.rst b/examples/tutorial/README.rst index 1c745078bc..a7e12ca250 100644 --- a/examples/tutorial/README.rst +++ b/examples/tutorial/README.rst @@ -48,7 +48,7 @@ Run .. code-block:: text $ flask --app flaskr init-db - $ flask --app flaskr run --debug + $ flask --app flaskr --debug run Open http://127.0.0.1:5000 in a browser. From 74e5263c88e51bb442879ab863a5d03292fc0a33 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 29 Dec 2022 09:52:18 -0800 Subject: [PATCH 152/229] new run debug docs --- docs/cli.rst | 4 ++-- docs/config.rst | 4 ++-- docs/debugging.rst | 4 ++-- docs/quickstart.rst | 2 +- docs/server.rst | 2 +- docs/tutorial/factory.rst | 2 +- examples/tutorial/README.rst | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index 22484f1731..a3ea268eb3 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -95,7 +95,7 @@ the ``--debug`` option. .. code-block:: console - $ flask --app hello --debug run + $ flask --app hello run --debug * Serving Flask app "hello" * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) @@ -550,7 +550,7 @@ a name such as "flask run". Click the *Script path* dropdown and change it to *Module name*, then input ``flask``. The *Parameters* field is set to the CLI command to execute along with any arguments. -This example uses ``--app hello --debug run``, which will run the development server in +This example uses ``--app hello run --debug``, which will run the development server in debug mode. ``--app hello`` should be the import or file with your Flask app. If you installed your project as a package in your virtualenv, you may uncheck the diff --git a/docs/config.rst b/docs/config.rst index 7cffc44bb2..9446e4565f 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -47,12 +47,12 @@ Debug Mode The :data:`DEBUG` config value is special because it may behave inconsistently if changed after the app has begun setting up. In order to set debug mode reliably, use the -``--debug`` option on the ``flask`` command. ``flask run`` will use the interactive +``--debug`` option on the ``flask run`` command. ``flask run`` will use the interactive debugger and reloader by default in debug mode. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug Using the option is recommended. While it is possible to set :data:`DEBUG` in your config or code, this is strongly discouraged. It can't be read early by the ``flask run`` diff --git a/docs/debugging.rst b/docs/debugging.rst index fb3604b036..18f4286758 100644 --- a/docs/debugging.rst +++ b/docs/debugging.rst @@ -43,7 +43,7 @@ The debugger is enabled by default when the development server is run in debug m .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug When running from Python code, passing ``debug=True`` enables debug mode, which is mostly equivalent. @@ -72,7 +72,7 @@ which can interfere. .. code-block:: text - $ flask --app hello --debug run --no-debugger --no-reload + $ flask --app hello run --debug --no-debugger --no-reload When running from Python: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f92bd24100..0d7ad3f695 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -108,7 +108,7 @@ To enable debug mode, use the ``--debug`` option. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug * Serving Flask app 'hello' * Debug mode: on * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) diff --git a/docs/server.rst b/docs/server.rst index a34dfab5dd..d38aa12089 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -24,7 +24,7 @@ debug mode. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug This enables debug mode, including the interactive debugger and reloader, and then starts the server on http://localhost:5000/. Use ``flask run --help`` to see the diff --git a/docs/tutorial/factory.rst b/docs/tutorial/factory.rst index c8e2c5f4e0..39febd135f 100644 --- a/docs/tutorial/factory.rst +++ b/docs/tutorial/factory.rst @@ -137,7 +137,7 @@ follow the tutorial. .. code-block:: text - $ flask --app flaskr --debug run + $ flask --app flaskr run --debug You'll see output similar to this: diff --git a/examples/tutorial/README.rst b/examples/tutorial/README.rst index a7e12ca250..1c745078bc 100644 --- a/examples/tutorial/README.rst +++ b/examples/tutorial/README.rst @@ -48,7 +48,7 @@ Run .. code-block:: text $ flask --app flaskr init-db - $ flask --app flaskr --debug run + $ flask --app flaskr run --debug Open http://127.0.0.1:5000 in a browser. From bb1f83c26586f1aa254d67a67e6b692034b4cb4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jan 2023 16:01:00 +0000 Subject: [PATCH 153/229] Bump dessant/lock-threads from 3 to 4 Bumps [dessant/lock-threads](https://github.com/dessant/lock-threads) from 3 to 4. - [Release notes](https://github.com/dessant/lock-threads/releases) - [Changelog](https://github.com/dessant/lock-threads/blob/master/CHANGELOG.md) - [Commits](https://github.com/dessant/lock-threads/compare/v3...v4) --- updated-dependencies: - dependency-name: dessant/lock-threads dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml index cd89f67c9d..a84a3a7370 100644 --- a/.github/workflows/lock.yaml +++ b/.github/workflows/lock.yaml @@ -11,7 +11,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v3 + - uses: dessant/lock-threads@v4 with: github-token: ${{ github.token }} issue-inactive-days: 14 From d7b6c1f6703df405c69da45e7e0ba3d1aed512ce Mon Sep 17 00:00:00 2001 From: Josh Michael Karamuth Date: Mon, 31 Oct 2022 12:49:16 +0400 Subject: [PATCH 154/229] Fix subdomain inheritance for nested blueprints. Fixes #4834 --- src/flask/blueprints.py | 9 ++++++++ tests/test_blueprints.py | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index f6d62ba83f..1278fb8ce5 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -453,6 +453,15 @@ def extend(bp_dict, parent_dict): for blueprint, bp_options in self._blueprints: bp_options = bp_options.copy() bp_url_prefix = bp_options.get("url_prefix") + bp_subdomain = bp_options.get("subdomain") + + if bp_subdomain is None: + bp_subdomain = blueprint.subdomain + + if state.subdomain is not None and bp_subdomain is None: + bp_options["subdomain"] = state.subdomain + elif bp_subdomain is not None: + bp_options["subdomain"] = bp_subdomain if bp_url_prefix is None: bp_url_prefix = blueprint.url_prefix diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 1bf130f52b..a83ae243e7 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -950,6 +950,53 @@ def index(): assert response.status_code == 200 +def test_nesting_subdomains(app, client) -> None: + subdomain = "api" + parent = flask.Blueprint("parent", __name__) + child = flask.Blueprint("child", __name__) + + @child.route("/child/") + def index(): + return "child" + + parent.register_blueprint(child) + app.register_blueprint(parent, subdomain=subdomain) + + client.allow_subdomain_redirects = True + + domain_name = "domain.tld" + app.config["SERVER_NAME"] = domain_name + response = client.get("/child/", base_url="http://api." + domain_name) + + assert response.status_code == 200 + + +def test_child_overrides_parent_subdomain(app, client) -> None: + child_subdomain = "api" + parent_subdomain = "parent" + parent = flask.Blueprint("parent", __name__) + child = flask.Blueprint("child", __name__, subdomain=child_subdomain) + + @child.route("/") + def index(): + return "child" + + parent.register_blueprint(child) + app.register_blueprint(parent, subdomain=parent_subdomain) + + client.allow_subdomain_redirects = True + + domain_name = "domain.tld" + app.config["SERVER_NAME"] = domain_name + response = client.get("/", base_url=f"http://{child_subdomain}.{domain_name}") + + assert response.status_code == 200 + + response = client.get("/", base_url=f"http://{parent_subdomain}.{domain_name}") + + assert response.status_code == 404 + + def test_unique_blueprint_names(app, client) -> None: bp = flask.Blueprint("bp", __name__) bp2 = flask.Blueprint("bp", __name__) From cabda5935322d75e7aedb3ee6d59fb7ab62bd674 Mon Sep 17 00:00:00 2001 From: pgjones Date: Wed, 4 Jan 2023 16:45:20 +0000 Subject: [PATCH 155/229] Ensure that blueprint subdomains suffix-chain This ensures that a child's subdomain prefixs any parent subdomain such that the full domain is child.parent.domain.tld and onwards with further nesting. This makes the most sense to users and mimics how url_prefixes work (although subdomains suffix). --- CHANGES.rst | 2 ++ docs/blueprints.rst | 13 +++++++++++++ src/flask/blueprints.py | 9 +++++++-- tests/test_blueprints.py | 6 ++++-- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index adf2623d59..bcec74a76b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,8 @@ Version 2.3.0 Unreleased +- Ensure subdomains are applied with nested blueprints. :issue:`4834` + Version 2.2.3 ------------- diff --git a/docs/blueprints.rst b/docs/blueprints.rst index af368bacf1..d5cf3d8237 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -140,6 +140,19 @@ name, and child URLs will be prefixed with the parent's URL prefix. url_for('parent.child.create') /parent/child/create +In addition a child blueprint's will gain their parent's subdomain, +with their subdomain as prefix if present i.e. + +.. code-block:: python + + parent = Blueprint('parent', __name__, subdomain='parent') + child = Blueprint('child', __name__, subdomain='child') + parent.register_blueprint(child) + app.register_blueprint(parent) + + url_for('parent.child.create', _external=True) + "child.parent.domain.tld" + Blueprint-specific before request functions, etc. registered with the parent will trigger for the child. If a child does not have an error handler that can handle a given exception, the parent's will be tried. diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 1278fb8ce5..2403be1cf8 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -358,6 +358,9 @@ def register(self, app: "Flask", options: dict) -> None: :param options: Keyword arguments forwarded from :meth:`~Flask.register_blueprint`. + .. versionchanged:: 2.3 + Nested blueprints now correctly apply subdomains. + .. versionchanged:: 2.0.1 Nested blueprints are registered with their dotted name. This allows different blueprints with the same name to be @@ -458,10 +461,12 @@ def extend(bp_dict, parent_dict): if bp_subdomain is None: bp_subdomain = blueprint.subdomain - if state.subdomain is not None and bp_subdomain is None: - bp_options["subdomain"] = state.subdomain + if state.subdomain is not None and bp_subdomain is not None: + bp_options["subdomain"] = bp_subdomain + "." + state.subdomain elif bp_subdomain is not None: bp_options["subdomain"] = bp_subdomain + elif state.subdomain is not None: + bp_options["subdomain"] = state.subdomain if bp_url_prefix is None: bp_url_prefix = blueprint.url_prefix diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index a83ae243e7..dbe00b974c 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -971,7 +971,7 @@ def index(): assert response.status_code == 200 -def test_child_overrides_parent_subdomain(app, client) -> None: +def test_child_and_parent_subdomain(app, client) -> None: child_subdomain = "api" parent_subdomain = "parent" parent = flask.Blueprint("parent", __name__) @@ -988,7 +988,9 @@ def index(): domain_name = "domain.tld" app.config["SERVER_NAME"] = domain_name - response = client.get("/", base_url=f"http://{child_subdomain}.{domain_name}") + response = client.get( + "/", base_url=f"http://{child_subdomain}.{parent_subdomain}.{domain_name}" + ) assert response.status_code == 200 From 2a9d16d01189249749324141f0c294b31bf7ba6b Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 9 Jan 2023 10:37:04 -0800 Subject: [PATCH 156/229] update tested python versions test 3.11 final test 3.12 dev update for tox 4 --- .github/workflows/tests.yaml | 23 +++++++++++++++-------- requirements/tests-pallets-dev.in | 5 ----- requirements/tests-pallets-dev.txt | 20 -------------------- tox.ini | 20 +++++++++++++++----- 4 files changed, 30 insertions(+), 38 deletions(-) delete mode 100644 requirements/tests-pallets-dev.in delete mode 100644 requirements/tests-pallets-dev.txt diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 674fb8b73b..89e548dbad 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -24,16 +24,17 @@ jobs: fail-fast: false matrix: include: - - {name: Linux, python: '3.10', os: ubuntu-latest, tox: py310} - - {name: Windows, python: '3.10', os: windows-latest, tox: py310} - - {name: Mac, python: '3.10', os: macos-latest, tox: py310} - - {name: '3.11-dev', python: '3.11-dev', os: ubuntu-latest, tox: py311} + - {name: Linux, python: '3.11', os: ubuntu-latest, tox: py311} + - {name: Windows, python: '3.11', os: windows-latest, tox: py311} + - {name: Mac, python: '3.11', os: macos-latest, tox: py311} + - {name: '3.12-dev', python: '3.12-dev', os: ubuntu-latest, tox: py312} + - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310} - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39} - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38} - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} - - {name: 'PyPy', python: 'pypy-3.7', os: ubuntu-latest, tox: pypy37} - - {name: 'Pallets Minimum Versions', python: '3.10', os: ubuntu-latest, tox: py-min} - - {name: 'Pallets Development Versions', python: '3.7', os: ubuntu-latest, tox: py-dev} + - {name: 'PyPy', python: 'pypy-3.9', os: ubuntu-latest, tox: pypy39} + - {name: 'Pallets Minimum Versions', python: '3.11', os: ubuntu-latest, tox: py311-min} + - {name: 'Pallets Development Versions', python: '3.7', os: ubuntu-latest, tox: py37-dev} - {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing} steps: - uses: actions/checkout@v3 @@ -47,5 +48,11 @@ jobs: pip install -U wheel pip install -U setuptools python -m pip install -U pip + - name: cache mypy + uses: actions/cache@v3.2.2 + with: + path: ./.mypy_cache + key: mypy|${{ matrix.python }}|${{ hashFiles('setup.cfg') }} + if: matrix.tox == 'typing' - run: pip install tox - - run: tox -e ${{ matrix.tox }} + - run: tox run -e ${{ matrix.tox }} diff --git a/requirements/tests-pallets-dev.in b/requirements/tests-pallets-dev.in deleted file mode 100644 index dddbe48a41..0000000000 --- a/requirements/tests-pallets-dev.in +++ /dev/null @@ -1,5 +0,0 @@ -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 diff --git a/requirements/tests-pallets-dev.txt b/requirements/tests-pallets-dev.txt deleted file mode 100644 index a74f556b17..0000000000 --- a/requirements/tests-pallets-dev.txt +++ /dev/null @@ -1,20 +0,0 @@ -# SHA1:692b640e7f835e536628f76de0afff1296524122 -# -# This file is autogenerated by pip-compile-multi -# To update, run: -# -# pip-compile-multi -# -click @ https://github.com/pallets/click/archive/refs/heads/main.tar.gz - # via -r requirements/tests-pallets-dev.in -itsdangerous @ https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz - # via -r requirements/tests-pallets-dev.in -jinja2 @ https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz - # via -r requirements/tests-pallets-dev.in -markupsafe @ https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz - # via - # -r requirements/tests-pallets-dev.in - # jinja2 - # werkzeug -werkzeug @ https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz - # via -r requirements/tests-pallets-dev.in diff --git a/tox.ini b/tox.ini index ee4d40f689..08c6dca2f5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,8 @@ [tox] envlist = - py3{11,10,9,8,7},pypy3{8,7} - py310-min + py3{12,11,10,9,8,7} + pypy3{9,8,7} + py311-min py37-dev style typing @@ -9,12 +10,17 @@ envlist = skip_missing_interpreters = true [testenv] +package = wheel +wheel_build_env = .pkg envtmpdir = {toxworkdir}/tmp/{envname} deps = -r requirements/tests.txt min: -r requirements/tests-pallets-min.txt - dev: -r requirements/tests-pallets-dev.txt - + dev: https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz + dev: https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz + dev: https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz + dev: https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz + dev: https://github.com/pallets/click/archive/refs/heads/main.tar.gz # examples/tutorial[test] # examples/javascript[test] # commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs:tests examples} @@ -23,12 +29,16 @@ commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs:tests} [testenv:style] deps = pre-commit skip_install = true -commands = pre-commit run --all-files --show-diff-on-failure +commands = pre-commit run --all-files [testenv:typing] +package = wheel +wheel_build_env = .pkg deps = -r requirements/typing.txt commands = mypy [testenv:docs] +package = wheel +wheel_build_env = .pkg deps = -r requirements/docs.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html From a7487701999fee55bb76a393e72c96ac8e277fcf Mon Sep 17 00:00:00 2001 From: Bhushan Mohanraj <50306448+bhushan-mohanraj@users.noreply.github.com> Date: Fri, 6 Jan 2023 22:14:03 -0500 Subject: [PATCH 157/229] clarify `View.as_view` docstring --- src/flask/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/flask/views.py b/src/flask/views.py index a82f191238..f86172b43c 100644 --- a/src/flask/views.py +++ b/src/flask/views.py @@ -92,8 +92,8 @@ def as_view( :attr:`init_every_request` to ``False``, the same instance will be used for every request. - The arguments passed to this method are forwarded to the view - class ``__init__`` method. + Except for ``name``, all other arguments passed to this method + are forwarded to the view class ``__init__`` method. .. versionchanged:: 2.2 Added the ``init_every_request`` class attribute. From 9da947a279d5ed5958609e0b6ec690160c496480 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 9 Jan 2023 12:45:16 -0800 Subject: [PATCH 158/229] set workflow permissions --- .github/workflows/lock.yaml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml index a84a3a7370..20bec85a7c 100644 --- a/.github/workflows/lock.yaml +++ b/.github/workflows/lock.yaml @@ -1,18 +1,25 @@ -# This does not automatically close "stale" issues. Instead, it locks closed issues after 2 weeks of no activity. -# If there's a new issue related to an old one, we've found it's much easier to work on as a new issue. - name: 'Lock threads' +# Lock closed issues that have not received any further activity for +# two weeks. This does not close open issues, only humans may do that. +# We find that it is easier to respond to new issues with fresh examples +# rather than continuing discussions on old issues. on: schedule: - cron: '0 0 * * *' +permissions: + issues: write + pull-requests: write + +concurrency: + group: lock + jobs: lock: runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@v4 with: - github-token: ${{ github.token }} issue-inactive-days: 14 pr-inactive-days: 14 From 6d6d986fc502c2c24fe3db0ec0dbb062d053e068 Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 18 Jan 2023 09:50:41 -0800 Subject: [PATCH 159/229] switch to pyproject.toml --- .flake8 | 26 ++++++++ .github/workflows/tests.yaml | 2 +- CHANGES.rst | 2 + pyproject.toml | 94 ++++++++++++++++++++++++++ setup.cfg | 123 ----------------------------------- setup.py | 17 ----- 6 files changed, 123 insertions(+), 141 deletions(-) create mode 100644 .flake8 create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..0980961647 --- /dev/null +++ b/.flake8 @@ -0,0 +1,26 @@ +[flake8] +# B = bugbear +# E = pycodestyle errors +# F = flake8 pyflakes +# W = pycodestyle warnings +# B9 = bugbear opinions +# ISC = implicit str concat +select = B, E, F, W, B9, ISC +ignore = + # slice notation whitespace, invalid + E203 + # import at top, too many circular import fixes + E402 + # line length, handled by bugbear B950 + E501 + # bare except, handled by bugbear B001 + E722 + # bin op line break, invalid + W503 + # requires Python 3.10 + B905 +# up to 88 allowed by bugbear B950 +max-line-length = 80 +per-file-ignores = + # __init__ exports names + src/flask/__init__.py: F401 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 89e548dbad..a00c535508 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -52,7 +52,7 @@ jobs: uses: actions/cache@v3.2.2 with: path: ./.mypy_cache - key: mypy|${{ matrix.python }}|${{ hashFiles('setup.cfg') }} + key: mypy|${{ matrix.python }}|${{ hashFiles('pyproject.toml') }} if: matrix.tox == 'typing' - run: pip install tox - run: tox run -e ${{ matrix.tox }} diff --git a/CHANGES.rst b/CHANGES.rst index bcec74a76b..94c16a3486 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,8 @@ Version 2.3.0 Unreleased +- Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``. + :pr:`4947` - Ensure subdomains are applied with nested blueprints. :issue:`4834` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..95a6e10095 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,94 @@ +[project] +name = "Flask" +description = "A simple framework for building complex web applications." +readme = "README.rst" +license = {text = "BSD-3-Clause"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +authors = [{name = "Armin Ronacher", email = "armin.ronacher@active-4.com"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Flask", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Internet :: WWW/HTTP :: WSGI", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Topic :: Software Development :: Libraries :: Application Frameworks", +] +requires-python = ">=3.7" +dependencies = [ + "Werkzeug>=2.2.2", + "Jinja2>=3.0", + "itsdangerous>=2.0", + "click>=8.0", + "importlib-metadata>=3.6.0; python_version < '3.10'", +] +dynamic = ["version"] + +[project.urls] +Donate = "https://palletsprojects.com/donate" +Documentation = "https://flask.palletsprojects.com/" +Changes = "https://flask.palletsprojects.com/changes/" +"Source Code" = "https://github.com/pallets/flask/" +"Issue Tracker" = "https://github.com/pallets/flask/issues/" +Twitter = "https://twitter.com/PalletsTeam" +Chat = "https://discord.gg/pallets" + +[project.optional-dependencies] +async = ["asgiref>=3.2"] +dotenv = ["python-dotenv"] + +[project.scripts] +flask = "flask.cli:main" + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.dynamic] +version = {attr = "flask.__version__"} + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = ["error"] + +[tool.coverage.run] +branch = true +source = ["flask", "tests"] + +[tool.coverage.paths] +source = ["src", "*/site-packages"] + +[tool.mypy] +python_version = "3.7" +files = ["src/flask"] +show_error_codes = true +pretty = true +#strict = true +allow_redefinition = true +disallow_subclassing_any = true +#disallow_untyped_calls = true +#disallow_untyped_defs = true +#disallow_incomplete_defs = true +no_implicit_optional = true +local_partial_types = true +#no_implicit_reexport = true +strict_equality = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unused_ignores = true +#warn_return_any = true +#warn_unreachable = true + +[[tool.mypy.overrides]] +module = [ + "asgiref.*", + "blinker.*", + "dotenv.*", + "cryptography.*", + "importlib_metadata", +] +ignore_missing_imports = true diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ea7f66e20a..0000000000 --- a/setup.cfg +++ /dev/null @@ -1,123 +0,0 @@ -[metadata] -name = Flask -version = attr: flask.__version__ -url = https://palletsprojects.com/p/flask -project_urls = - Donate = https://palletsprojects.com/donate - Documentation = https://flask.palletsprojects.com/ - Changes = https://flask.palletsprojects.com/changes/ - Source Code = https://github.com/pallets/flask/ - Issue Tracker = https://github.com/pallets/flask/issues/ - Twitter = https://twitter.com/PalletsTeam - Chat = https://discord.gg/pallets -license = BSD-3-Clause -author = Armin Ronacher -author_email = armin.ronacher@active-4.com -maintainer = Pallets -maintainer_email = contact@palletsprojects.com -description = A simple framework for building complex web applications. -long_description = file: README.rst -long_description_content_type = text/x-rst -classifiers = - Development Status :: 5 - Production/Stable - Environment :: Web Environment - Framework :: Flask - Intended Audience :: Developers - License :: OSI Approved :: BSD License - Operating System :: OS Independent - Programming Language :: Python - Topic :: Internet :: WWW/HTTP :: Dynamic Content - Topic :: Internet :: WWW/HTTP :: WSGI - Topic :: Internet :: WWW/HTTP :: WSGI :: Application - Topic :: Software Development :: Libraries :: Application Frameworks - -[options] -packages = find: -package_dir = = src -include_package_data = True -python_requires = >= 3.7 -# Dependencies are in setup.py for GitHub's dependency graph. - -[options.packages.find] -where = src - -[options.entry_points] -console_scripts = - flask = flask.cli:main - -[tool:pytest] -testpaths = tests -filterwarnings = - error - -[coverage:run] -branch = True -source = - flask - tests - -[coverage:paths] -source = - src - */site-packages - -[flake8] -# B = bugbear -# E = pycodestyle errors -# F = flake8 pyflakes -# W = pycodestyle warnings -# B9 = bugbear opinions -# ISC = implicit str concat -select = B, E, F, W, B9, ISC -ignore = - # slice notation whitespace, invalid - E203 - # import at top, too many circular import fixes - E402 - # line length, handled by bugbear B950 - E501 - # bare except, handled by bugbear B001 - E722 - # bin op line break, invalid - W503 - # requires Python 3.10 - B905 -# up to 88 allowed by bugbear B950 -max-line-length = 80 -per-file-ignores = - # __init__ exports names - src/flask/__init__.py: F401 - -[mypy] -files = src/flask, tests/typing -python_version = 3.7 -show_error_codes = True -allow_redefinition = True -disallow_subclassing_any = True -# disallow_untyped_calls = True -# disallow_untyped_defs = True -# disallow_incomplete_defs = True -no_implicit_optional = True -local_partial_types = True -# no_implicit_reexport = True -strict_equality = True -warn_redundant_casts = True -warn_unused_configs = True -warn_unused_ignores = True -# warn_return_any = True -# warn_unreachable = True - -[mypy-asgiref.*] -ignore_missing_imports = True - -[mypy-blinker.*] -ignore_missing_imports = True - -[mypy-dotenv.*] -ignore_missing_imports = True - -[mypy-cryptography.*] -ignore_missing_imports = True - -[mypy-importlib_metadata] -ignore_missing_imports = True diff --git a/setup.py b/setup.py deleted file mode 100644 index 6717546774..0000000000 --- a/setup.py +++ /dev/null @@ -1,17 +0,0 @@ -from setuptools import setup - -# Metadata goes in setup.cfg. These are here for GitHub's dependency graph. -setup( - name="Flask", - install_requires=[ - "Werkzeug >= 2.2.2", - "Jinja2 >= 3.0", - "itsdangerous >= 2.0", - "click >= 8.0", - "importlib-metadata >= 3.6.0; python_version < '3.10'", - ], - extras_require={ - "async": ["asgiref >= 3.2"], - "dotenv": ["python-dotenv"], - }, -) From 8f13f5b6d672f1ba434f9c8ebe2c1b1dd385962e Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 18 Jan 2023 10:21:37 -0800 Subject: [PATCH 160/229] update docs and examples for pyproject setup.py -> pyproject.toml venv -> .venv --- CONTRIBUTING.rst | 6 ++-- docs/cli.rst | 41 +++++++--------------- docs/deploying/eventlet.rst | 4 +-- docs/deploying/gevent.rst | 4 +-- docs/deploying/gunicorn.rst | 4 +-- docs/deploying/mod_wsgi.rst | 6 ++-- docs/deploying/uwsgi.rst | 4 +-- docs/deploying/waitress.rst | 4 +-- docs/installation.rst | 10 +++--- docs/patterns/packages.rst | 25 ++++++------- docs/tutorial/deploy.rst | 21 ++++------- docs/tutorial/install.rst | 56 +++++++++++++----------------- docs/tutorial/layout.rst | 8 ++--- docs/tutorial/tests.rst | 22 ++++++------ examples/javascript/.gitignore | 2 +- examples/javascript/README.rst | 4 +-- examples/javascript/pyproject.toml | 26 ++++++++++++++ examples/javascript/setup.cfg | 29 ---------------- examples/javascript/setup.py | 3 -- examples/tutorial/.gitignore | 2 +- examples/tutorial/README.rst | 8 ++--- examples/tutorial/pyproject.toml | 28 +++++++++++++++ examples/tutorial/setup.cfg | 28 --------------- examples/tutorial/setup.py | 3 -- 24 files changed, 153 insertions(+), 195 deletions(-) create mode 100644 examples/javascript/pyproject.toml delete mode 100644 examples/javascript/setup.cfg delete mode 100644 examples/javascript/setup.py create mode 100644 examples/tutorial/pyproject.toml delete mode 100644 examples/tutorial/setup.cfg delete mode 100644 examples/tutorial/setup.py diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8d209048b8..8962490fe5 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -102,14 +102,14 @@ First time setup .. code-block:: text - $ python3 -m venv env - $ . env/bin/activate + $ python3 -m venv .venv + $ . .venv/bin/activate - Windows .. code-block:: text - > py -3 -m venv env + > py -3 -m venv .venv > env\Scripts\activate - Upgrade pip and setuptools. diff --git a/docs/cli.rst b/docs/cli.rst index a3ea268eb3..ec2ea22d77 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -280,25 +280,25 @@ script. Activating the virtualenv will set the variables. .. group-tab:: Bash - Unix Bash, :file:`venv/bin/activate`:: + Unix Bash, :file:`.venv/bin/activate`:: $ export FLASK_APP=hello .. group-tab:: Fish - Fish, :file:`venv/bin/activate.fish`:: + Fish, :file:`.venv/bin/activate.fish`:: $ set -x FLASK_APP hello .. group-tab:: CMD - Windows CMD, :file:`venv\\Scripts\\activate.bat`:: + Windows CMD, :file:`.venv\\Scripts\\activate.bat`:: > set FLASK_APP=hello .. group-tab:: Powershell - Windows Powershell, :file:`venv\\Scripts\\activate.ps1`:: + Windows Powershell, :file:`.venv\\Scripts\\activate.ps1`:: > $env:FLASK_APP = "hello" @@ -438,24 +438,16 @@ Plugins Flask will automatically load commands specified in the ``flask.commands`` `entry point`_. This is useful for extensions that want to add commands when -they are installed. Entry points are specified in :file:`setup.py` :: +they are installed. Entry points are specified in :file:`pyproject.toml`: - from setuptools import setup - - setup( - name='flask-my-extension', - ..., - entry_points={ - 'flask.commands': [ - 'my-command=flask_my_extension.commands:cli' - ], - }, - ) +.. code-block:: toml + [project.entry-points."flask.commands"] + my-command = "my_extension.commands:cli" .. _entry point: https://packaging.python.org/tutorials/packaging-projects/#entry-points -Inside :file:`flask_my_extension/commands.py` you can then export a Click +Inside :file:`my_extension/commands.py` you can then export a Click object:: import click @@ -493,19 +485,12 @@ Create an instance of :class:`~cli.FlaskGroup` and pass it the factory:: def cli(): """Management script for the Wiki application.""" -Define the entry point in :file:`setup.py`:: +Define the entry point in :file:`pyproject.toml`: - from setuptools import setup +.. code-block:: toml - setup( - name='flask-my-extension', - ..., - entry_points={ - 'console_scripts': [ - 'wiki=wiki:cli' - ], - }, - ) + [project.scripts] + wiki = "wiki:cli" Install the application in the virtualenv in editable mode and the custom script is available. Note that you don't need to set ``--app``. :: diff --git a/docs/deploying/eventlet.rst b/docs/deploying/eventlet.rst index 243be5ebb3..3653c01ea8 100644 --- a/docs/deploying/eventlet.rst +++ b/docs/deploying/eventlet.rst @@ -34,8 +34,8 @@ Create a virtualenv, install your application, then install .. code-block:: text $ cd hello-app - $ python -m venv venv - $ . venv/bin/activate + $ python -m venv .venv + $ . .venv/bin/activate $ pip install . # install your application $ pip install eventlet diff --git a/docs/deploying/gevent.rst b/docs/deploying/gevent.rst index aae63e89e8..448b93e78c 100644 --- a/docs/deploying/gevent.rst +++ b/docs/deploying/gevent.rst @@ -33,8 +33,8 @@ Create a virtualenv, install your application, then install ``gevent``. .. code-block:: text $ cd hello-app - $ python -m venv venv - $ . venv/bin/activate + $ python -m venv .venv + $ . .venv/bin/activate $ pip install . # install your application $ pip install gevent diff --git a/docs/deploying/gunicorn.rst b/docs/deploying/gunicorn.rst index 93d11d3964..c50edc2326 100644 --- a/docs/deploying/gunicorn.rst +++ b/docs/deploying/gunicorn.rst @@ -30,8 +30,8 @@ Create a virtualenv, install your application, then install .. code-block:: text $ cd hello-app - $ python -m venv venv - $ . venv/bin/activate + $ python -m venv .venv + $ . .venv/bin/activate $ pip install . # install your application $ pip install gunicorn diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index eae973deb6..23e8227989 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -33,8 +33,8 @@ Create a virtualenv, install your application, then install .. code-block:: text $ cd hello-app - $ python -m venv venv - $ . venv/bin/activate + $ python -m venv .venv + $ . .venv/bin/activate $ pip install . # install your application $ pip install mod_wsgi @@ -89,6 +89,6 @@ mod_wsgi to drop to that user after starting. .. code-block:: text - $ sudo /home/hello/venv/bin/mod_wsgi-express start-server \ + $ sudo /home/hello/.venv/bin/mod_wsgi-express start-server \ /home/hello/wsgi.py \ --user hello --group hello --port 80 --processes 4 diff --git a/docs/deploying/uwsgi.rst b/docs/deploying/uwsgi.rst index 2da5efe2d9..1f9d5eca00 100644 --- a/docs/deploying/uwsgi.rst +++ b/docs/deploying/uwsgi.rst @@ -29,8 +29,8 @@ Create a virtualenv, install your application, then install ``pyuwsgi``. .. code-block:: text $ cd hello-app - $ python -m venv venv - $ . venv/bin/activate + $ python -m venv .venv + $ . .venv/bin/activate $ pip install . # install your application $ pip install pyuwsgi diff --git a/docs/deploying/waitress.rst b/docs/deploying/waitress.rst index eb70e058a8..aeafb9f776 100644 --- a/docs/deploying/waitress.rst +++ b/docs/deploying/waitress.rst @@ -27,8 +27,8 @@ Create a virtualenv, install your application, then install .. code-block:: text $ cd hello-app - $ python -m venv venv - $ . venv/bin/activate + $ python -m venv .venv + $ . .venv/bin/activate $ pip install . # install your application $ pip install waitress diff --git a/docs/installation.rst b/docs/installation.rst index 8a338e182a..276fdc3dd4 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -85,7 +85,7 @@ environments. Create an environment ~~~~~~~~~~~~~~~~~~~~~ -Create a project folder and a :file:`venv` folder within: +Create a project folder and a :file:`.venv` folder within: .. tabs:: @@ -95,7 +95,7 @@ Create a project folder and a :file:`venv` folder within: $ mkdir myproject $ cd myproject - $ python3 -m venv venv + $ python3 -m venv .venv .. group-tab:: Windows @@ -103,7 +103,7 @@ Create a project folder and a :file:`venv` folder within: > mkdir myproject > cd myproject - > py -3 -m venv venv + > py -3 -m venv .venv .. _install-activate-env: @@ -119,13 +119,13 @@ Before you work on your project, activate the corresponding environment: .. code-block:: text - $ . venv/bin/activate + $ . .venv/bin/activate .. group-tab:: Windows .. code-block:: text - > venv\Scripts\activate + > .venv\Scripts\activate Your shell prompt will change to show the name of the activated environment. diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 13f8270ee8..12c4608335 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -42,19 +42,20 @@ You should then end up with something like that:: But how do you run your application now? The naive ``python yourapplication/__init__.py`` will not work. Let's just say that Python does not want modules in packages to be the startup file. But that is not -a big problem, just add a new file called :file:`setup.py` next to the inner -:file:`yourapplication` folder with the following contents:: +a big problem, just add a new file called :file:`pyproject.toml` next to the inner +:file:`yourapplication` folder with the following contents: - from setuptools import setup +.. code-block:: toml - setup( - name='yourapplication', - packages=['yourapplication'], - include_package_data=True, - install_requires=[ - 'flask', - ], - ) + [project] + name = "yourapplication" + dependencies = [ + "flask", + ] + + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" Install your application so it is importable: @@ -98,7 +99,7 @@ And this is what :file:`views.py` would look like:: You should then end up with something like that:: /yourapplication - setup.py + pyproject.toml /yourapplication __init__.py views.py diff --git a/docs/tutorial/deploy.rst b/docs/tutorial/deploy.rst index 436ed5e812..eb3a53ac5e 100644 --- a/docs/tutorial/deploy.rst +++ b/docs/tutorial/deploy.rst @@ -14,22 +14,13 @@ application. Build and Install ----------------- -When you want to deploy your application elsewhere, you build a -distribution file. The current standard for Python distribution is the -*wheel* format, with the ``.whl`` extension. Make sure the wheel library -is installed first: +When you want to deploy your application elsewhere, you build a *wheel* +(``.whl``) file. Install and use the ``build`` tool to do this. .. code-block:: none - $ pip install wheel - -Running ``setup.py`` with Python gives you a command line tool to issue -build-related commands. The ``bdist_wheel`` command will build a wheel -distribution file. - -.. code-block:: none - - $ python setup.py bdist_wheel + $ pip install build + $ python -m build --wheel You can find the file in ``dist/flaskr-1.0.0-py3-none-any.whl``. The file name is in the format of {project name}-{version}-{python tag} @@ -54,7 +45,7 @@ create the database in the instance folder. When Flask detects that it's installed (not in editable mode), it uses a different directory for the instance folder. You can find it at -``venv/var/flaskr-instance`` instead. +``.venv/var/flaskr-instance`` instead. Configure the Secret Key @@ -77,7 +68,7 @@ Create the ``config.py`` file in the instance folder, which the factory will read from if it exists. Copy the generated value into it. .. code-block:: python - :caption: ``venv/var/flaskr-instance/config.py`` + :caption: ``.venv/var/flaskr-instance/config.py`` SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' diff --git a/docs/tutorial/install.rst b/docs/tutorial/install.rst index f6820ebde9..9bb1234eda 100644 --- a/docs/tutorial/install.rst +++ b/docs/tutorial/install.rst @@ -1,11 +1,10 @@ Make the Project Installable ============================ -Making your project installable means that you can build a -*distribution* file and install that in another environment, just like -you installed Flask in your project's environment. This makes deploying -your project the same as installing any other library, so you're using -all the standard Python tools to manage everything. +Making your project installable means that you can build a *wheel* file and install that +in another environment, just like you installed Flask in your project's environment. +This makes deploying your project the same as installing any other library, so you're +using all the standard Python tools to manage everything. Installing also comes with other benefits that might not be obvious from the tutorial or as a new Python user, including: @@ -28,31 +27,25 @@ the tutorial or as a new Python user, including: Describe the Project -------------------- -The ``setup.py`` file describes your project and the files that belong -to it. +The ``pyproject.toml`` file describes your project and how to build it. -.. code-block:: python - :caption: ``setup.py`` +.. code-block:: toml + :caption: ``pyproject.toml`` - from setuptools import find_packages, setup + [project] + name = "flaskr" + version = "1.0.0" + dependencies = [ + "flask", + ] - setup( - name='flaskr', - version='1.0.0', - packages=find_packages(), - include_package_data=True, - install_requires=[ - 'flask', - ], - ) + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" -``packages`` tells Python what package directories (and the Python files -they contain) to include. ``find_packages()`` finds these directories -automatically so you don't have to type them out. To include other -files, such as the static and templates directories, -``include_package_data`` is set. Python needs another file named -``MANIFEST.in`` to tell what this other data is. +The setuptools build backend needs another file named ``MANIFEST.in`` to tell it about +non-Python files to include. .. code-block:: none :caption: ``MANIFEST.in`` @@ -62,9 +55,8 @@ files, such as the static and templates directories, graft flaskr/templates global-exclude *.pyc -This tells Python to copy everything in the ``static`` and ``templates`` -directories, and the ``schema.sql`` file, but to exclude all bytecode -files. +This tells the build to copy everything in the ``static`` and ``templates`` directories, +and the ``schema.sql`` file, but to exclude all bytecode files. See the official `Packaging tutorial `_ and `detailed guide `_ for more explanation of the files @@ -83,10 +75,10 @@ Use ``pip`` to install your project in the virtual environment. $ pip install -e . -This tells pip to find ``setup.py`` in the current directory and install -it in *editable* or *development* mode. Editable mode means that as you -make changes to your local code, you'll only need to re-install if you -change the metadata about the project, such as its dependencies. +This tells pip to find ``pyproject.toml`` in the current directory and install the +project in *editable* or *development* mode. Editable mode means that as you make +changes to your local code, you'll only need to re-install if you change the metadata +about the project, such as its dependencies. You can observe that the project is now installed with ``pip list``. diff --git a/docs/tutorial/layout.rst b/docs/tutorial/layout.rst index b6a09f0377..6f8e59f44d 100644 --- a/docs/tutorial/layout.rst +++ b/docs/tutorial/layout.rst @@ -41,7 +41,7 @@ The project directory will contain: * ``flaskr/``, a Python package containing your application code and files. * ``tests/``, a directory containing test modules. -* ``venv/``, a Python virtual environment where Flask and other +* ``.venv/``, a Python virtual environment where Flask and other dependencies are installed. * Installation files telling Python how to install your project. * Version control config, such as `git`_. You should make a habit of @@ -80,8 +80,8 @@ By the end, your project layout will look like this: │ ├── test_db.py │ ├── test_auth.py │ └── test_blog.py - ├── venv/ - ├── setup.py + ├── .venv/ + ├── pyproject.toml └── MANIFEST.in If you're using version control, the following files that are generated @@ -92,7 +92,7 @@ write. For example, with git: .. code-block:: none :caption: ``.gitignore`` - venv/ + .venv/ *.pyc __pycache__/ diff --git a/docs/tutorial/tests.rst b/docs/tutorial/tests.rst index cb60790cf5..f4744cda27 100644 --- a/docs/tutorial/tests.rst +++ b/docs/tutorial/tests.rst @@ -490,20 +490,18 @@ no longer exist in the database. Running the Tests ----------------- -Some extra configuration, which is not required but makes running -tests with coverage less verbose, can be added to the project's -``setup.cfg`` file. +Some extra configuration, which is not required but makes running tests with coverage +less verbose, can be added to the project's ``pyproject.toml`` file. -.. code-block:: none - :caption: ``setup.cfg`` +.. code-block:: toml + :caption: ``pyproject.toml`` - [tool:pytest] - testpaths = tests + [tool.pytest.ini_options] + testpaths = ["tests"] - [coverage:run] - branch = True - source = - flaskr + [tool.coverage.run] + branch = true + source = ["flaskr"] To run the tests, use the ``pytest`` command. It will find and run all the test functions you've written. @@ -514,7 +512,7 @@ the test functions you've written. ========================= test session starts ========================== platform linux -- Python 3.6.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0 - rootdir: /home/user/Projects/flask-tutorial, inifile: setup.cfg + rootdir: /home/user/Projects/flask-tutorial collected 23 items tests/test_auth.py ........ [ 34%] diff --git a/examples/javascript/.gitignore b/examples/javascript/.gitignore index 85a35845ad..a306afbc08 100644 --- a/examples/javascript/.gitignore +++ b/examples/javascript/.gitignore @@ -1,4 +1,4 @@ -venv/ +.venv/ *.pyc __pycache__/ instance/ diff --git a/examples/javascript/README.rst b/examples/javascript/README.rst index 23c7ce436d..697bb21760 100644 --- a/examples/javascript/README.rst +++ b/examples/javascript/README.rst @@ -23,8 +23,8 @@ Install .. code-block:: text - $ python3 -m venv venv - $ . venv/bin/activate + $ python3 -m venv .venv + $ . .venv/bin/activate $ pip install -e . diff --git a/examples/javascript/pyproject.toml b/examples/javascript/pyproject.toml new file mode 100644 index 0000000000..ce326ea715 --- /dev/null +++ b/examples/javascript/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "js_example" +version = "1.1.0" +description = "Demonstrates making AJAX requests to Flask." +readme = "README.rst" +license = {text = "BSD-3-Clause"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +dependencies = ["flask"] + +[project.urls] +Documentation = "https://flask.palletsprojects.com/patterns/jquery/" + +[project.optional-dependencies] +test = ["pytest", "blinker"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = ["error"] + +[tool.coverage.run] +branch = true +source = ["js_example", "tests"] diff --git a/examples/javascript/setup.cfg b/examples/javascript/setup.cfg deleted file mode 100644 index f509ddfe50..0000000000 --- a/examples/javascript/setup.cfg +++ /dev/null @@ -1,29 +0,0 @@ -[metadata] -name = js_example -version = 1.1.0 -url = https://flask.palletsprojects.com/patterns/jquery/ -license = BSD-3-Clause -maintainer = Pallets -maintainer_email = contact@palletsprojects.com -description = Demonstrates making AJAX requests to Flask. -long_description = file: README.rst -long_description_content_type = text/x-rst - -[options] -packages = find: -include_package_data = true -install_requires = - Flask - -[options.extras_require] -test = - pytest - blinker - -[tool:pytest] -testpaths = tests - -[coverage:run] -branch = True -source = - js_example diff --git a/examples/javascript/setup.py b/examples/javascript/setup.py deleted file mode 100644 index 606849326a..0000000000 --- a/examples/javascript/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup() diff --git a/examples/tutorial/.gitignore b/examples/tutorial/.gitignore index 85a35845ad..a306afbc08 100644 --- a/examples/tutorial/.gitignore +++ b/examples/tutorial/.gitignore @@ -1,4 +1,4 @@ -venv/ +.venv/ *.pyc __pycache__/ instance/ diff --git a/examples/tutorial/README.rst b/examples/tutorial/README.rst index 1c745078bc..653c216729 100644 --- a/examples/tutorial/README.rst +++ b/examples/tutorial/README.rst @@ -23,13 +23,13 @@ default Git version is the main branch. :: Create a virtualenv and activate it:: - $ python3 -m venv venv - $ . venv/bin/activate + $ python3 -m venv .venv + $ . .venv/bin/activate Or on Windows cmd:: - $ py -3 -m venv venv - $ venv\Scripts\activate.bat + $ py -3 -m venv .venv + $ .venv\Scripts\activate.bat Install Flaskr:: diff --git a/examples/tutorial/pyproject.toml b/examples/tutorial/pyproject.toml new file mode 100644 index 0000000000..c86eb61f19 --- /dev/null +++ b/examples/tutorial/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "flaskr" +version = "1.0.0" +description = "The basic blog app built in the Flask tutorial." +readme = "README.rst" +license = {text = "BSD-3-Clause"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +dependencies = [ + "flask", +] + +[project.urls] +Documentation = "https://flask.palletsprojects.com/tutorial/" + +[project.optional-dependencies] +test = ["pytest"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = ["error"] + +[tool.coverage.run] +branch = true +source = ["flaskr", "tests"] diff --git a/examples/tutorial/setup.cfg b/examples/tutorial/setup.cfg deleted file mode 100644 index d001093b48..0000000000 --- a/examples/tutorial/setup.cfg +++ /dev/null @@ -1,28 +0,0 @@ -[metadata] -name = flaskr -version = 1.0.0 -url = https://flask.palletsprojects.com/tutorial/ -license = BSD-3-Clause -maintainer = Pallets -maintainer_email = contact@palletsprojects.com -description = The basic blog app built in the Flask tutorial. -long_description = file: README.rst -long_description_content_type = text/x-rst - -[options] -packages = find: -include_package_data = true -install_requires = - Flask - -[options.extras_require] -test = - pytest - -[tool:pytest] -testpaths = tests - -[coverage:run] -branch = True -source = - flaskr diff --git a/examples/tutorial/setup.py b/examples/tutorial/setup.py deleted file mode 100644 index 606849326a..0000000000 --- a/examples/tutorial/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup() From 261e4a6cf287180b69c4db407791e43ce90e50ad Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 18 Jan 2023 10:31:08 -0800 Subject: [PATCH 161/229] fix flake8 bugbear errors --- .flake8 | 23 ++++++++++++----------- tests/test_appctx.py | 6 +++--- tests/test_basic.py | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.flake8 b/.flake8 index 0980961647..a5ce884451 100644 --- a/.flake8 +++ b/.flake8 @@ -1,12 +1,12 @@ [flake8] -# B = bugbear -# E = pycodestyle errors -# F = flake8 pyflakes -# W = pycodestyle warnings -# B9 = bugbear opinions -# ISC = implicit str concat -select = B, E, F, W, B9, ISC -ignore = +extend-select = + # bugbear + B + # bugbear opinions + B9 + # implicit str concat + ISC +extend-ignore = # slice notation whitespace, invalid E203 # import at top, too many circular import fixes @@ -15,10 +15,11 @@ ignore = E501 # bare except, handled by bugbear B001 E722 - # bin op line break, invalid - W503 - # requires Python 3.10 + # zip with strict=, requires python >= 3.10 B905 + # string formatting opinion, B028 renamed to B907 + B028 + B907 # up to 88 allowed by bugbear B950 max-line-length = 80 per-file-ignores = diff --git a/tests/test_appctx.py b/tests/test_appctx.py index f5ca0bde42..aa3a8b4e5c 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -120,14 +120,14 @@ def cleanup(exception): @app.route("/") def index(): - raise Exception("dummy") + raise ValueError("dummy") - with pytest.raises(Exception, match="dummy"): + with pytest.raises(ValueError, match="dummy"): with app.app_context(): client.get("/") assert len(cleanup_stuff) == 1 - assert isinstance(cleanup_stuff[0], Exception) + assert isinstance(cleanup_stuff[0], ValueError) assert str(cleanup_stuff[0]) == "dummy" diff --git a/tests/test_basic.py b/tests/test_basic.py index d547012a5a..9aca667938 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1472,11 +1472,11 @@ def test_static_route_with_host_matching(): rv = flask.url_for("static", filename="index.html", _external=True) assert rv == "http://example.com/static/index.html" # Providing static_host without host_matching=True should error. - with pytest.raises(Exception): + with pytest.raises(AssertionError): flask.Flask(__name__, static_host="example.com") # Providing host_matching=True with static_folder # but without static_host should error. - with pytest.raises(Exception): + with pytest.raises(AssertionError): flask.Flask(__name__, host_matching=True) # Providing host_matching=True without static_host # but with static_folder=None should not error. From 3a35977d5fbcc424688d32154d250bc0bae918d6 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 19 Jan 2023 06:35:15 -0800 Subject: [PATCH 162/229] stop ignoring flake8 e402 --- .flake8 | 2 -- examples/javascript/js_example/__init__.py | 2 +- tests/test_apps/blueprintapp/__init__.py | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.flake8 b/.flake8 index a5ce884451..8f3b4fd4bc 100644 --- a/.flake8 +++ b/.flake8 @@ -9,8 +9,6 @@ extend-select = extend-ignore = # slice notation whitespace, invalid E203 - # import at top, too many circular import fixes - E402 # line length, handled by bugbear B950 E501 # bare except, handled by bugbear B001 diff --git a/examples/javascript/js_example/__init__.py b/examples/javascript/js_example/__init__.py index 068b2d98ed..0ec3ca215a 100644 --- a/examples/javascript/js_example/__init__.py +++ b/examples/javascript/js_example/__init__.py @@ -2,4 +2,4 @@ app = Flask(__name__) -from js_example import views # noqa: F401 +from js_example import views # noqa: E402, F401 diff --git a/tests/test_apps/blueprintapp/__init__.py b/tests/test_apps/blueprintapp/__init__.py index 4b05798531..ad594cf1da 100644 --- a/tests/test_apps/blueprintapp/__init__.py +++ b/tests/test_apps/blueprintapp/__init__.py @@ -2,8 +2,8 @@ app = Flask(__name__) app.config["DEBUG"] = True -from blueprintapp.apps.admin import admin -from blueprintapp.apps.frontend import frontend +from blueprintapp.apps.admin import admin # noqa: E402 +from blueprintapp.apps.frontend import frontend # noqa: E402 app.register_blueprint(admin) app.register_blueprint(frontend) From 99b34f7148b77d4b1311593590629c2bc8e8e772 Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 18 Jan 2023 10:31:08 -0800 Subject: [PATCH 163/229] move and update flake8 config --- .flake8 | 25 ++++++++++++++++++++ examples/javascript/js_example/__init__.py | 2 +- setup.cfg | 27 ---------------------- tests/test_appctx.py | 6 ++--- tests/test_apps/blueprintapp/__init__.py | 4 ++-- tests/test_basic.py | 4 ++-- 6 files changed, 33 insertions(+), 35 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..8f3b4fd4bc --- /dev/null +++ b/.flake8 @@ -0,0 +1,25 @@ +[flake8] +extend-select = + # bugbear + B + # bugbear opinions + B9 + # implicit str concat + ISC +extend-ignore = + # slice notation whitespace, invalid + E203 + # line length, handled by bugbear B950 + E501 + # bare except, handled by bugbear B001 + E722 + # zip with strict=, requires python >= 3.10 + B905 + # string formatting opinion, B028 renamed to B907 + B028 + B907 +# up to 88 allowed by bugbear B950 +max-line-length = 80 +per-file-ignores = + # __init__ exports names + src/flask/__init__.py: F401 diff --git a/examples/javascript/js_example/__init__.py b/examples/javascript/js_example/__init__.py index 068b2d98ed..0ec3ca215a 100644 --- a/examples/javascript/js_example/__init__.py +++ b/examples/javascript/js_example/__init__.py @@ -2,4 +2,4 @@ app = Flask(__name__) -from js_example import views # noqa: F401 +from js_example import views # noqa: E402, F401 diff --git a/setup.cfg b/setup.cfg index ea7f66e20a..736bd50f27 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,33 +61,6 @@ source = src */site-packages -[flake8] -# B = bugbear -# E = pycodestyle errors -# F = flake8 pyflakes -# W = pycodestyle warnings -# B9 = bugbear opinions -# ISC = implicit str concat -select = B, E, F, W, B9, ISC -ignore = - # slice notation whitespace, invalid - E203 - # import at top, too many circular import fixes - E402 - # line length, handled by bugbear B950 - E501 - # bare except, handled by bugbear B001 - E722 - # bin op line break, invalid - W503 - # requires Python 3.10 - B905 -# up to 88 allowed by bugbear B950 -max-line-length = 80 -per-file-ignores = - # __init__ exports names - src/flask/__init__.py: F401 - [mypy] files = src/flask, tests/typing python_version = 3.7 diff --git a/tests/test_appctx.py b/tests/test_appctx.py index f5ca0bde42..aa3a8b4e5c 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -120,14 +120,14 @@ def cleanup(exception): @app.route("/") def index(): - raise Exception("dummy") + raise ValueError("dummy") - with pytest.raises(Exception, match="dummy"): + with pytest.raises(ValueError, match="dummy"): with app.app_context(): client.get("/") assert len(cleanup_stuff) == 1 - assert isinstance(cleanup_stuff[0], Exception) + assert isinstance(cleanup_stuff[0], ValueError) assert str(cleanup_stuff[0]) == "dummy" diff --git a/tests/test_apps/blueprintapp/__init__.py b/tests/test_apps/blueprintapp/__init__.py index 4b05798531..ad594cf1da 100644 --- a/tests/test_apps/blueprintapp/__init__.py +++ b/tests/test_apps/blueprintapp/__init__.py @@ -2,8 +2,8 @@ app = Flask(__name__) app.config["DEBUG"] = True -from blueprintapp.apps.admin import admin -from blueprintapp.apps.frontend import frontend +from blueprintapp.apps.admin import admin # noqa: E402 +from blueprintapp.apps.frontend import frontend # noqa: E402 app.register_blueprint(admin) app.register_blueprint(frontend) diff --git a/tests/test_basic.py b/tests/test_basic.py index d547012a5a..9aca667938 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1472,11 +1472,11 @@ def test_static_route_with_host_matching(): rv = flask.url_for("static", filename="index.html", _external=True) assert rv == "http://example.com/static/index.html" # Providing static_host without host_matching=True should error. - with pytest.raises(Exception): + with pytest.raises(AssertionError): flask.Flask(__name__, static_host="example.com") # Providing host_matching=True with static_folder # but without static_host should error. - with pytest.raises(Exception): + with pytest.raises(AssertionError): flask.Flask(__name__, host_matching=True) # Providing host_matching=True without static_host # but with static_folder=None should not error. From 0b4b61146ffca0f7dfc25f1b245df89442293335 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 20 Jan 2023 13:45:15 -0800 Subject: [PATCH 164/229] build, provenance, publish workflow --- .github/workflows/lock.yaml | 17 +++++--- .github/workflows/publish.yaml | 72 ++++++++++++++++++++++++++++++++++ .github/workflows/tests.yaml | 8 ++-- requirements/build.in | 1 + requirements/build.txt | 17 ++++++++ 5 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/publish.yaml create mode 100644 requirements/build.in create mode 100644 requirements/build.txt diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml index cd89f67c9d..c790fae5cb 100644 --- a/.github/workflows/lock.yaml +++ b/.github/workflows/lock.yaml @@ -1,18 +1,25 @@ -# This does not automatically close "stale" issues. Instead, it locks closed issues after 2 weeks of no activity. -# If there's a new issue related to an old one, we've found it's much easier to work on as a new issue. - name: 'Lock threads' +# Lock closed issues that have not received any further activity for +# two weeks. This does not close open issues, only humans may do that. +# We find that it is easier to respond to new issues with fresh examples +# rather than continuing discussions on old issues. on: schedule: - cron: '0 0 * * *' +permissions: + issues: write + pull-requests: write + +concurrency: + group: lock + jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v3 + - uses: dessant/lock-threads@c1b35aecc5cdb1a34539d14196df55838bb2f836 with: - github-token: ${{ github.token }} issue-inactive-days: 14 pr-inactive-days: 14 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000000..0ed4955916 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,72 @@ +name: Publish +on: + push: + tags: + - '*' +jobs: + build: + runs-on: ubuntu-latest + outputs: + hash: ${{ steps.hash.outputs.hash }} + steps: + - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c + - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 + with: + python-version: '3.x' + cache: 'pip' + cache-dependency-path: 'requirements/*.txt' + - run: pip install -r requirements/build.txt + # Use the commit date instead of the current date during the build. + - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV + - run: python -m build + # Generate hashes used for provenance. + - name: generate hash + id: hash + run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT + - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce + with: + path: ./dist + provenance: + needs: ['build'] + permissions: + actions: read + id-token: write + contents: write + # Can't pin with hash due to how this workflow works. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0 + with: + base64-subjects: ${{ needs.build.outputs.hash }} + create-release: + # Upload the sdist, wheels, and provenance to a GitHub release. They remain + # available as build artifacts for a while as well. + needs: ['provenance'] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a + - name: create release + run: > + gh release create --draft --repo ${{ github.repository }} + ${{ github.ref_name }} + *.intoto.jsonl/* artifact/* + env: + GH_TOKEN: ${{ github.token }} + publish-pypi: + needs: ['provenance'] + # Wait for approval before attempting to upload to PyPI. This allows reviewing the + # files in the draft release. + environment: 'publish' + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a + # Try uploading to Test PyPI first, in case something fails. + - uses: pypa/gh-action-pypi-publish@c7f29f7adef1a245bd91520e94867e5c6eedddcc + with: + password: ${{ secrets.TEST_PYPI_TOKEN }} + repository_url: https://test.pypi.org/legacy/ + packages_dir: artifact/ + - uses: pypa/gh-action-pypi-publish@c7f29f7adef1a245bd91520e94867e5c6eedddcc + with: + password: ${{ secrets.PYPI_TOKEN }} + packages_dir: artifact/ diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 89e548dbad..79a56fcaa8 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -35,10 +35,10 @@ jobs: - {name: 'PyPy', python: 'pypy-3.9', os: ubuntu-latest, tox: pypy39} - {name: 'Pallets Minimum Versions', python: '3.11', os: ubuntu-latest, tox: py311-min} - {name: 'Pallets Development Versions', python: '3.7', os: ubuntu-latest, tox: py37-dev} - - {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing} + - {name: Typing, python: '3.11', os: ubuntu-latest, tox: typing} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c + - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 with: python-version: ${{ matrix.python }} cache: 'pip' @@ -49,7 +49,7 @@ jobs: pip install -U setuptools python -m pip install -U pip - name: cache mypy - uses: actions/cache@v3.2.2 + uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12 with: path: ./.mypy_cache key: mypy|${{ matrix.python }}|${{ hashFiles('setup.cfg') }} diff --git a/requirements/build.in b/requirements/build.in new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/requirements/build.in @@ -0,0 +1 @@ +build diff --git a/requirements/build.txt b/requirements/build.txt new file mode 100644 index 0000000000..a735b3d0d1 --- /dev/null +++ b/requirements/build.txt @@ -0,0 +1,17 @@ +# SHA1:80754af91bfb6d1073585b046fe0a474ce868509 +# +# This file is autogenerated by pip-compile-multi +# To update, run: +# +# pip-compile-multi +# +build==0.9.0 + # via -r requirements/build.in +packaging==23.0 + # via build +pep517==0.13.0 + # via build +tomli==2.0.1 + # via + # build + # pep517 From d93760d8bd6e367faefe9ad550f753e32ef9a12d Mon Sep 17 00:00:00 2001 From: Andrii Kolomoiets Date: Mon, 23 Jan 2023 17:01:49 +0200 Subject: [PATCH 165/229] Fix function argument name --- docs/views.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/views.rst b/docs/views.rst index 8937d7b55c..68a3462ad4 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -297,7 +297,7 @@ provide get (list) and post (create) methods. db.session.commit() return jsonify(item.to_json()) - def register_api(app, model, url): + def register_api(app, model, name): item = ItemAPI.as_view(f"{name}-item", model) group = GroupAPI.as_view(f"{name}-group", model) app.add_url_rule(f"/{name}/", view_func=item) From 94a23a3e24b6736ca3b54773de0ce384270c457a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Feb 2023 16:01:23 +0000 Subject: [PATCH 166/229] Bump actions/setup-python from 4.4.0 to 4.5.0 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.4.0 to 4.5.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/5ccb29d8773c3f3f653e1705f474dfaa8a06a912...d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/publish.yaml | 2 +- .github/workflows/tests.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 0ed4955916..edfdf55d5a 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -10,7 +10,7 @@ jobs: hash: ${{ steps.hash.outputs.hash }} steps: - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c - - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 + - uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 with: python-version: '3.x' cache: 'pip' diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 65b9877d3c..832535bb74 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -38,7 +38,7 @@ jobs: - {name: Typing, python: '3.11', os: ubuntu-latest, tox: typing} steps: - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c - - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 + - uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 with: python-version: ${{ matrix.python }} cache: 'pip' From 74c256872b5f0be65e9f71b91a73ac4b02647298 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Feb 2023 16:01:30 +0000 Subject: [PATCH 167/229] Bump actions/cache from 3.2.3 to 3.2.4 Bumps [actions/cache](https://github.com/actions/cache) from 3.2.3 to 3.2.4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/58c146cc91c5b9e778e71775dfe9bf1442ad9a12...627f0f41f6904a5b1efbaed9f96d9eb58e92e920) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 65b9877d3c..840f429158 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -49,7 +49,7 @@ jobs: pip install -U setuptools python -m pip install -U pip - name: cache mypy - uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12 + uses: actions/cache@627f0f41f6904a5b1efbaed9f96d9eb58e92e920 with: path: ./.mypy_cache key: mypy|${{ matrix.python }}|${{ hashFiles('pyproject.toml') }} From 9abe28130d4f0aff48b4f9931ebc15bd7617f19f Mon Sep 17 00:00:00 2001 From: owgreen Date: Fri, 3 Feb 2023 00:43:02 +0900 Subject: [PATCH 168/229] fix doc --- docs/patterns/appfactories.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/patterns/appfactories.rst b/docs/patterns/appfactories.rst index 415c10fa47..a76e676f32 100644 --- a/docs/patterns/appfactories.rst +++ b/docs/patterns/appfactories.rst @@ -91,7 +91,7 @@ To run such an application, you can use the :command:`flask` command: .. code-block:: text - $ flask run --app hello run + $ flask --app hello run Flask will automatically detect the factory if it is named ``create_app`` or ``make_app`` in ``hello``. You can also pass arguments @@ -99,7 +99,7 @@ to the factory like this: .. code-block:: text - $ flask run --app hello:create_app(local_auth=True)`` + $ flask --app hello:create_app(local_auth=True) run`` Then the ``create_app`` factory in ``myapp`` is called with the keyword argument ``local_auth=True``. See :doc:`/cli` for more detail. From c1d01f69994e87489d04c3218a417f517ab6c88e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Feb 2023 04:42:52 +0000 Subject: [PATCH 169/229] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b1dee57df..d5e7a9c8ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: files: "^(?!examples/)" args: ["--application-directories", "src"] - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.1.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 From a15da89dbb4be39be09ed7e5b57a22acfb117e6d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Feb 2023 04:43:02 +0000 Subject: [PATCH 170/229] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_reqctx.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_reqctx.py b/tests/test_reqctx.py index abfacb98bf..6c38b66186 100644 --- a/tests/test_reqctx.py +++ b/tests/test_reqctx.py @@ -227,7 +227,6 @@ def index(): def test_session_dynamic_cookie_name(): - # This session interface will use a cookie with a different name if the # requested url ends with the string "dynamic_cookie" class PathAwareSessionInterface(SecureCookieSessionInterface): From 428d9430bc69031150a8d50ac8af17cc8082ea4e Mon Sep 17 00:00:00 2001 From: "Maxim G. Ivanov" Date: Thu, 9 Feb 2023 09:38:57 +0700 Subject: [PATCH 171/229] Fix command-line formatting --- docs/patterns/packages.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 13f8270ee8..3c7ae425b1 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -66,6 +66,8 @@ To use the ``flask`` command and run your application you need to set the ``--app`` option that tells Flask where to find the application instance: +.. code-block:: text + $ flask --app yourapplication run What did we gain from this? Now we can restructure the application a bit From dca8cf013b2c0086539697ae6e4515a1c819092c Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 8 Feb 2023 17:30:53 -0800 Subject: [PATCH 172/229] rewrite celery background tasks docs --- docs/patterns/celery.rst | 270 +++++++++++++++++++++++++++++---------- 1 file changed, 200 insertions(+), 70 deletions(-) diff --git a/docs/patterns/celery.rst b/docs/patterns/celery.rst index 228a04a8aa..a236f6dd68 100644 --- a/docs/patterns/celery.rst +++ b/docs/patterns/celery.rst @@ -1,105 +1,235 @@ -Celery Background Tasks -======================= +Background Tasks with Celery +============================ -If your application has a long running task, such as processing some uploaded -data or sending email, you don't want to wait for it to finish during a -request. Instead, use a task queue to send the necessary data to another -process that will run the task in the background while the request returns -immediately. +If your application has a long running task, such as processing some uploaded data or +sending email, you don't want to wait for it to finish during a request. Instead, use a +task queue to send the necessary data to another process that will run the task in the +background while the request returns immediately. + +`Celery`_ is a powerful task queue that can be used for simple background tasks as well +as complex multi-stage programs and schedules. This guide will show you how to configure +Celery using Flask. Read Celery's `First Steps with Celery`_ guide to learn how to use +Celery itself. + +.. _Celery: https://celery.readthedocs.io +.. _First Steps with Celery: https://celery.readthedocs.io/en/latest/getting-started/first-steps-with-celery.html -Celery is a powerful task queue that can be used for simple background tasks -as well as complex multi-stage programs and schedules. This guide will show you -how to configure Celery using Flask, but assumes you've already read the -`First Steps with Celery `_ -guide in the Celery documentation. Install ------- -Celery is a separate Python package. Install it from PyPI using pip:: +Install Celery from PyPI, for example using pip: + +.. code-block:: text $ pip install celery -Configure ---------- -The first thing you need is a Celery instance, this is called the celery -application. It serves the same purpose as the :class:`~flask.Flask` -object in Flask, just for Celery. Since this instance is used as the -entry-point for everything you want to do in Celery, like creating tasks -and managing workers, it must be possible for other modules to import it. +Integrate Celery with Flask +--------------------------- -For instance you can place this in a ``tasks`` module. While you can use -Celery without any reconfiguration with Flask, it becomes a bit nicer by -subclassing tasks and adding support for Flask's application contexts and -hooking it up with the Flask configuration. +You can use Celery without any integration with Flask, but it's convenient to configure +it through Flask's config, and to let tasks access the Flask application. -This is all that is necessary to integrate Celery with Flask: +Celery uses similar ideas to Flask, with a ``Celery`` app object that has configuration +and registers tasks. While creating a Flask app, use the following code to create and +configure a Celery app as well. .. code-block:: python - from celery import Celery - - def make_celery(app): - celery = Celery(app.import_name) - celery.conf.update(app.config["CELERY_CONFIG"]) + from celery import Celery, Task - class ContextTask(celery.Task): - def __call__(self, *args, **kwargs): + def celery_init_app(app: Flask) -> Celery: + class FlaskTask(Task): + def __call__(self, *args: object, **kwargs: object) -> object: with app.app_context(): return self.run(*args, **kwargs) - celery.Task = ContextTask - return celery + celery_app = Celery(app.name, task_cls=FlaskTask) + celery_app.config_from_object(app.config["CELERY"]) + celery_app.set_default() + app.extensions["celery"] = celery_app + return celery_app -The function creates a new Celery object, configures it with the broker -from the application config, updates the rest of the Celery config from -the Flask config and then creates a subclass of the task that wraps the -task execution in an application context. +This creates and returns a ``Celery`` app object. Celery `configuration`_ is taken from +the ``CELERY`` key in the Flask configuration. The Celery app is set as the default, so +that it is seen during each request. The ``Task`` subclass automatically runs task +functions with a Flask app context active, so that services like your database +connections are available. -.. note:: - Celery 5.x deprecated uppercase configuration keys, and 6.x will - remove them. See their official `migration guide`_. +.. _configuration: https://celery.readthedocs.io/en/stable/userguide/configuration.html -.. _migration guide: https://docs.celeryproject.org/en/stable/userguide/configuration.html#conf-old-settings-map. +Here's a basic ``example.py`` that configures Celery to use Redis for communication. We +enable a result backend, but ignore results by default. This allows us to store results +only for tasks where we care about the result. -An example task ---------------- - -Let's write a task that adds two numbers together and returns the result. We -configure Celery's broker and backend to use Redis, create a ``celery`` -application using the factory from above, and then use it to define the task. :: +.. code-block:: python from flask import Flask - flask_app = Flask(__name__) - flask_app.config.update(CELERY_CONFIG={ - 'broker_url': 'redis://localhost:6379', - 'result_backend': 'redis://localhost:6379', - }) - celery = make_celery(flask_app) + app = Flask(__name__) + app.config.from_mapping( + CELERY=dict( + broker_url="redis://localhost", + result_backend="redis://localhost", + task_ignore_result=True, + ), + ) + celery_app = celery_init_app(app) + +Point the ``celery worker`` command at this and it will find the ``celery_app`` object. + +.. code-block:: text + + $ celery -A example worker --loglevel INFO + +You can also run the ``celery beat`` command to run tasks on a schedule. See Celery's +docs for more information about defining schedules. + +.. code-block:: text + + $ celery -A example beat --loglevel INFO + + +Application Factory +------------------- + +When using the Flask application factory pattern, call the ``celery_init_app`` function +inside the factory. It sets ``app.extensions["celery"]`` to the Celery app object, which +can be used to get the Celery app from the Flask app returned by the factory. + +.. code-block:: python + + def create_app() -> Flask: + app = Flask(__name__) + app.config.from_mapping( + CELERY=dict( + broker_url="redis://localhost", + result_backend="redis://localhost", + task_ignore_result=True, + ), + ) + app.config.from_prefixed_env() + celery_init_app(app) + return app + +To use ``celery`` commands, Celery needs an app object, but that's no longer directly +available. Create a ``make_celery.py`` file that calls the Flask app factory and gets +the Celery app from the returned Flask app. + +.. code-block:: python + + from example import create_app + + flask_app = create_app() + celery_app = flask_app.extensions["celery"] + +Point the ``celery`` command to this file. + +.. code-block:: text + + $ celery -A make_celery worker --loglevel INFO + $ celery -A make_celery beat --loglevel INFO + - @celery.task() - def add_together(a, b): +Defining Tasks +-------------- + +Using ``@celery_app.task`` to decorate task functions requires access to the +``celery_app`` object, which won't be available when using the factory pattern. It also +means that the decorated tasks are tied to the specific Flask and Celery app instances, +which could be an issue during testing if you change configuration for a test. + +Instead, use Celery's ``@shared_task`` decorator. This creates task objects that will +access whatever the "current app" is, which is a similar concept to Flask's blueprints +and app context. This is why we called ``celery_app.set_default()`` above. + +Here's an example task that adds two numbers together and returns the result. + +.. code-block:: python + + from celery import shared_task + + @shared_task(ignore_result=False) + def add_together(a: int, b: int) -> int: return a + b -This task can now be called in the background:: +Earlier, we configured Celery to ignore task results by default. Since we want to know +the return value of this task, we set ``ignore_result=False``. On the other hand, a task +that didn't need a result, such as sending an email, wouldn't set this. + + +Calling Tasks +------------- + +The decorated function becomes a task object with methods to call it in the background. +The simplest way is to use the ``delay(*args, **kwargs)`` method. See Celery's docs for +more methods. + +A Celery worker must be running to run the task. Starting a worker is shown in the +previous sections. + +.. code-block:: python + + from flask import request - result = add_together.delay(23, 42) - result.wait() # 65 + @app.post("/add") + def start_add() -> dict[str, object]: + a = request.form.get("a", type=int) + b = request.form.get("b", type=int) + result = add_together.delay(a, b) + return {"result_id": result.id} -Run a worker ------------- +The route doesn't get the task's result immediately. That would defeat the purpose by +blocking the response. Instead, we return the running task's result id, which we can use +later to get the result. -If you jumped in and already executed the above code you will be -disappointed to learn that ``.wait()`` will never actually return. -That's because you also need to run a Celery worker to receive and execute the -task. :: - $ celery -A your_application.celery worker +Getting Results +--------------- + +To fetch the result of the task we started above, we'll add another route that takes the +result id we returned before. We return whether the task is finished (ready), whether it +finished successfully, and what the return value (or error) was if it is finished. + +.. code-block:: python + + from celery.result import AsyncResult + + @app.get("/result/") + def task_result(id: str) -> dict[str, object]: + result = AsyncResult(id) + return { + "ready": result.ready(), + "successful": result.successful(), + "value": result.result if result.ready() else None, + } + +Now you can start the task using the first route, then poll for the result using the +second route. This keeps the Flask request workers from being blocked waiting for tasks +to finish. + + +Passing Data to Tasks +--------------------- + +The "add" task above took two integers as arguments. To pass arguments to tasks, Celery +has to serialize them to a format that it can pass to other processes. Therefore, +passing complex objects is not recommended. For example, it would be impossible to pass +a SQLAlchemy model object, since that object is probably not serializable and is tied to +the session that queried it. + +Pass the minimal amount of data necessary to fetch or recreate any complex data within +the task. Consider a task that will run when the logged in user asks for an archive of +their data. The Flask request knows the logged in user, and has the user object queried +from the database. It got that by querying the database for a given id, so the task can +do the same thing. Pass the user's id rather than the user object. + +.. code-block:: python -The ``your_application`` string has to point to your application's package -or module that creates the ``celery`` object. + @shared_task + def generate_user_archive(user_id: str) -> None: + user = db.session.get(User, user_id) + ... -Now that the worker is running, ``wait`` will return the result once the task -is finished. + generate_user_archive.delay(current_user.id) From 3f195248dcd59f8eb08e282b7980dc04b97d7391 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 9 Feb 2023 10:50:57 -0800 Subject: [PATCH 173/229] add celery example --- docs/patterns/celery.rst | 7 ++ examples/celery/README.md | 27 +++++ examples/celery/make_celery.py | 4 + examples/celery/pyproject.toml | 11 ++ examples/celery/requirements.txt | 56 +++++++++ examples/celery/src/task_app/__init__.py | 39 +++++++ examples/celery/src/task_app/tasks.py | 23 ++++ .../celery/src/task_app/templates/index.html | 108 ++++++++++++++++++ examples/celery/src/task_app/views.py | 38 ++++++ 9 files changed, 313 insertions(+) create mode 100644 examples/celery/README.md create mode 100644 examples/celery/make_celery.py create mode 100644 examples/celery/pyproject.toml create mode 100644 examples/celery/requirements.txt create mode 100644 examples/celery/src/task_app/__init__.py create mode 100644 examples/celery/src/task_app/tasks.py create mode 100644 examples/celery/src/task_app/templates/index.html create mode 100644 examples/celery/src/task_app/views.py diff --git a/docs/patterns/celery.rst b/docs/patterns/celery.rst index a236f6dd68..2e9a43a731 100644 --- a/docs/patterns/celery.rst +++ b/docs/patterns/celery.rst @@ -14,6 +14,10 @@ Celery itself. .. _Celery: https://celery.readthedocs.io .. _First Steps with Celery: https://celery.readthedocs.io/en/latest/getting-started/first-steps-with-celery.html +The Flask repository contains `an example `_ +based on the information on this page, which also shows how to use JavaScript to submit +tasks and poll for progress and results. + Install ------- @@ -209,6 +213,9 @@ Now you can start the task using the first route, then poll for the result using second route. This keeps the Flask request workers from being blocked waiting for tasks to finish. +The Flask repository contains `an example `_ +using JavaScript to submit tasks and poll for progress and results. + Passing Data to Tasks --------------------- diff --git a/examples/celery/README.md b/examples/celery/README.md new file mode 100644 index 0000000000..91782019e2 --- /dev/null +++ b/examples/celery/README.md @@ -0,0 +1,27 @@ +Background Tasks with Celery +============================ + +This example shows how to configure Celery with Flask, how to set up an API for +submitting tasks and polling results, and how to use that API with JavaScript. See +[Flask's documentation about Celery](https://flask.palletsprojects.com/patterns/celery/). + +From this directory, create a virtualenv and install the application into it. Then run a +Celery worker. + +```shell +$ python3 -m venv .venv +$ . ./.venv/bin/activate +$ pip install -r requirements.txt && pip install -e . +$ celery -A make_celery worker --loglevel INFO +``` + +In a separate terminal, activate the virtualenv and run the Flask development server. + +```shell +$ . ./.venv/bin/activate +$ flask -A task_app --debug run +``` + +Go to http://localhost:5000/ and use the forms to submit tasks. You can see the polling +requests in the browser dev tools and the Flask logs. You can see the tasks submitting +and completing in the Celery logs. diff --git a/examples/celery/make_celery.py b/examples/celery/make_celery.py new file mode 100644 index 0000000000..f7d138e642 --- /dev/null +++ b/examples/celery/make_celery.py @@ -0,0 +1,4 @@ +from task_app import create_app + +flask_app = create_app() +celery_app = flask_app.extensions["celery"] diff --git a/examples/celery/pyproject.toml b/examples/celery/pyproject.toml new file mode 100644 index 0000000000..88ba6b960c --- /dev/null +++ b/examples/celery/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "flask-example-celery" +version = "1.0.0" +description = "Example Flask application with Celery background tasks." +readme = "README.md" +requires-python = ">=3.7" +dependencies = ["flask>=2.2.2", "celery[redis]>=5.2.7"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/examples/celery/requirements.txt b/examples/celery/requirements.txt new file mode 100644 index 0000000000..b283401366 --- /dev/null +++ b/examples/celery/requirements.txt @@ -0,0 +1,56 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile pyproject.toml +# +amqp==5.1.1 + # via kombu +async-timeout==4.0.2 + # via redis +billiard==3.6.4.0 + # via celery +celery[redis]==5.2.7 + # via flask-example-celery (pyproject.toml) +click==8.1.3 + # via + # celery + # click-didyoumean + # click-plugins + # click-repl + # flask +click-didyoumean==0.3.0 + # via celery +click-plugins==1.1.1 + # via celery +click-repl==0.2.0 + # via celery +flask==2.2.2 + # via flask-example-celery (pyproject.toml) +itsdangerous==2.1.2 + # via flask +jinja2==3.1.2 + # via flask +kombu==5.2.4 + # via celery +markupsafe==2.1.2 + # via + # jinja2 + # werkzeug +prompt-toolkit==3.0.36 + # via click-repl +pytz==2022.7.1 + # via celery +redis==4.5.1 + # via celery +six==1.16.0 + # via click-repl +vine==5.0.0 + # via + # amqp + # celery + # kombu +wcwidth==0.2.6 + # via prompt-toolkit +werkzeug==2.2.2 + # via flask diff --git a/examples/celery/src/task_app/__init__.py b/examples/celery/src/task_app/__init__.py new file mode 100644 index 0000000000..dafff8aad8 --- /dev/null +++ b/examples/celery/src/task_app/__init__.py @@ -0,0 +1,39 @@ +from celery import Celery +from celery import Task +from flask import Flask +from flask import render_template + + +def create_app() -> Flask: + app = Flask(__name__) + app.config.from_mapping( + CELERY=dict( + broker_url="redis://localhost", + result_backend="redis://localhost", + task_ignore_result=True, + ), + ) + app.config.from_prefixed_env() + celery_init_app(app) + + @app.route("/") + def index() -> str: + return render_template("index.html") + + from . import views + + app.register_blueprint(views.bp) + return app + + +def celery_init_app(app: Flask) -> Celery: + class FlaskTask(Task): + def __call__(self, *args: object, **kwargs: object) -> object: + with app.app_context(): + return self.run(*args, **kwargs) + + celery_app = Celery(app.name, task_cls=FlaskTask) + celery_app.config_from_object(app.config["CELERY"]) + celery_app.set_default() + app.extensions["celery"] = celery_app + return celery_app diff --git a/examples/celery/src/task_app/tasks.py b/examples/celery/src/task_app/tasks.py new file mode 100644 index 0000000000..b6b3595d22 --- /dev/null +++ b/examples/celery/src/task_app/tasks.py @@ -0,0 +1,23 @@ +import time + +from celery import shared_task +from celery import Task + + +@shared_task(ignore_result=False) +def add(a: int, b: int) -> int: + return a + b + + +@shared_task() +def block() -> None: + time.sleep(5) + + +@shared_task(bind=True, ignore_result=False) +def process(self: Task, total: int) -> object: + for i in range(total): + self.update_state(state="PROGRESS", meta={"current": i + 1, "total": total}) + time.sleep(1) + + return {"current": total, "total": total} diff --git a/examples/celery/src/task_app/templates/index.html b/examples/celery/src/task_app/templates/index.html new file mode 100644 index 0000000000..4e1145cb8f --- /dev/null +++ b/examples/celery/src/task_app/templates/index.html @@ -0,0 +1,108 @@ + + + + + Celery Example + + +

Celery Example

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

Add

+

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

+
+
+ +
+

Result:

+ +
+

Block

+

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

+ +
+

+ +
+

Process

+

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

+
+ +
+

+ + + + diff --git a/examples/celery/src/task_app/views.py b/examples/celery/src/task_app/views.py new file mode 100644 index 0000000000..99cf92dc20 --- /dev/null +++ b/examples/celery/src/task_app/views.py @@ -0,0 +1,38 @@ +from celery.result import AsyncResult +from flask import Blueprint +from flask import request + +from . import tasks + +bp = Blueprint("tasks", __name__, url_prefix="/tasks") + + +@bp.get("/result/") +def result(id: str) -> dict[str, object]: + result = AsyncResult(id) + ready = result.ready() + return { + "ready": ready, + "successful": result.successful() if ready else None, + "value": result.get() if ready else result.result, + } + + +@bp.post("/add") +def add() -> dict[str, object]: + a = request.form.get("a", type=int) + b = request.form.get("b", type=int) + result = tasks.add.delay(a, b) + return {"result_id": result.id} + + +@bp.post("/block") +def block() -> dict[str, object]: + result = tasks.block.delay() + return {"result_id": result.id} + + +@bp.post("/process") +def process() -> dict[str, object]: + result = tasks.process.delay(total=request.form.get("total", type=int)) + return {"result_id": result.id} From ab93222bd6d4ea26e3aa832a0409489530f3f5e0 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 10 Feb 2023 09:58:48 -0800 Subject: [PATCH 174/229] point to app-scoped blueprint methods --- src/flask/blueprints.py | 70 ++++++++++++++++++++++------------------- src/flask/scaffold.py | 40 ++++++++++++++++++++++- 2 files changed, 77 insertions(+), 33 deletions(-) diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index f6d62ba83f..eb6642358d 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -478,8 +478,11 @@ def add_url_rule( provide_automatic_options: t.Optional[bool] = None, **options: t.Any, ) -> None: - """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for - the :func:`url_for` function is prefixed with the name of the blueprint. + """Register a URL rule with the blueprint. See :meth:`.Flask.add_url_rule` for + full documentation. + + The URL rule is prefixed with the blueprint's URL prefix. The endpoint name, + used with :func:`url_for`, is prefixed with the blueprint's name. """ if endpoint and "." in endpoint: raise ValueError("'endpoint' may not contain a dot '.' character.") @@ -501,8 +504,8 @@ def add_url_rule( def app_template_filter( self, name: t.Optional[str] = None ) -> t.Callable[[T_template_filter], T_template_filter]: - """Register a custom template filter, available application wide. Like - :meth:`Flask.template_filter` but for a blueprint. + """Register a template filter, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_filter`. :param name: the optional name of the filter, otherwise the function name will be used. @@ -518,9 +521,9 @@ def decorator(f: T_template_filter) -> T_template_filter: def add_app_template_filter( self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None ) -> None: - """Register a custom template filter, available application wide. Like - :meth:`Flask.add_template_filter` but for a blueprint. Works exactly - like the :meth:`app_template_filter` decorator. + """Register a template filter, available in any template rendered by the + application. Works like the :meth:`app_template_filter` decorator. Equivalent to + :meth:`.Flask.add_template_filter`. :param name: the optional name of the filter, otherwise the function name will be used. @@ -535,8 +538,8 @@ def register_template(state: BlueprintSetupState) -> None: def app_template_test( self, name: t.Optional[str] = None ) -> t.Callable[[T_template_test], T_template_test]: - """Register a custom template test, available application wide. Like - :meth:`Flask.template_test` but for a blueprint. + """Register a template test, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_test`. .. versionadded:: 0.10 @@ -554,9 +557,9 @@ def decorator(f: T_template_test) -> T_template_test: def add_app_template_test( self, f: ft.TemplateTestCallable, name: t.Optional[str] = None ) -> None: - """Register a custom template test, available application wide. Like - :meth:`Flask.add_template_test` but for a blueprint. Works exactly - like the :meth:`app_template_test` decorator. + """Register a template test, available in any template rendered by the + application. Works like the :meth:`app_template_test` decorator. Equivalent to + :meth:`.Flask.add_template_test`. .. versionadded:: 0.10 @@ -573,8 +576,8 @@ def register_template(state: BlueprintSetupState) -> None: def app_template_global( self, name: t.Optional[str] = None ) -> t.Callable[[T_template_global], T_template_global]: - """Register a custom template global, available application wide. Like - :meth:`Flask.template_global` but for a blueprint. + """Register a template global, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_global`. .. versionadded:: 0.10 @@ -592,9 +595,9 @@ def decorator(f: T_template_global) -> T_template_global: def add_app_template_global( self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None ) -> None: - """Register a custom template global, available application wide. Like - :meth:`Flask.add_template_global` but for a blueprint. Works exactly - like the :meth:`app_template_global` decorator. + """Register a template global, available in any template rendered by the + application. Works like the :meth:`app_template_global` decorator. Equivalent to + :meth:`.Flask.add_template_global`. .. versionadded:: 0.10 @@ -609,8 +612,8 @@ def register_template(state: BlueprintSetupState) -> None: @setupmethod def before_app_request(self, f: T_before_request) -> T_before_request: - """Like :meth:`Flask.before_request`. Such a function is executed - before each request, even if outside of a blueprint. + """Like :meth:`before_request`, but before every request, not only those handled + by the blueprint. Equivalent to :meth:`.Flask.before_request`. """ self.record_once( lambda s: s.app.before_request_funcs.setdefault(None, []).append(f) @@ -621,8 +624,8 @@ def before_app_request(self, f: T_before_request) -> T_before_request: def before_app_first_request( self, f: T_before_first_request ) -> T_before_first_request: - """Like :meth:`Flask.before_first_request`. Such a function is - executed before the first request to the application. + """Register a function to run before the first request to the application is + handled by the worker. Equivalent to :meth:`.Flask.before_first_request`. .. deprecated:: 2.2 Will be removed in Flask 2.3. Run setup code when creating @@ -642,8 +645,8 @@ def before_app_first_request( @setupmethod def after_app_request(self, f: T_after_request) -> T_after_request: - """Like :meth:`Flask.after_request` but for a blueprint. Such a function - is executed after each request, even if outside of the blueprint. + """Like :meth:`after_request`, but after every request, not only those handled + by the blueprint. Equivalent to :meth:`.Flask.after_request`. """ self.record_once( lambda s: s.app.after_request_funcs.setdefault(None, []).append(f) @@ -652,9 +655,8 @@ def after_app_request(self, f: T_after_request) -> T_after_request: @setupmethod def teardown_app_request(self, f: T_teardown) -> T_teardown: - """Like :meth:`Flask.teardown_request` but for a blueprint. Such a - function is executed when tearing down each request, even if outside of - the blueprint. + """Like :meth:`teardown_request`, but after every request, not only those + handled by the blueprint. Equivalent to :meth:`.Flask.teardown_request`. """ self.record_once( lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f) @@ -665,8 +667,8 @@ def teardown_app_request(self, f: T_teardown) -> T_teardown: def app_context_processor( self, f: T_template_context_processor ) -> T_template_context_processor: - """Like :meth:`Flask.context_processor` but for a blueprint. Such a - function is executed each request, even if outside of the blueprint. + """Like :meth:`context_processor`, but for templates rendered by every view, not + only by the blueprint. Equivalent to :meth:`.Flask.context_processor`. """ self.record_once( lambda s: s.app.template_context_processors.setdefault(None, []).append(f) @@ -677,8 +679,8 @@ def app_context_processor( def app_errorhandler( self, code: t.Union[t.Type[Exception], int] ) -> t.Callable[[T_error_handler], T_error_handler]: - """Like :meth:`Flask.errorhandler` but for a blueprint. This - handler is used for all requests, even if outside of the blueprint. + """Like :meth:`errorhandler`, but for every request, not only those handled by + the blueprint. Equivalent to :meth:`.Flask.errorhandler`. """ def decorator(f: T_error_handler) -> T_error_handler: @@ -691,7 +693,9 @@ def decorator(f: T_error_handler) -> T_error_handler: def app_url_value_preprocessor( self, f: T_url_value_preprocessor ) -> T_url_value_preprocessor: - """Same as :meth:`url_value_preprocessor` but application wide.""" + """Like :meth:`url_value_preprocessor`, but for every request, not only those + handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`. + """ self.record_once( lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) ) @@ -699,7 +703,9 @@ def app_url_value_preprocessor( @setupmethod def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults: - """Same as :meth:`url_defaults` but application wide.""" + """Like :meth:`url_defaults`, but for every request, not only those handled by + the blueprint. Equivalent to :meth:`.Flask.url_defaults`. + """ self.record_once( lambda s: s.app.url_default_functions.setdefault(None, []).append(f) ) diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index ebfc741f1a..7277b33ad3 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -561,6 +561,11 @@ def load_user(): a non-``None`` value, the value is handled as if it was the return value from the view, and further request handling is stopped. + + This is available on both app and blueprint objects. When used on an app, this + executes before every request. When used on a blueprint, this executes before + every request that the blueprint handles. To register with a blueprint and + execute before every request, use :meth:`.Blueprint.before_app_request`. """ self.before_request_funcs.setdefault(None, []).append(f) return f @@ -577,6 +582,11 @@ def after_request(self, f: T_after_request) -> T_after_request: ``after_request`` functions will not be called. Therefore, this should not be used for actions that must execute, such as to close resources. Use :meth:`teardown_request` for that. + + This is available on both app and blueprint objects. When used on an app, this + executes after every request. When used on a blueprint, this executes after + every request that the blueprint handles. To register with a blueprint and + execute after every request, use :meth:`.Blueprint.after_app_request`. """ self.after_request_funcs.setdefault(None, []).append(f) return f @@ -606,6 +616,11 @@ def teardown_request(self, f: T_teardown) -> T_teardown: ``try``/``except`` block and log any errors. The return values of teardown functions are ignored. + + This is available on both app and blueprint objects. When used on an app, this + executes after every request. When used on a blueprint, this executes after + every request that the blueprint handles. To register with a blueprint and + execute after every request, use :meth:`.Blueprint.teardown_app_request`. """ self.teardown_request_funcs.setdefault(None, []).append(f) return f @@ -615,7 +630,15 @@ def context_processor( self, f: T_template_context_processor, ) -> T_template_context_processor: - """Registers a template context processor function.""" + """Registers a template context processor function. These functions run before + rendering a template. The keys of the returned dict are added as variables + available in the template. + + This is available on both app and blueprint objects. When used on an app, this + is called for every rendered template. When used on a blueprint, this is called + for templates rendered from the blueprint's views. To register with a blueprint + and affect every template, use :meth:`.Blueprint.app_context_processor`. + """ self.template_context_processors[None].append(f) return f @@ -635,6 +658,11 @@ def url_value_preprocessor( The function is passed the endpoint name and values dict. The return value is ignored. + + This is available on both app and blueprint objects. When used on an app, this + is called for every request. When used on a blueprint, this is called for + requests that the blueprint handles. To register with a blueprint and affect + every request, use :meth:`.Blueprint.app_url_value_preprocessor`. """ self.url_value_preprocessors[None].append(f) return f @@ -644,6 +672,11 @@ def url_defaults(self, f: T_url_defaults) -> T_url_defaults: """Callback function for URL defaults for all view functions of the application. It's called with the endpoint and values and should update the values passed in place. + + This is available on both app and blueprint objects. When used on an app, this + is called for every request. When used on a blueprint, this is called for + requests that the blueprint handles. To register with a blueprint and affect + every request, use :meth:`.Blueprint.app_url_defaults`. """ self.url_default_functions[None].append(f) return f @@ -667,6 +700,11 @@ def page_not_found(error): def special_exception_handler(error): return 'Database connection failed', 500 + This is available on both app and blueprint objects. When used on an app, this + can handle errors from every request. When used on a blueprint, this can handle + errors from requests that the blueprint handles. To register with a blueprint + and affect every request, use :meth:`.Blueprint.app_errorhandler`. + .. versionadded:: 0.7 Use :meth:`register_error_handler` instead of modifying :attr:`error_handler_spec` directly, for application wide error From ba2b3094d1a8177059ea68853a48fcd5e90920fe Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 10 Feb 2023 10:50:48 -0800 Subject: [PATCH 175/229] fix test client arg for query string example --- docs/reqcontext.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 2b109770e5..70ea13e366 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -69,11 +69,12 @@ everything that runs in the block will have access to :data:`request`, populated with your test data. :: def generate_report(year): - format = request.args.get('format') + format = request.args.get("format") ... with app.test_request_context( - '/make_report/2017', data={'format': 'short'}): + "/make_report/2017", query_string={"format": "short"} + ): generate_report() If you see that error somewhere else in your code not related to From a6cd8f212e762b8f70e00f3341b27799c0fb657a Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 10 Feb 2023 14:48:02 -0800 Subject: [PATCH 176/229] document the lifecycle of a flask application and request --- docs/index.rst | 1 + docs/lifecycle.rst | 168 ++++++++++++++++++++++++++++++++++++++++++++ docs/reqcontext.rst | 5 +- 3 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 docs/lifecycle.rst diff --git a/docs/index.rst b/docs/index.rst index 983f612f3f..747749d053 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -48,6 +48,7 @@ community-maintained extensions to add even more functionality. config signals views + lifecycle appcontext reqcontext blueprints diff --git a/docs/lifecycle.rst b/docs/lifecycle.rst new file mode 100644 index 0000000000..2344d98ad7 --- /dev/null +++ b/docs/lifecycle.rst @@ -0,0 +1,168 @@ +Application Structure and Lifecycle +=================================== + +Flask makes it pretty easy to write a web application. But there are quite a few +different parts to an application and to each request it handles. Knowing what happens +during application setup, serving, and handling requests will help you know what's +possible in Flask and how to structure your application. + + +Application Setup +----------------- + +The first step in creating a Flask application is creating the application object. Each +Flask application is an instance of the :class:`.Flask` class, which collects all +configuration, extensions, and views. + +.. code-block:: python + + from flask import Flask + + app = Flask(__name__) + app.config.from_mapping( + SECRET_KEY="dev", + ) + app.config.from_prefixed_env() + + @app.route("/") + def index(): + return "Hello, World!" + +This is known as the "application setup phase", it's the code you write that's outside +any view functions or other handlers. It can be split up between different modules and +sub-packages, but all code that you want to be part of your application must be imported +in order for it to be registered. + +All application setup must be completed before you start serving your application and +handling requests. This is because WSGI servers divide work between multiple workers, or +can be distributed across multiple machines. If the configuration changed in one worker, +there's no way for Flask to ensure consistency between other workers. + +Flask tries to help developers catch some of these setup ordering issues by showing an +error if setup-related methods are called after requests are handled. In that case +you'll see this error: + + The setup method 'route' can no longer be called on the application. It has already + handled its first request, any changes will not be applied consistently. + Make sure all imports, decorators, functions, etc. needed to set up the application + are done before running it. + +However, it is not possible for Flask to detect all cases of out-of-order setup. In +general, don't do anything to modify the ``Flask`` app object and ``Blueprint`` objects +from within view functions that run during requests. This includes: + +- Adding routes, view functions, and other request handlers with ``@app.route``, + ``@app.errorhandler``, ``@app.before_request``, etc. +- Registering blueprints. +- Loading configuration with ``app.config``. +- Setting up the Jinja template environment with ``app.jinja_env``. +- Setting a session interface, instead of the default itsdangerous cookie. +- Setting a JSON provider with ``app.json``, instead of the default provider. +- Creating and initializing Flask extensions. + + +Serving the Application +----------------------- + +Flask is a WSGI application framework. The other half of WSGI is the WSGI server. During +development, Flask, through Werkzeug, provides a development WSGI server with the +``flask run`` CLI command. When you are done with development, use a production server +to serve your application, see :doc:`deploying/index`. + +Regardless of what server you're using, it will follow the :pep:`3333` WSGI spec. The +WSGI server will be told how to access your Flask application object, which is the WSGI +application. Then it will start listening for HTTP requests, translate the request data +into a WSGI environ, and call the WSGI application with that data. The WSGI application +will return data that is translated into an HTTP response. + +#. Browser or other client makes HTTP request. +#. WSGI server receives request. +#. WSGI server converts HTTP data to WSGI ``environ`` dict. +#. WSGI server calls WSGI application with the ``environ``. +#. Flask, the WSGI application, does all its internal processing to route the request + to a view function, handle errors, etc. +#. Flask translates View function return into WSGI response data, passes it to WSGI + server. +#. WSGI server creates and send an HTTP response. +#. Client receives the HTTP response. + + +Middleware +~~~~~~~~~~ + +The WSGI application above is a callable that behaves in a certain way. Middleware +is a WSGI application that wraps another WSGI application. It's a similar concept to +Python decorators. The outermost middleware will be called by the server. It can modify +the data passed to it, then call the WSGI application (or further middleware) that it +wraps, and so on. And it can take the return value of that call and modify it further. + +From the WSGI server's perspective, there is one WSGI application, the one it calls +directly. Typically, Flask is the "real" application at the end of the chain of +middleware. But even Flask can call further WSGI applications, although that's an +advanced, uncommon use case. + +A common middleware you'll see used with Flask is Werkzeug's +:class:`~werkzeug.middleware.proxy_fix.ProxyFix`, which modifies the request to look +like it came directly from a client even if it passed through HTTP proxies on the way. +There are other middleware that can handle serving static files, authentication, etc. + + +How a Request is Handled +------------------------ + +For us, the interesting part of the steps above is when Flask gets called by the WSGI +server (or middleware). At that point, it will do quite a lot to handle the request and +generate the response. At the most basic, it will match the URL to a view function, call +the view function, and pass the return value back to the server. But there are many more +parts that you can use to customize its behavior. + +#. WSGI server calls the Flask object, which calls :meth:`.Flask.wsgi_app`. +#. A :class:`.RequestContext` object is created. This converts the WSGI ``environ`` + dict into a :class:`.Request` object. It also creates an :class:`AppContext` object. +#. The :doc:`app context ` is pushed, which makes :data:`.current_app` and + :data:`.g` available. +#. The :data:`.appcontext_pushed` signal is sent. +#. The :doc:`request context ` is pushed, which makes :attr:`.request` and + :class:`.session` available. +#. The session is opened, loading any existing session data using the app's + :attr:`~.Flask.session_interface`, an instance of :class:`.SessionInterface`. +#. The URL is matched against the URL rules registered with the :meth:`~.Flask.route` + decorator during application setup. If there is no match, the error - usually a 404, + 405, or redirect - is stored to be handled later. +#. The :data:`.request_started` signal is sent. +#. Any :meth:`~.Flask.url_value_preprocessor` decorated functions are called. +#. Any :meth:`~.Flask.before_request` decorated functions are called. If any of + these function returns a value it is treated as the response immediately. +#. If the URL didn't match a route a few steps ago, that error is raised now. +#. The :meth:`~.Flask.route` decorated view function associated with the matched URL + is called and returns a value to be used as the response. +#. If any step so far raised an exception, and there is an :meth:`~.Flask.errorhandler` + decorated function that matches the exception class or HTTP error code, it is + called to handle the error and return a response. +#. Whatever returned a response value - a before request function, the view, or an + error handler, that value is converted to a :class:`.Response` object. +#. Any :func:`~.after_this_request` decorated functions are called, then cleared. +#. Any :meth:`~.Flask.after_request` decorated functions are called, which can modify + the response object. +#. The session is saved, persisting any modified session data using the app's + :attr:`~.Flask.session_interface`. +#. The :data:`.request_finished` signal is sent. +#. If any step so far raised an exception, and it was not handled by an error handler + function, it is handled now. HTTP exceptions are treated as responses with their + corresponding status code, other exceptions are converted to a generic 500 response. + The :data:`.got_request_exception` signal is sent. +#. The response object's status, headers, and body are returned to the WSGI server. +#. Any :meth:`~.Flask.teardown_request` decorated functions are called. +#. The :data:`.request_tearing_down` signal is sent. +#. The request context is popped, :attr:`.request` and :class:`.session` are no longer + available. +#. Any :meth:`~.Flask.teardown_appcontext` decorated functions are called. +#. The :data:`.appcontext_tearing_down` signal is sent. +#. The app context is popped, :data:`.current_app` and :data:`.g` are no longer + available. +#. The :data:`.appcontext_popped` signal is sent. + +There are even more decorators and customization points than this, but that aren't part +of every request lifecycle. They're more specific to certain things you might use during +a request, such as templates, building URLs, or handling JSON data. See the rest of this +documentation, as well as the :doc:`api` to explore further. diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 2b109770e5..70ea13e366 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -69,11 +69,12 @@ everything that runs in the block will have access to :data:`request`, populated with your test data. :: def generate_report(year): - format = request.args.get('format') + format = request.args.get("format") ... with app.test_request_context( - '/make_report/2017', data={'format': 'short'}): + "/make_report/2017", query_string={"format": "short"} + ): generate_report() If you see that error somewhere else in your code not related to From aa040c085c8c560616069f06c34cf796c10b05f1 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 10 Feb 2023 15:07:24 -0800 Subject: [PATCH 177/229] run latest black format --- src/flask/templating.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/flask/templating.py b/src/flask/templating.py index 25cc3f95e6..edbbe93017 100644 --- a/src/flask/templating.py +++ b/src/flask/templating.py @@ -134,7 +134,7 @@ def _render(app: "Flask", template: Template, context: t.Dict[str, t.Any]) -> st def render_template( template_name_or_list: t.Union[str, Template, t.List[t.Union[str, Template]]], - **context: t.Any + **context: t.Any, ) -> str: """Render a template by name with the given context. @@ -180,7 +180,7 @@ def generate() -> t.Iterator[str]: def stream_template( template_name_or_list: t.Union[str, Template, t.List[t.Union[str, Template]]], - **context: t.Any + **context: t.Any, ) -> t.Iterator[str]: """Render a template by name with the given context as a stream. This returns an iterator of strings, which can be used as a From 24df8fc89d5659d041b91b30a2ada9de49ec2264 Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 15 Feb 2023 14:24:56 -0800 Subject: [PATCH 178/229] show 'run --debug' in docs Reverts commit 4d69165ab6e17fa754139d348cdfd9edacbcb999. Now that a release has this option, it's ok to show it in the docs. It had been reverted because the 2.2.x docs showed it before 2.2.3 was released. --- docs/cli.rst | 12 ++++++++++-- docs/config.rst | 12 ++++++------ docs/debugging.rst | 4 ++-- docs/quickstart.rst | 2 +- docs/server.rst | 2 +- docs/tutorial/factory.rst | 2 +- examples/celery/README.md | 2 +- examples/tutorial/README.rst | 2 +- 8 files changed, 23 insertions(+), 15 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index 22484f1731..be5a0b701b 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -95,7 +95,7 @@ the ``--debug`` option. .. code-block:: console - $ flask --app hello --debug run + $ flask --app hello run --debug * Serving Flask app "hello" * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) @@ -103,6 +103,14 @@ the ``--debug`` option. * Debugger is active! * Debugger PIN: 223-456-919 +The ``--debug`` option can also be passed to the top level ``flask`` command to enable +debug mode for any command. The following two ``run`` calls are equivalent. + +.. code-block:: console + + $ flask --app hello --debug run + $ flask --app hello run --debug + Watch and Ignore Files with the Reloader ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -550,7 +558,7 @@ a name such as "flask run". Click the *Script path* dropdown and change it to *Module name*, then input ``flask``. The *Parameters* field is set to the CLI command to execute along with any arguments. -This example uses ``--app hello --debug run``, which will run the development server in +This example uses ``--app hello run --debug``, which will run the development server in debug mode. ``--app hello`` should be the import or file with your Flask app. If you installed your project as a package in your virtualenv, you may uncheck the diff --git a/docs/config.rst b/docs/config.rst index 7cffc44bb2..9db2045f7a 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -47,17 +47,17 @@ Debug Mode The :data:`DEBUG` config value is special because it may behave inconsistently if changed after the app has begun setting up. In order to set debug mode reliably, use the -``--debug`` option on the ``flask`` command. ``flask run`` will use the interactive -debugger and reloader by default in debug mode. +``--debug`` option on the ``flask`` or ``flask run`` command. ``flask run`` will use the +interactive debugger and reloader by default in debug mode. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug Using the option is recommended. While it is possible to set :data:`DEBUG` in your -config or code, this is strongly discouraged. It can't be read early by the ``flask run`` -command, and some systems or extensions may have already configured themselves based on -a previous value. +config or code, this is strongly discouraged. It can't be read early by the +``flask run`` command, and some systems or extensions may have already configured +themselves based on a previous value. Builtin Configuration Values diff --git a/docs/debugging.rst b/docs/debugging.rst index fb3604b036..18f4286758 100644 --- a/docs/debugging.rst +++ b/docs/debugging.rst @@ -43,7 +43,7 @@ The debugger is enabled by default when the development server is run in debug m .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug When running from Python code, passing ``debug=True`` enables debug mode, which is mostly equivalent. @@ -72,7 +72,7 @@ which can interfere. .. code-block:: text - $ flask --app hello --debug run --no-debugger --no-reload + $ flask --app hello run --debug --no-debugger --no-reload When running from Python: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 02dbc97835..ad9e3bc4e8 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -108,7 +108,7 @@ To enable debug mode, use the ``--debug`` option. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug * Serving Flask app 'hello' * Debug mode: on * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) diff --git a/docs/server.rst b/docs/server.rst index a34dfab5dd..d38aa12089 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -24,7 +24,7 @@ debug mode. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug This enables debug mode, including the interactive debugger and reloader, and then starts the server on http://localhost:5000/. Use ``flask run --help`` to see the diff --git a/docs/tutorial/factory.rst b/docs/tutorial/factory.rst index c8e2c5f4e0..39febd135f 100644 --- a/docs/tutorial/factory.rst +++ b/docs/tutorial/factory.rst @@ -137,7 +137,7 @@ follow the tutorial. .. code-block:: text - $ flask --app flaskr --debug run + $ flask --app flaskr run --debug You'll see output similar to this: diff --git a/examples/celery/README.md b/examples/celery/README.md index 91782019e2..038eb51eb6 100644 --- a/examples/celery/README.md +++ b/examples/celery/README.md @@ -19,7 +19,7 @@ In a separate terminal, activate the virtualenv and run the Flask development se ```shell $ . ./.venv/bin/activate -$ flask -A task_app --debug run +$ flask -A task_app run --debug ``` Go to http://localhost:5000/ and use the forms to submit tasks. You can see the polling diff --git a/examples/tutorial/README.rst b/examples/tutorial/README.rst index a7e12ca250..1c745078bc 100644 --- a/examples/tutorial/README.rst +++ b/examples/tutorial/README.rst @@ -48,7 +48,7 @@ Run .. code-block:: text $ flask --app flaskr init-db - $ flask --app flaskr --debug run + $ flask --app flaskr run --debug Open http://127.0.0.1:5000 in a browser. From 41d4f62909bb426c84e9d057151f7d734695320a Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 15 Feb 2023 14:20:33 -0800 Subject: [PATCH 179/229] release version 2.2.3 --- CHANGES.rst | 6 ++---- src/flask/__init__.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 79e66e956d..cd1a04c489 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,11 +1,9 @@ Version 2.2.3 ------------- -Unreleased +Released 2023-02-15 -- Autoescaping is now enabled by default for ``.svg`` files. Inside - templates this behavior can be changed with the ``autoescape`` tag. - :issue:`4831` +- Autoescape is enabled by default for ``.svg`` template files. :issue:`4831` - Fix the type of ``template_folder`` to accept ``pathlib.Path``. :issue:`4892` - Add ``--debug`` option to the ``flask run`` command. :issue:`4777` diff --git a/src/flask/__init__.py b/src/flask/__init__.py index 4bd5231146..463f55f255 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -42,7 +42,7 @@ from .templating import stream_template as stream_template from .templating import stream_template_string as stream_template_string -__version__ = "2.2.3.dev" +__version__ = "2.2.3" def __getattr__(name): From c4c7f504be222c3aca9062656498ec3c72e2b2ad Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 16 Feb 2023 06:27:25 -0800 Subject: [PATCH 180/229] update dependencies --- requirements/build.txt | 6 +++--- requirements/dev.txt | 28 +++++++++++++--------------- requirements/docs.txt | 20 ++++++++++---------- requirements/tests.txt | 14 ++++++++++---- requirements/typing.txt | 14 +++++++++----- src/flask/app.py | 4 ++-- 6 files changed, 47 insertions(+), 39 deletions(-) diff --git a/requirements/build.txt b/requirements/build.txt index a735b3d0d1..f8a3ce7582 100644 --- a/requirements/build.txt +++ b/requirements/build.txt @@ -5,13 +5,13 @@ # # pip-compile-multi # -build==0.9.0 +build==0.10.0 # via -r requirements/build.in packaging==23.0 # via build -pep517==0.13.0 +pyproject-hooks==1.0.0 # via build tomli==2.0.1 # via # build - # pep517 + # pyproject-hooks diff --git a/requirements/dev.txt b/requirements/dev.txt index 41b2619ce3..ccb1921ab5 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,9 +8,9 @@ -r docs.txt -r tests.txt -r typing.txt -build==0.9.0 +build==0.10.0 # via pip-tools -cachetools==5.2.0 +cachetools==5.3.0 # via tox cfgv==3.3.1 # via pre-commit @@ -24,37 +24,35 @@ colorama==0.4.6 # via tox distlib==0.3.6 # via virtualenv -filelock==3.8.2 +filelock==3.9.0 # via # tox # virtualenv -identify==2.5.11 +identify==2.5.18 # via pre-commit nodeenv==1.7.0 # via pre-commit -pep517==0.13.0 - # via build pip-compile-multi==2.6.1 # via -r requirements/dev.in -pip-tools==6.12.1 +pip-tools==6.12.2 # via pip-compile-multi -platformdirs==2.6.0 +platformdirs==3.0.0 # via # tox # virtualenv -pre-commit==2.20.0 +pre-commit==3.0.4 # via -r requirements/dev.in -pyproject-api==1.2.1 +pyproject-api==1.5.0 # via tox +pyproject-hooks==1.0.0 + # via build pyyaml==6.0 # via pre-commit -toml==0.10.2 - # via pre-commit -toposort==1.7 +toposort==1.9 # via pip-compile-multi -tox==4.0.16 +tox==4.4.5 # via -r requirements/dev.in -virtualenv==20.17.1 +virtualenv==20.19.0 # via # pre-commit # tox diff --git a/requirements/docs.txt b/requirements/docs.txt index b1e46bde86..5c0568ca65 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -5,13 +5,13 @@ # # pip-compile-multi # -alabaster==0.7.12 +alabaster==0.7.13 # via sphinx babel==2.11.0 # via sphinx certifi==2022.12.7 # via requests -charset-normalizer==2.1.1 +charset-normalizer==3.0.1 # via requests docutils==0.17.1 # via @@ -23,21 +23,21 @@ imagesize==1.4.1 # via sphinx jinja2==3.1.2 # via sphinx -markupsafe==2.1.1 +markupsafe==2.1.2 # via jinja2 -packaging==22.0 +packaging==23.0 # via # pallets-sphinx-themes # sphinx pallets-sphinx-themes==2.0.3 # via -r requirements/docs.in -pygments==2.13.0 +pygments==2.14.0 # via # sphinx # sphinx-tabs -pytz==2022.7 +pytz==2022.7.1 # via babel -requests==2.28.1 +requests==2.28.2 # via sphinx snowballstemmer==2.2.0 # via sphinx @@ -52,11 +52,11 @@ sphinx-issues==3.0.1 # via -r requirements/docs.in sphinx-tabs==3.3.1 # via -r requirements/docs.in -sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-applehelp==1.0.4 # via sphinx sphinxcontrib-devhelp==1.0.2 # via sphinx -sphinxcontrib-htmlhelp==2.0.0 +sphinxcontrib-htmlhelp==2.0.1 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx @@ -66,5 +66,5 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -urllib3==1.26.13 +urllib3==1.26.14 # via requests diff --git a/requirements/tests.txt b/requirements/tests.txt index aff42de283..15cf399db8 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -11,13 +11,19 @@ attrs==22.2.0 # via pytest blinker==1.5 # via -r requirements/tests.in -iniconfig==1.1.1 +exceptiongroup==1.1.0 # via pytest -packaging==22.0 +greenlet==2.0.2 ; python_version < "3.11" + # via -r requirements/tests.in +iniconfig==2.0.0 + # via pytest +packaging==23.0 # via pytest pluggy==1.0.0 # via pytest -pytest==7.2.0 +pytest==7.2.1 # via -r requirements/tests.in -python-dotenv==0.21.0 +python-dotenv==0.21.1 # via -r requirements/tests.in +tomli==2.0.1 + # via pytest diff --git a/requirements/typing.txt b/requirements/typing.txt index ad8dd594df..fbabf5087b 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -7,19 +7,23 @@ # cffi==1.15.1 # via cryptography -cryptography==38.0.4 +cryptography==39.0.1 # via -r requirements/typing.in -mypy==0.991 +mypy==1.0.0 # via -r requirements/typing.in -mypy-extensions==0.4.3 +mypy-extensions==1.0.0 # via mypy pycparser==2.21 # via cffi +tomli==2.0.1 + # via mypy types-contextvars==2.4.7 # via -r requirements/typing.in types-dataclasses==0.6.6 # via -r requirements/typing.in -types-setuptools==65.6.0.2 +types-docutils==0.19.1.4 + # via types-setuptools +types-setuptools==67.3.0.1 # via -r requirements/typing.in -typing-extensions==4.4.0 +typing-extensions==4.5.0 # via mypy diff --git a/src/flask/app.py b/src/flask/app.py index 0ac4bbb5ae..ff6b097382 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1248,7 +1248,7 @@ def __init__(self, *args, **kwargs): """ cls = self.test_client_class if cls is None: - from .testing import FlaskClient as cls # type: ignore + from .testing import FlaskClient as cls return cls( # type: ignore self, self.response_class, use_cookies=use_cookies, **kwargs ) @@ -1266,7 +1266,7 @@ def test_cli_runner(self, **kwargs: t.Any) -> "FlaskCliRunner": cls = self.test_cli_runner_class if cls is None: - from .testing import FlaskCliRunner as cls # type: ignore + from .testing import FlaskCliRunner as cls return cls(self, **kwargs) # type: ignore From 6650764e9719402de2aaa6f321bdec587699c6b2 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 23 Feb 2023 08:35:16 -0800 Subject: [PATCH 181/229] remove previously deprecated code --- CHANGES.rst | 18 ++ docs/api.rst | 6 - docs/config.rst | 69 +------- src/flask/__init__.py | 4 +- src/flask/app.py | 340 +------------------------------------ src/flask/blueprints.py | 106 +----------- src/flask/globals.py | 29 +--- src/flask/helpers.py | 35 +--- src/flask/json/__init__.py | 220 +++--------------------- src/flask/json/provider.py | 104 +----------- src/flask/scaffold.py | 15 -- tests/conftest.py | 1 - tests/test_async.py | 9 - tests/test_basic.py | 39 ----- tests/test_blueprints.py | 10 +- tests/test_helpers.py | 22 +-- 16 files changed, 81 insertions(+), 946 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0b8d2cfde7..64d5770cbb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,24 @@ Version 2.3.0 Unreleased +- Remove previously deprecated code. :pr:`4995` + + - The ``push`` and ``pop`` methods of the deprecated ``_app_ctx_stack`` and + ``_request_ctx_stack`` objects are removed. ``top`` still exists to give + extensions more time to update, but it will be removed. + - The ``FLASK_ENV`` environment variable, ``ENV`` config key, and ``app.env`` + property are removed. + - The ``session_cookie_name``, ``send_file_max_age_default``, ``use_x_sendfile``, + ``propagate_exceptions``, and ``templates_auto_reload`` properties on ``app`` + are removed. + - The ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_MIMETYPE``, and + ``JSONIFY_PRETTYPRINT_REGULAR`` config keys are removed. + - The ``app.before_first_request`` and ``bp.before_app_first_request`` decorators + are removed. + - ``json_encoder`` and ``json_decoder`` attributes on app and blueprint, and the + corresponding ``json.JSONEncoder`` and ``JSONDecoder`` classes, are removed. + - The ``json.htmlsafe_dumps`` and ``htmlsafe_dump`` functions are removed. + - Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``. :pr:`4947` - Ensure subdomains are applied with nested blueprints. :issue:`4834` diff --git a/docs/api.rst b/docs/api.rst index afbe0b79e6..bf37700f2e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -270,12 +270,6 @@ HTML ``