From 9d9108fe25600c566643f902255d74ff68d88967 Mon Sep 17 00:00:00 2001 From: Kevin Kirsche Date: Tue, 14 Sep 2021 12:29:42 -0400 Subject: [PATCH 01/12] fix: typo docs/debugging.rst:72 docs/debugging.rst:72: controled ==> controlled --- docs/debugging.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/debugging.rst b/docs/debugging.rst index a9f984b4..66118de2 100644 --- a/docs/debugging.rst +++ b/docs/debugging.rst @@ -69,7 +69,7 @@ enables the debugger and reloader. ``FLASK_ENV`` can only 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 controled separately from +mostly equivalent. Debug mode can be controlled separately from ``FLASK_ENV`` with the ``FLASK_DEBUG`` environment variable as well. .. code-block:: python From 6a4bf9eec13e035cf10ecc16e462049b4c967e41 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 1 Oct 2021 09:39:10 -0700 Subject: [PATCH 02/12] use exception chaining fixes flake8-bugbear B904 --- src/flask/app.py | 4 ++-- src/flask/cli.py | 28 ++++++++++++++-------------- src/flask/debughelpers.py | 5 +++-- src/flask/scaffold.py | 2 +- src/flask/signals.py | 2 +- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 301694ac..9097c464 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1621,7 +1621,7 @@ class Flask(Scaffold): except ImportError: raise RuntimeError( "Install Flask with the 'async' extra in order to use async views." - ) + ) from None # Check that Werkzeug isn't using its fallback ContextVar class. if ContextVar.__module__ == "werkzeug.local": @@ -1727,7 +1727,7 @@ class Flask(Scaffold): " response. The return type must be a string," " dict, tuple, Response instance, or WSGI" f" callable, but it was a {type(rv).__name__}." - ).with_traceback(sys.exc_info()[2]) + ).with_traceback(sys.exc_info()[2]) from None else: raise TypeError( "The view function did not return a valid" diff --git a/src/flask/cli.py b/src/flask/cli.py index 0d101d0a..7ab4fa1c 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -69,15 +69,16 @@ def find_best_app(script_info, module): if isinstance(app, Flask): return app - except TypeError: + except TypeError as e: if not _called_with_wrong_args(app_factory): raise + raise NoAppException( f"Detected factory {attr_name!r} in module {module.__name__!r}," " but could not call it without arguments. Use" f" \"FLASK_APP='{module.__name__}:{attr_name}(args)'\"" " to specify arguments." - ) + ) from e raise NoAppException( "Failed to find Flask application or factory in module" @@ -161,7 +162,7 @@ def find_app_by_string(script_info, module, app_name): except SyntaxError: raise NoAppException( f"Failed to parse {app_name!r} as an attribute name or function call." - ) + ) from None if isinstance(expr, ast.Name): name = expr.id @@ -184,7 +185,7 @@ def find_app_by_string(script_info, module, app_name): # message with the full expression instead. raise NoAppException( f"Failed to parse arguments as literal values: {app_name!r}." - ) + ) from None else: raise NoAppException( f"Failed to parse {app_name!r} as an attribute name or function call." @@ -192,17 +193,17 @@ def find_app_by_string(script_info, module, app_name): try: attr = getattr(module, name) - except AttributeError: + except AttributeError as e: raise NoAppException( f"Failed to find attribute {name!r} in {module.__name__!r}." - ) + ) from e # If the attribute is a function, call it with any args and kwargs # to get the real application. if inspect.isfunction(attr): try: app = call_factory(script_info, attr, args, kwargs) - except TypeError: + except TypeError as e: if not _called_with_wrong_args(attr): raise @@ -210,7 +211,7 @@ def find_app_by_string(script_info, module, app_name): f"The factory {app_name!r} in module" f" {module.__name__!r} could not be called with the" " specified arguments." - ) + ) from e else: app = attr @@ -257,16 +258,15 @@ def locate_app(script_info, module_name, app_name, raise_if_not_found=True): try: __import__(module_name) - except ImportError: + except ImportError as e: # Reraise the ImportError if it occurred within the imported module. # Determine this by checking whether the trace has a depth > 1. if sys.exc_info()[2].tb_next: raise NoAppException( - f"While importing {module_name!r}, an ImportError was" - f" raised:\n\n{traceback.format_exc()}" - ) + f"While importing {module_name!r}, an ImportError was raised." + ) from e elif raise_if_not_found: - raise NoAppException(f"Could not import {module_name!r}.") + raise NoAppException(f"Could not import {module_name!r}.") from e else: return @@ -725,7 +725,7 @@ class CertParamType(click.ParamType): "Using ad-hoc certificates requires the cryptography library.", ctx, param, - ) + ) from None return value diff --git a/src/flask/debughelpers.py b/src/flask/debughelpers.py index ce65c487..212f7d7e 100644 --- a/src/flask/debughelpers.py +++ b/src/flask/debughelpers.py @@ -83,10 +83,11 @@ def attach_enctype_error_multidict(request): def __getitem__(self, key): try: return oldcls.__getitem__(self, key) - except KeyError: + except KeyError as e: if key not in request.form: raise - raise DebugFilesKeyError(request, key) + + raise DebugFilesKeyError(request, key) from e newcls.__name__ = oldcls.__name__ newcls.__module__ = oldcls.__module__ diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 42eabcfc..07ca5c65 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -715,7 +715,7 @@ class Scaffold: f"'{code_or_exception}' is not a recognized HTTP error" " code. Use a subclass of HTTPException with that code" " instead." - ) + ) from None self.error_handler_spec[None][code][exc_class] = t.cast( "ErrorHandlerCallable[Exception]", f diff --git a/src/flask/signals.py b/src/flask/signals.py index 63667bdb..2c6d6469 100644 --- a/src/flask/signals.py +++ b/src/flask/signals.py @@ -29,7 +29,7 @@ except ImportError: raise RuntimeError( "Signalling support is unavailable because the blinker" " library is not installed." - ) + ) from None connect = connect_via = connected_to = temporarily_connected_to = _fail disconnect = _fail From 42a6da2da38af65a0f097bb88e3e2bee8f137222 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 1 Oct 2021 09:46:38 -0700 Subject: [PATCH 03/12] update requirements --- .pre-commit-config.yaml | 4 ++-- requirements/dev.txt | 26 +++++++++++++------------- requirements/docs.txt | 6 +++--- requirements/tests.txt | 2 +- requirements/typing.txt | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b648e3f0..3ef408f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ ci: autoupdate_schedule: monthly repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.25.0 + rev: v2.29.0 hooks: - id: pyupgrade args: ["--py36-plus"] @@ -14,7 +14,7 @@ repos: files: "^(?!examples/)" args: ["--application-directories", "src"] - repo: https://github.com/psf/black - rev: 21.8b0 + rev: 21.9b0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 diff --git a/requirements/dev.txt b/requirements/dev.txt index 4962c2ac..19f50095 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -22,25 +22,25 @@ cffi==1.14.6 # via cryptography cfgv==3.3.1 # via pre-commit -charset-normalizer==2.0.4 +charset-normalizer==2.0.6 # via requests click==8.0.1 # via pip-tools -cryptography==3.4.8 +cryptography==35.0.0 # via -r requirements/typing.in -distlib==0.3.2 +distlib==0.3.3 # via virtualenv docutils==0.16 # via # sphinx # sphinx-tabs -filelock==3.0.12 +filelock==3.2.0 # via # tox # virtualenv -greenlet==1.1.1 +greenlet==1.1.2 # via -r requirements/tests.in -identify==2.2.13 +identify==2.2.15 # via pre-commit idna==3.2 # via requests @@ -68,9 +68,9 @@ pallets-sphinx-themes==2.0.1 # via -r requirements/docs.in pep517==0.11.0 # via pip-tools -pip-tools==6.2.0 +pip-tools==6.3.0 # via -r requirements/dev.in -platformdirs==2.3.0 +platformdirs==2.4.0 # via virtualenv pluggy==1.0.0 # via @@ -106,7 +106,7 @@ six==1.16.0 # virtualenv snowballstemmer==2.1.0 # via sphinx -sphinx==4.1.2 +sphinx==4.2.0 # via # -r requirements/docs.in # pallets-sphinx-themes @@ -139,19 +139,19 @@ toml==0.10.2 # tox tomli==1.2.1 # via pep517 -tox==3.24.3 +tox==3.24.4 # via -r requirements/dev.in types-contextvars==0.1.4 # via -r requirements/typing.in types-dataclasses==0.1.7 # via -r requirements/typing.in -types-setuptools==57.0.2 +types-setuptools==57.4.0 # via -r requirements/typing.in typing-extensions==3.10.0.2 # via mypy -urllib3==1.26.6 +urllib3==1.26.7 # via requests -virtualenv==20.7.2 +virtualenv==20.8.1 # via # pre-commit # tox diff --git a/requirements/docs.txt b/requirements/docs.txt index a11b7d3c..43b83f93 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -10,7 +10,7 @@ babel==2.9.1 # via sphinx certifi==2021.5.30 # via requests -charset-normalizer==2.0.4 +charset-normalizer==2.0.6 # via requests docutils==0.16 # via @@ -42,7 +42,7 @@ requests==2.26.0 # via sphinx snowballstemmer==2.1.0 # via sphinx -sphinx==4.1.2 +sphinx==4.2.0 # via # -r requirements/docs.in # pallets-sphinx-themes @@ -67,7 +67,7 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -urllib3==1.26.6 +urllib3==1.26.7 # via requests # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/tests.txt b/requirements/tests.txt index 66bc443d..11889378 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -10,7 +10,7 @@ attrs==21.2.0 # via pytest blinker==1.4 # via -r requirements/tests.in -greenlet==1.1.1 +greenlet==1.1.2 # via -r requirements/tests.in iniconfig==1.1.1 # via pytest diff --git a/requirements/typing.txt b/requirements/typing.txt index 106a0a21..724aa2a2 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -6,7 +6,7 @@ # cffi==1.14.6 # via cryptography -cryptography==3.4.8 +cryptography==35.0.0 # via -r requirements/typing.in mypy==0.910 # via -r requirements/typing.in @@ -20,7 +20,7 @@ types-contextvars==0.1.4 # via -r requirements/typing.in types-dataclasses==0.1.7 # via -r requirements/typing.in -types-setuptools==57.0.2 +types-setuptools==57.4.0 # via -r requirements/typing.in typing-extensions==3.10.0.2 # via mypy From 3a78f501e9eadd9225a00194aefc54dbdca1df74 Mon Sep 17 00:00:00 2001 From: Kasper Primdal Lauritzen Date: Mon, 27 Sep 2021 10:45:29 +0200 Subject: [PATCH 04/12] Fix broken link to Flask-WTF --- docs/patterns/wtforms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index e5fd500d..3d626f50 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -19,7 +19,7 @@ forms. fun. You can get it from `PyPI `_. -.. _Flask-WTF: https://flask-wtf.readthedocs.io/en/stable/ +.. _Flask-WTF: https://flask-wtf.readthedocs.io/ The Forms --------- From 22933a8cb424ffd65f2bec5bbfdbfa7e42105470 Mon Sep 17 00:00:00 2001 From: Pedro Torcatt Date: Wed, 22 Sep 2021 21:20:26 -0400 Subject: [PATCH 05/12] fix docs for Flask.test_client_class --- src/flask/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/flask/app.py b/src/flask/app.py index 9097c464..5179305a 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -366,7 +366,8 @@ class Flask(Scaffold): #: .. versionadded:: 1.1.0 url_map_class = Map - #: the test client that is used with when `test_client` is used. + #: The :meth:`test_client` method creates an instance of this test + #: client class. Defaults to :class:`~flask.testing.FlaskClient`. #: #: .. versionadded:: 0.7 test_client_class: t.Optional[t.Type["FlaskClient"]] = None From 1a40d9b9761b304150c3b82c656dbb0e425e8e2e Mon Sep 17 00:00:00 2001 From: Seth Rutner Date: Tue, 21 Sep 2021 15:44:39 -0700 Subject: [PATCH 06/12] fix grammar in links to app and request context --- docs/appcontext.rst | 2 +- docs/reqcontext.rst | 6 +++--- docs/shell.rst | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/appcontext.rst b/docs/appcontext.rst index 68176494..b214f254 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -8,7 +8,7 @@ a request, CLI command, or other activity. Rather than passing the application around to each function, the :data:`current_app` and :data:`g` proxies are accessed instead. -This is similar to the :doc:`/reqcontext`, which keeps track of +This is similar to :doc:`/reqcontext`, which keeps track of request-level data during a request. A corresponding application context is pushed when a request context is pushed. diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 31a83cff..b67745ed 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -8,7 +8,7 @@ request. Rather than passing the request object to each function that runs during a request, the :data:`request` and :data:`session` proxies are accessed instead. -This is similar to the :doc:`/appcontext`, which keeps track of the +This is similar to :doc:`/appcontext`, which keeps track of the application-level data independent of a request. A corresponding application context is pushed when a request context is pushed. @@ -33,8 +33,8 @@ Lifetime of the Context ----------------------- When a Flask application begins handling a request, it pushes a request -context, which also pushes an :doc:`/appcontext`. When the request ends -it pops the request context then the application context. +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 diff --git a/docs/shell.rst b/docs/shell.rst index 47efba37..7e42e285 100644 --- a/docs/shell.rst +++ b/docs/shell.rst @@ -21,8 +21,7 @@ that these functions are not only there for interactive shell usage, but also for unit testing and other situations that require a faked request context. -Generally it's recommended that you read the :doc:`reqcontext` -chapter of the documentation first. +Generally it's recommended that you read :doc:`reqcontext` first. Command Line Interface ---------------------- From 76858515944121ae3faf699f85ab2c4b7806d3d1 Mon Sep 17 00:00:00 2001 From: Makonede <61922615+Makonede@users.noreply.github.com> Date: Fri, 1 Oct 2021 18:07:32 -0700 Subject: [PATCH 07/12] fix list numbering --- artwork/LICENSE.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artwork/LICENSE.rst b/artwork/LICENSE.rst index 605e41cb..99c58a21 100644 --- a/artwork/LICENSE.rst +++ b/artwork/LICENSE.rst @@ -10,7 +10,7 @@ following conditions are met: 1. Redistributions of source code must retain the above copyright notice and this list of conditions. -3. Neither the name of the copyright holder nor the names of its +2. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. From 166a2a6207027bff07fdfc5590ce04f9b37e9e8f Mon Sep 17 00:00:00 2001 From: Matthias Paulsen Date: Tue, 10 Aug 2021 00:23:57 +0200 Subject: [PATCH 08/12] Fix callback order for nested blueprints Handlers registered via url_value_preprocessor, before_request, context_processor, and url_defaults are called in downward order: First on the app and last on the current blueprint. Handlers registered via after_request and teardown_request are called in upward order: First on the current blueprint and last on the app. --- CHANGES.rst | 3 ++ src/flask/app.py | 48 +++++++++++------------- tests/test_blueprints.py | 80 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 26 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index eeba61ab..1b4551ab 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,9 @@ Unreleased :issue:`4096` - The CLI loader handles ``**kwargs`` in a ``create_app`` function. :issue:`4170` +- Fix the order of ``before_request`` and other callbacks that trigger + before the view returns. They are called from the app down to the + closest nested blueprint. :issue:`4229` Version 2.0.1 diff --git a/src/flask/app.py b/src/flask/app.py index 5179305a..a9bdba65 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -745,12 +745,12 @@ class Flask(Scaffold): :param context: the context as a dictionary that is updated in place to add extra variables. """ - funcs: t.Iterable[ - TemplateContextProcessorCallable - ] = self.template_context_processors[None] + funcs: t.Iterable[TemplateContextProcessorCallable] = [] + if None in self.template_context_processors: + funcs = chain(funcs, self.template_context_processors[None]) reqctx = _request_ctx_stack.top if reqctx is not None: - for bp in request.blueprints: + for bp in reversed(request.blueprints): if bp in self.template_context_processors: funcs = chain(funcs, self.template_context_processors[bp]) orig_ctx = context.copy() @@ -1806,7 +1806,9 @@ class Flask(Scaffold): # This is called by url_for, which can be called outside a # request, can't use request.blueprints. bps = _split_blueprint_path(endpoint.rpartition(".")[0]) - bp_funcs = chain.from_iterable(self.url_default_functions[bp] for bp in bps) + bp_funcs = chain.from_iterable( + self.url_default_functions[bp] for bp in reversed(bps) + ) funcs = chain(funcs, bp_funcs) for func in funcs: @@ -1846,19 +1848,17 @@ class Flask(Scaffold): further request handling is stopped. """ - funcs: t.Iterable[URLValuePreprocessorCallable] = self.url_value_preprocessors[ - None - ] - for bp in request.blueprints: - if bp in self.url_value_preprocessors: - funcs = chain(funcs, self.url_value_preprocessors[bp]) + funcs: t.Iterable[URLValuePreprocessorCallable] = [] + for name in chain([None], reversed(request.blueprints)): + if name in self.url_value_preprocessors: + funcs = chain(funcs, self.url_value_preprocessors[name]) for func in funcs: func(request.endpoint, request.view_args) - funcs: t.Iterable[BeforeRequestCallable] = self.before_request_funcs[None] - for bp in request.blueprints: - if bp in self.before_request_funcs: - funcs = chain(funcs, self.before_request_funcs[bp]) + funcs: t.Iterable[BeforeRequestCallable] = [] + for name in chain([None], reversed(request.blueprints)): + if name in self.before_request_funcs: + funcs = chain(funcs, self.before_request_funcs[name]) for func in funcs: rv = self.ensure_sync(func)() if rv is not None: @@ -1881,11 +1881,9 @@ class Flask(Scaffold): """ ctx = _request_ctx_stack.top funcs: t.Iterable[AfterRequestCallable] = ctx._after_request_functions - for bp in request.blueprints: - if bp in self.after_request_funcs: - funcs = chain(funcs, reversed(self.after_request_funcs[bp])) - if None in self.after_request_funcs: - funcs = chain(funcs, reversed(self.after_request_funcs[None])) + for name in chain(request.blueprints, [None]): + if name in self.after_request_funcs: + funcs = chain(funcs, reversed(self.after_request_funcs[name])) for handler in funcs: response = self.ensure_sync(handler)(response) if not self.session_interface.is_null_session(ctx.session): @@ -1917,12 +1915,10 @@ class Flask(Scaffold): """ if exc is _sentinel: exc = sys.exc_info()[1] - funcs: t.Iterable[TeardownCallable] = reversed( - self.teardown_request_funcs[None] - ) - for bp in request.blueprints: - if bp in self.teardown_request_funcs: - funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) + funcs: t.Iterable[TeardownCallable] = [] + for name in chain(request.blueprints, [None]): + if name in self.teardown_request_funcs: + funcs = chain(funcs, reversed(self.teardown_request_funcs[name])) for func in funcs: self.ensure_sync(func)(exc) request_tearing_down.send(self, exc=exc) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index a124c612..e02cd4be 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -837,6 +837,86 @@ def test_nested_blueprint(app, client): assert client.get("/parent/child/grandchild/no").data == b"Grandchild no" +def test_nested_callback_order(app, client): + parent = flask.Blueprint("parent", __name__) + child = flask.Blueprint("child", __name__) + + @app.before_request + def app_before1(): + flask.g.setdefault("seen", []).append("app_1") + + @app.teardown_request + def app_teardown1(e=None): + assert flask.g.seen.pop() == "app_1" + + @app.before_request + def app_before2(): + flask.g.setdefault("seen", []).append("app_2") + + @app.teardown_request + def app_teardown2(e=None): + assert flask.g.seen.pop() == "app_2" + + @app.context_processor + def app_ctx(): + return dict(key="app") + + @parent.before_request + def parent_before1(): + flask.g.setdefault("seen", []).append("parent_1") + + @parent.teardown_request + def parent_teardown1(e=None): + assert flask.g.seen.pop() == "parent_1" + + @parent.before_request + def parent_before2(): + flask.g.setdefault("seen", []).append("parent_2") + + @parent.teardown_request + def parent_teardown2(e=None): + assert flask.g.seen.pop() == "parent_2" + + @parent.context_processor + def parent_ctx(): + return dict(key="parent") + + @child.before_request + def child_before1(): + flask.g.setdefault("seen", []).append("child_1") + + @child.teardown_request + def child_teardown1(e=None): + assert flask.g.seen.pop() == "child_1" + + @child.before_request + def child_before2(): + flask.g.setdefault("seen", []).append("child_2") + + @child.teardown_request + def child_teardown2(e=None): + assert flask.g.seen.pop() == "child_2" + + @child.context_processor + def child_ctx(): + return dict(key="child") + + @child.route("/a") + def a(): + return ", ".join(flask.g.seen) + + @child.route("/b") + def b(): + return flask.render_template_string("{{ key }}") + + parent.register_blueprint(child) + app.register_blueprint(parent) + assert ( + client.get("/a").data == b"app_1, app_2, parent_1, parent_2, child_1, child_2" + ) + assert client.get("/b").data == b"child" + + @pytest.mark.parametrize( "parent_init, child_init, parent_registration, child_registration", [ From 3f6cdbd8b35f2b887dddbdac5ef6461833b6dd3b Mon Sep 17 00:00:00 2001 From: David Lord Date: Sun, 3 Oct 2021 20:19:33 -0700 Subject: [PATCH 09/12] use similar code for all callback-applying methods avoid building nested chain iterables avoid triggering defaultdict when looking up registries apply functions as they are looked up --- src/flask/app.py | 103 ++++++++++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index a9bdba65..23b99e2c 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -58,17 +58,12 @@ from .signals import request_started from .signals import request_tearing_down from .templating import DispatchingJinjaLoader from .templating import Environment -from .typing import AfterRequestCallable from .typing import BeforeFirstRequestCallable -from .typing import BeforeRequestCallable from .typing import ResponseReturnValue 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 from .wrappers import Request from .wrappers import Response @@ -745,20 +740,21 @@ class Flask(Scaffold): :param context: the context as a dictionary that is updated in place to add extra variables. """ - funcs: t.Iterable[TemplateContextProcessorCallable] = [] - if None in self.template_context_processors: - funcs = chain(funcs, self.template_context_processors[None]) - reqctx = _request_ctx_stack.top - if reqctx is not None: - for bp in reversed(request.blueprints): - if bp in self.template_context_processors: - funcs = chain(funcs, self.template_context_processors[bp]) + names: t.Iterable[t.Optional[str]] = (None,) + + # A template may be rendered outside a request context. + if request: + names = chain(names, reversed(request.blueprints)) + + # The values passed to render_template take precedence. Keep a + # copy to re-apply after all context functions. orig_ctx = context.copy() - for func in funcs: - context.update(func()) - # make sure the original values win. This makes it possible to - # easier add new variables in context processors without breaking - # existing views. + + for name in names: + if name in self.template_context_processors: + for func in self.template_context_processors[name]: + context.update(func()) + context.update(orig_ctx) def make_shell_context(self) -> dict: @@ -1278,9 +1274,10 @@ class Flask(Scaffold): class, or ``None`` if a suitable handler is not found. """ exc_class, code = self._get_exc_class_and_code(type(e)) + names = (*request.blueprints, None) - for c in [code, None] if code is not None else [None]: - for name in chain(request.blueprints, [None]): + for c in (code, None) if code is not None else (None,): + for name in names: handler_map = self.error_handler_spec[name][c] if not handler_map: @@ -1800,19 +1797,19 @@ class Flask(Scaffold): .. versionadded:: 0.7 """ - funcs: t.Iterable[URLDefaultCallable] = self.url_default_functions[None] + names: t.Iterable[t.Optional[str]] = (None,) + # url_for may be called outside a request context, parse the + # passed endpoint instead of using request.blueprints. if "." in endpoint: - # This is called by url_for, which can be called outside a - # request, can't use request.blueprints. - bps = _split_blueprint_path(endpoint.rpartition(".")[0]) - bp_funcs = chain.from_iterable( - self.url_default_functions[bp] for bp in reversed(bps) + names = chain( + names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0])) ) - funcs = chain(funcs, bp_funcs) - for func in funcs: - func(endpoint, values) + for name in names: + if name in self.url_default_functions: + for func in self.url_default_functions[name]: + func(endpoint, values) def handle_url_build_error( self, error: Exception, endpoint: str, values: dict @@ -1847,22 +1844,20 @@ class Flask(Scaffold): value is handled as if it was the return value from the view, and further request handling is stopped. """ + names = (None, *reversed(request.blueprints)) - funcs: t.Iterable[URLValuePreprocessorCallable] = [] - for name in chain([None], reversed(request.blueprints)): + for name in names: if name in self.url_value_preprocessors: - funcs = chain(funcs, self.url_value_preprocessors[name]) - for func in funcs: - func(request.endpoint, request.view_args) + for url_func in self.url_value_preprocessors[name]: + url_func(request.endpoint, request.view_args) - funcs: t.Iterable[BeforeRequestCallable] = [] - for name in chain([None], reversed(request.blueprints)): + for name in names: if name in self.before_request_funcs: - funcs = chain(funcs, self.before_request_funcs[name]) - for func in funcs: - rv = self.ensure_sync(func)() - if rv is not None: - return rv + for before_func in self.before_request_funcs[name]: + rv = self.ensure_sync(before_func)() + + if rv is not None: + return rv return None @@ -1880,14 +1875,18 @@ class Flask(Scaffold): instance of :attr:`response_class`. """ ctx = _request_ctx_stack.top - funcs: t.Iterable[AfterRequestCallable] = ctx._after_request_functions - for name in chain(request.blueprints, [None]): + + for func in ctx._after_request_functions: + response = self.ensure_sync(func)(response) + + for name in chain(request.blueprints, (None,)): if name in self.after_request_funcs: - funcs = chain(funcs, reversed(self.after_request_funcs[name])) - for handler in funcs: - response = self.ensure_sync(handler)(response) + for func in reversed(self.after_request_funcs[name]): + response = self.ensure_sync(func)(response) + if not self.session_interface.is_null_session(ctx.session): self.session_interface.save_session(self, ctx.session, response) + return response def do_teardown_request( @@ -1915,12 +1914,12 @@ class Flask(Scaffold): """ if exc is _sentinel: exc = sys.exc_info()[1] - funcs: t.Iterable[TeardownCallable] = [] - for name in chain(request.blueprints, [None]): + + for name in chain(request.blueprints, (None,)): if name in self.teardown_request_funcs: - funcs = chain(funcs, reversed(self.teardown_request_funcs[name])) - for func in funcs: - self.ensure_sync(func)(exc) + for func in reversed(self.teardown_request_funcs[name]): + self.ensure_sync(func)(exc) + request_tearing_down.send(self, exc=exc) def do_teardown_appcontext( @@ -1942,8 +1941,10 @@ class Flask(Scaffold): """ if exc is _sentinel: exc = sys.exc_info()[1] + for func in reversed(self.teardown_appcontext_funcs): self.ensure_sync(func)(exc) + appcontext_tearing_down.send(self, exc=exc) def app_context(self) -> AppContext: From 174fe4453a1a52069d31c9ff4e24f3ef4aa6913e Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 4 Oct 2021 07:26:47 -0700 Subject: [PATCH 10/12] release version 2.0.2 --- CHANGES.rst | 2 +- src/flask/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1b4551ab..4fec7c54 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,7 +3,7 @@ Version 2.0.2 ------------- -Unreleased +Released 2021-10-04 - Fix type annotation for ``teardown_*`` methods. :issue:`4093` - Fix type annotation for ``before_request`` and ``before_app_request`` diff --git a/src/flask/__init__.py b/src/flask/__init__.py index 471ba7a6..43b54683 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -43,4 +43,4 @@ 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 -__version__ = "2.0.2.dev0" +__version__ = "2.0.2" From 6d65595a3c0a02d098c36f7d337e053e07975316 Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 5 Oct 2021 08:03:30 -0700 Subject: [PATCH 11/12] try to address flakiness of lazy loading test --- tests/test_cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index e7462bde..9c51ffe9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -334,7 +334,9 @@ def test_lazy_load_error(monkeypatch): lazy = DispatchingApp(bad_load, use_eager_loading=False) with pytest.raises(BadExc): - lazy._flush_bg_loading_exception() + # reduce flakiness by waiting for the internal loading lock + with lazy._lock: + lazy._flush_bg_loading_exception() def test_with_appcontext(runner): From b2b60450f7b9fae08bbd22f771f7b973aee6a135 Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 5 Oct 2021 09:11:00 -0700 Subject: [PATCH 12/12] allow lazy loading test to fail on pypy --- tests/test_cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index 9c51ffe9..08ba4800 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,7 @@ # This file was part of Flask-CLI and was modified under the terms of # its Revised BSD License. Copyright © 2015 CERN. import os +import platform import ssl import sys import types @@ -320,6 +321,7 @@ def test_scriptinfo(test_apps, monkeypatch): assert app.name == "testapp" +@pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="flaky on pypy") def test_lazy_load_error(monkeypatch): """When using lazy loading, the correct exception should be re-raised.