Skip to content

Commit 70d04b5

Browse files
authored
pass context through dispatch methods (#5818)
2 parents 88a65bb + 6a64969 commit 70d04b5

6 files changed

Lines changed: 167 additions & 72 deletions

File tree

CHANGES.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ Unreleased
99
a deprecated alias. If an app context is already pushed, it is not reused
1010
when dispatching a request. This greatly simplifies the internal code for tracking
1111
the active context. :issue:`5639`
12+
- Many ``Flask`` methods involved in request dispatch now take the current
13+
``AppContext`` as the first parameter, instead of using the proxy objects.
14+
If subclasses were overriding these methods, the old signature is detected,
15+
shows a deprecation warning, and will continue to work during the
16+
deprecation period. :issue:`5815`
1217
- ``template_filter``, ``template_test``, and ``template_global`` decorators
1318
can be used without parentheses. :issue:`5729`
1419

src/flask/app.py

Lines changed: 129 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from __future__ import annotations
22

33
import collections.abc as cabc
4+
import inspect
45
import os
56
import sys
67
import typing as t
78
import weakref
89
from datetime import timedelta
10+
from functools import update_wrapper
911
from inspect import iscoroutinefunction
1012
from itertools import chain
1113
from types import TracebackType
@@ -30,6 +32,7 @@
3032
from . import typing as ft
3133
from .ctx import AppContext
3234
from .globals import _cv_app
35+
from .globals import app_ctx
3336
from .globals import g
3437
from .globals import request
3538
from .globals import session
@@ -73,6 +76,35 @@ def _make_timedelta(value: timedelta | int | None) -> timedelta | None:
7376
return timedelta(seconds=value)
7477

7578

79+
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
80+
81+
82+
# Other methods may call the overridden method with the new ctx arg. Remove it
83+
# and call the method with the remaining args.
84+
def remove_ctx(f: F) -> F:
85+
def wrapper(self: Flask, *args: t.Any, **kwargs: t.Any) -> t.Any:
86+
if args and isinstance(args[0], AppContext):
87+
args = args[1:]
88+
89+
return f(self, *args, **kwargs)
90+
91+
return update_wrapper(wrapper, f) # type: ignore[return-value]
92+
93+
94+
# The overridden method may call super().base_method without the new ctx arg.
95+
# Add it to the args for the call.
96+
def add_ctx(f: F) -> F:
97+
def wrapper(self: Flask, *args: t.Any, **kwargs: t.Any) -> t.Any:
98+
if not args:
99+
args = (app_ctx._get_current_object(),)
100+
elif not isinstance(args[0], AppContext):
101+
args = (app_ctx._get_current_object(), *args)
102+
103+
return f(self, *args, **kwargs)
104+
105+
return update_wrapper(wrapper, f) # type: ignore[return-value]
106+
107+
76108
class Flask(App):
77109
"""The flask object implements a WSGI application and acts as the central
78110
object. It is passed the name of the module or package of the
@@ -218,6 +250,62 @@ class Flask(App):
218250
#: .. versionadded:: 0.8
219251
session_interface: SessionInterface = SecureCookieSessionInterface()
220252

253+
def __init_subclass__(cls, **kwargs: t.Any) -> None:
254+
import warnings
255+
256+
# These method signatures were updated to take a ctx param. Detect
257+
# overridden methods in subclasses that still have the old signature.
258+
# Show a deprecation warning and wrap to call with correct args.
259+
for method in (
260+
cls.handle_http_exception,
261+
cls.handle_user_exception,
262+
cls.handle_exception,
263+
cls.log_exception,
264+
cls.dispatch_request,
265+
cls.full_dispatch_request,
266+
cls.finalize_request,
267+
cls.make_default_options_response,
268+
cls.preprocess_request,
269+
cls.process_response,
270+
cls.do_teardown_request,
271+
cls.do_teardown_appcontext,
272+
):
273+
base_method = getattr(Flask, method.__name__)
274+
275+
if method is base_method:
276+
# not overridden
277+
continue
278+
279+
# get the second parameter (first is self)
280+
iter_params = iter(inspect.signature(method).parameters.values())
281+
next(iter_params)
282+
param = next(iter_params, None)
283+
284+
# must have second parameter named ctx or annotated AppContext
285+
if param is None or not (
286+
# no annotation, match name
287+
(param.annotation is inspect.Parameter.empty and param.name == "ctx")
288+
or (
289+
# string annotation, access path ends with AppContext
290+
isinstance(param.annotation, str)
291+
and param.annotation.rpartition(".")[2] == "AppContext"
292+
)
293+
or (
294+
# class annotation
295+
inspect.isclass(param.annotation)
296+
and issubclass(param.annotation, AppContext)
297+
)
298+
):
299+
warnings.warn(
300+
f"The '{method.__name__}' method now takes 'ctx: AppContext'"
301+
" as the first parameter. The old signature is deprecated"
302+
" and will not be supported in Flask 4.0.",
303+
DeprecationWarning,
304+
stacklevel=2,
305+
)
306+
setattr(cls, method.__name__, remove_ctx(method))
307+
setattr(Flask, method.__name__, add_ctx(base_method))
308+
221309
def __init__(
222310
self,
223311
import_name: str,
@@ -498,7 +586,9 @@ def raise_routing_exception(self, request: Request) -> t.NoReturn:
498586

499587
raise FormDataRoutingRedirect(request)
500588

501-
def update_template_context(self, context: dict[str, t.Any]) -> None:
589+
def update_template_context(
590+
self, ctx: AppContext, context: dict[str, t.Any]
591+
) -> None:
502592
"""Update the template context with some commonly used variables.
503593
This injects request, session, config and g into the template
504594
context as well as everything template context processors want
@@ -512,7 +602,7 @@ def update_template_context(self, context: dict[str, t.Any]) -> None:
512602
names: t.Iterable[str | None] = (None,)
513603

514604
# A template may be rendered outside a request context.
515-
if (ctx := _cv_app.get(None)) is not None and ctx.has_request:
605+
if ctx.has_request:
516606
names = chain(names, reversed(ctx.request.blueprints))
517607

518608
# The values passed to render_template take precedence. Keep a
@@ -737,7 +827,7 @@ def test_cli_runner(self, **kwargs: t.Any) -> FlaskCliRunner:
737827
return cls(self, **kwargs) # type: ignore
738828

739829
def handle_http_exception(
740-
self, e: HTTPException
830+
self, ctx: AppContext, e: HTTPException
741831
) -> HTTPException | ft.ResponseReturnValue:
742832
"""Handles an HTTP exception. By default this will invoke the
743833
registered error handlers and fall back to returning the
@@ -766,13 +856,13 @@ def handle_http_exception(
766856
if isinstance(e, RoutingException):
767857
return e
768858

769-
handler = self._find_error_handler(e, request.blueprints)
859+
handler = self._find_error_handler(e, ctx.request.blueprints)
770860
if handler is None:
771861
return e
772862
return self.ensure_sync(handler)(e) # type: ignore[no-any-return]
773863

774864
def handle_user_exception(
775-
self, e: Exception
865+
self, ctx: AppContext, e: Exception
776866
) -> HTTPException | ft.ResponseReturnValue:
777867
"""This method is called whenever an exception occurs that
778868
should be handled. A special case is :class:`~werkzeug
@@ -794,16 +884,16 @@ def handle_user_exception(
794884
e.show_exception = True
795885

796886
if isinstance(e, HTTPException) and not self.trap_http_exception(e):
797-
return self.handle_http_exception(e)
887+
return self.handle_http_exception(ctx, e)
798888

799-
handler = self._find_error_handler(e, request.blueprints)
889+
handler = self._find_error_handler(e, ctx.request.blueprints)
800890

801891
if handler is None:
802892
raise
803893

804894
return self.ensure_sync(handler)(e) # type: ignore[no-any-return]
805895

806-
def handle_exception(self, e: Exception) -> Response:
896+
def handle_exception(self, ctx: AppContext, e: Exception) -> Response:
807897
"""Handle an exception that did not have an error handler
808898
associated with it, or that was raised from an error handler.
809899
This always causes a 500 ``InternalServerError``.
@@ -846,19 +936,20 @@ def handle_exception(self, e: Exception) -> Response:
846936

847937
raise e
848938

849-
self.log_exception(exc_info)
939+
self.log_exception(ctx, exc_info)
850940
server_error: InternalServerError | ft.ResponseReturnValue
851941
server_error = InternalServerError(original_exception=e)
852-
handler = self._find_error_handler(server_error, request.blueprints)
942+
handler = self._find_error_handler(server_error, ctx.request.blueprints)
853943

854944
if handler is not None:
855945
server_error = self.ensure_sync(handler)(server_error)
856946

857-
return self.finalize_request(server_error, from_error_handler=True)
947+
return self.finalize_request(ctx, server_error, from_error_handler=True)
858948

859949
def log_exception(
860950
self,
861-
exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]),
951+
ctx: AppContext,
952+
exc_info: tuple[type, BaseException, TracebackType] | tuple[None, None, None],
862953
) -> None:
863954
"""Logs an exception. This is called by :meth:`handle_exception`
864955
if debugging is disabled and right before the handler is called.
@@ -868,10 +959,10 @@ def log_exception(
868959
.. versionadded:: 0.8
869960
"""
870961
self.logger.error(
871-
f"Exception on {request.path} [{request.method}]", exc_info=exc_info
962+
f"Exception on {ctx.request.path} [{ctx.request.method}]", exc_info=exc_info
872963
)
873964

874-
def dispatch_request(self) -> ft.ResponseReturnValue:
965+
def dispatch_request(self, ctx: AppContext) -> ft.ResponseReturnValue:
875966
"""Does the request dispatching. Matches the URL and returns the
876967
return value of the view or error handler. This does not have to
877968
be a response object. In order to convert the return value to a
@@ -881,7 +972,7 @@ def dispatch_request(self) -> ft.ResponseReturnValue:
881972
This no longer does the exception handling, this code was
882973
moved to the new :meth:`full_dispatch_request`.
883974
"""
884-
req = _cv_app.get().request
975+
req = ctx.request
885976

886977
if req.routing_exception is not None:
887978
self.raise_routing_exception(req)
@@ -892,12 +983,12 @@ def dispatch_request(self) -> ft.ResponseReturnValue:
892983
getattr(rule, "provide_automatic_options", False)
893984
and req.method == "OPTIONS"
894985
):
895-
return self.make_default_options_response()
986+
return self.make_default_options_response(ctx)
896987
# otherwise dispatch to the handler for that endpoint
897988
view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment]
898989
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
899990

900-
def full_dispatch_request(self) -> Response:
991+
def full_dispatch_request(self, ctx: AppContext) -> Response:
901992
"""Dispatches the request and on top of that performs request
902993
pre and postprocessing as well as HTTP exception catching and
903994
error handling.
@@ -908,15 +999,16 @@ def full_dispatch_request(self) -> Response:
908999

9091000
try:
9101001
request_started.send(self, _async_wrapper=self.ensure_sync)
911-
rv = self.preprocess_request()
1002+
rv = self.preprocess_request(ctx)
9121003
if rv is None:
913-
rv = self.dispatch_request()
1004+
rv = self.dispatch_request(ctx)
9141005
except Exception as e:
915-
rv = self.handle_user_exception(e)
916-
return self.finalize_request(rv)
1006+
rv = self.handle_user_exception(ctx, e)
1007+
return self.finalize_request(ctx, rv)
9171008

9181009
def finalize_request(
9191010
self,
1011+
ctx: AppContext,
9201012
rv: ft.ResponseReturnValue | HTTPException,
9211013
from_error_handler: bool = False,
9221014
) -> Response:
@@ -934,7 +1026,7 @@ def finalize_request(
9341026
"""
9351027
response = self.make_response(rv)
9361028
try:
937-
response = self.process_response(response)
1029+
response = self.process_response(ctx, response)
9381030
request_finished.send(
9391031
self, _async_wrapper=self.ensure_sync, response=response
9401032
)
@@ -946,15 +1038,14 @@ def finalize_request(
9461038
)
9471039
return response
9481040

949-
def make_default_options_response(self) -> Response:
1041+
def make_default_options_response(self, ctx: AppContext) -> Response:
9501042
"""This method is called to create the default ``OPTIONS`` response.
9511043
This can be changed through subclassing to change the default
9521044
behavior of ``OPTIONS`` responses.
9531045
9541046
.. versionadded:: 0.7
9551047
"""
956-
adapter = _cv_app.get().url_adapter
957-
methods = adapter.allowed_methods() # type: ignore[union-attr]
1048+
methods = ctx.url_adapter.allowed_methods() # type: ignore[union-attr]
9581049
rv = self.response_class()
9591050
rv.allow.update(methods)
9601051
return rv
@@ -1260,7 +1351,7 @@ def make_response(self, rv: ft.ResponseReturnValue) -> Response:
12601351

12611352
return rv
12621353

1263-
def preprocess_request(self) -> ft.ResponseReturnValue | None:
1354+
def preprocess_request(self, ctx: AppContext) -> ft.ResponseReturnValue | None:
12641355
"""Called before the request is dispatched. Calls
12651356
:attr:`url_value_preprocessors` registered with the app and the
12661357
current blueprint (if any). Then calls :attr:`before_request_funcs`
@@ -1270,7 +1361,7 @@ def preprocess_request(self) -> ft.ResponseReturnValue | None:
12701361
value is handled as if it was the return value from the view, and
12711362
further request handling is stopped.
12721363
"""
1273-
req = _cv_app.get().request
1364+
req = ctx.request
12741365
names = (None, *reversed(req.blueprints))
12751366

12761367
for name in names:
@@ -1288,7 +1379,7 @@ def preprocess_request(self) -> ft.ResponseReturnValue | None:
12881379

12891380
return None
12901381

1291-
def process_response(self, response: Response) -> Response:
1382+
def process_response(self, ctx: AppContext, response: Response) -> Response:
12921383
"""Can be overridden in order to modify the response object
12931384
before it's sent to the WSGI server. By default this will
12941385
call all the :meth:`after_request` decorated functions.
@@ -1301,8 +1392,6 @@ def process_response(self, response: Response) -> Response:
13011392
:return: a new response object or the same, has to be an
13021393
instance of :attr:`response_class`.
13031394
"""
1304-
ctx = _cv_app.get()
1305-
13061395
for func in ctx._after_request_functions:
13071396
response = self.ensure_sync(func)(response)
13081397

@@ -1316,7 +1405,9 @@ def process_response(self, response: Response) -> Response:
13161405

13171406
return response
13181407

1319-
def do_teardown_request(self, exc: BaseException | None = None) -> None:
1408+
def do_teardown_request(
1409+
self, ctx: AppContext, exc: BaseException | None = None
1410+
) -> None:
13201411
"""Called after the request is dispatched and the response is finalized,
13211412
right before the request context is popped. Called by
13221413
:meth:`.AppContext.pop`.
@@ -1331,16 +1422,16 @@ def do_teardown_request(self, exc: BaseException | None = None) -> None:
13311422
.. versionchanged:: 0.9
13321423
Added the ``exc`` argument.
13331424
"""
1334-
req = _cv_app.get().request
1335-
1336-
for name in chain(req.blueprints, (None,)):
1425+
for name in chain(ctx.request.blueprints, (None,)):
13371426
if name in self.teardown_request_funcs:
13381427
for func in reversed(self.teardown_request_funcs[name]):
13391428
self.ensure_sync(func)(exc)
13401429

13411430
request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc)
13421431

1343-
def do_teardown_appcontext(self, exc: BaseException | None = None) -> None:
1432+
def do_teardown_appcontext(
1433+
self, ctx: AppContext, exc: BaseException | None = None
1434+
) -> None:
13441435
"""Called right before the application context is popped. Called by
13451436
:meth:`.AppContext.pop`.
13461437
@@ -1473,17 +1564,17 @@ def wsgi_app(
14731564
try:
14741565
try:
14751566
ctx.push()
1476-
response = self.full_dispatch_request()
1567+
response = self.full_dispatch_request(ctx)
14771568
except Exception as e:
14781569
error = e
1479-
response = self.handle_exception(e)
1570+
response = self.handle_exception(ctx, e)
14801571
except: # noqa: B001
14811572
error = sys.exc_info()[1]
14821573
raise
14831574
return response(environ, start_response)
14841575
finally:
14851576
if "werkzeug.debug.preserve_context" in environ:
1486-
environ["werkzeug.debug.preserve_context"](_cv_app.get())
1577+
environ["werkzeug.debug.preserve_context"](ctx)
14871578

14881579
if error is not None and self.should_ignore_error(error):
14891580
error = None

0 commit comments

Comments
 (0)