Pass the request ctx rather than use the globals in the app

The globals have a performance penalty which can be justified for the
convinience in user code. In the app however the ctx can easily be
passed through the method calls thereby reducing the performance
penalty.

This may affect extensions if they have subclassed the app and
overridden these methods.
This commit is contained in:
pgjones 2023-08-20 21:37:37 +01:00
parent 74d923515c
commit d8262aa58c
5 changed files with 80 additions and 42 deletions

View file

@ -6,7 +6,8 @@ Unreleased
- Remove previously deprecated code. :pr:`5223` - Remove previously deprecated code. :pr:`5223`
- Restructure the code such that the Flask (app) and Blueprint - Restructure the code such that the Flask (app) and Blueprint
classes have Sans-IO bases. :pr:`5127` classes have Sans-IO bases. :pr:`5127`
- Pass the request ctx rather than use the globals in the app class
methods. :pr:`5229`
Version 2.3.3 Version 2.3.3
------------- -------------

View file

@ -692,12 +692,17 @@ class Flask(App):
return cls(self, **kwargs) # type: ignore return cls(self, **kwargs) # type: ignore
def handle_http_exception( def handle_http_exception(
self, e: HTTPException self,
e: HTTPException,
ctx: RequestContext,
) -> HTTPException | ft.ResponseReturnValue: ) -> HTTPException | ft.ResponseReturnValue:
"""Handles an HTTP exception. By default this will invoke the """Handles an HTTP exception. By default this will invoke the
registered error handlers and fall back to returning the registered error handlers and fall back to returning the
exception as response. exception as response.
.. versionchanged:: 3.0
The request context, ctx, is now a required argument.
.. versionchanged:: 1.0.3 .. versionchanged:: 1.0.3
``RoutingException``, used internally for actions such as ``RoutingException``, used internally for actions such as
slash redirects during routing, is not passed to error slash redirects during routing, is not passed to error
@ -721,13 +726,15 @@ class Flask(App):
if isinstance(e, RoutingException): if isinstance(e, RoutingException):
return e return e
handler = self._find_error_handler(e, request.blueprints) handler = self._find_error_handler(e, ctx.request.blueprints)
if handler is None: if handler is None:
return e return e
return self.ensure_sync(handler)(e) return self.ensure_sync(handler)(e)
def handle_user_exception( def handle_user_exception(
self, e: Exception self,
e: Exception,
ctx: RequestContext,
) -> HTTPException | ft.ResponseReturnValue: ) -> HTTPException | ft.ResponseReturnValue:
"""This method is called whenever an exception occurs that """This method is called whenever an exception occurs that
should be handled. A special case is :class:`~werkzeug should be handled. A special case is :class:`~werkzeug
@ -736,6 +743,9 @@ class Flask(App):
return a response value or reraise the exception with the same return a response value or reraise the exception with the same
traceback. traceback.
.. versionchanged:: 3.0
The request context, ctx, is now a required argument.
.. versionchanged:: 1.0 .. versionchanged:: 1.0
Key errors raised from request data like ``form`` show the Key errors raised from request data like ``form`` show the
bad key in debug mode rather than a generic bad request bad key in debug mode rather than a generic bad request
@ -749,16 +759,16 @@ class Flask(App):
e.show_exception = True e.show_exception = True
if isinstance(e, HTTPException) and not self.trap_http_exception(e): if isinstance(e, HTTPException) and not self.trap_http_exception(e):
return self.handle_http_exception(e) return self.handle_http_exception(e, ctx)
handler = self._find_error_handler(e, request.blueprints) handler = self._find_error_handler(e, ctx.request.blueprints)
if handler is None: if handler is None:
raise raise
return self.ensure_sync(handler)(e) return self.ensure_sync(handler)(e)
def handle_exception(self, e: Exception) -> Response: def handle_exception(self, e: Exception, ctx: RequestContext) -> Response:
"""Handle an exception that did not have an error handler """Handle an exception that did not have an error handler
associated with it, or that was raised from an error handler. associated with it, or that was raised from an error handler.
This always causes a 500 ``InternalServerError``. This always causes a 500 ``InternalServerError``.
@ -775,6 +785,9 @@ class Flask(App):
always receive the ``InternalServerError``. The original always receive the ``InternalServerError``. The original
unhandled exception is available as ``e.original_exception``. unhandled exception is available as ``e.original_exception``.
.. versionchanged:: 3.0
The request context, ctx, is now a required argument.
.. versionchanged:: 1.1.0 .. versionchanged:: 1.1.0
Always passes the ``InternalServerError`` instance to the Always passes the ``InternalServerError`` instance to the
handler, setting ``original_exception`` to the unhandled handler, setting ``original_exception`` to the unhandled
@ -801,77 +814,87 @@ class Flask(App):
raise e raise e
self.log_exception(exc_info) self.log_exception(exc_info, ctx)
server_error: InternalServerError | ft.ResponseReturnValue server_error: InternalServerError | ft.ResponseReturnValue
server_error = InternalServerError(original_exception=e) server_error = InternalServerError(original_exception=e)
handler = self._find_error_handler(server_error, request.blueprints) handler = self._find_error_handler(server_error, ctx.request.blueprints)
if handler is not None: if handler is not None:
server_error = self.ensure_sync(handler)(server_error) server_error = self.ensure_sync(handler)(server_error)
return self.finalize_request(server_error, from_error_handler=True) return self.finalize_request(server_error, ctx, from_error_handler=True)
def log_exception( def log_exception(
self, self,
exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]), exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]),
ctx: RequestContext,
) -> None: ) -> None:
"""Logs an exception. This is called by :meth:`handle_exception` """Logs an exception. This is called by :meth:`handle_exception`
if debugging is disabled and right before the handler is called. if debugging is disabled and right before the handler is called.
The default implementation logs the exception as error on the The default implementation logs the exception as error on the
:attr:`logger`. :attr:`logger`.
.. versionchanged:: 3.0
The request context, ctx, is now a required argument.
.. versionadded:: 0.8 .. versionadded:: 0.8
""" """
self.logger.error( self.logger.error(
f"Exception on {request.path} [{request.method}]", exc_info=exc_info f"Exception on {ctx.request.path} [{ctx.request.method}]", exc_info=exc_info
) )
def dispatch_request(self) -> ft.ResponseReturnValue: def dispatch_request(self, ctx: RequestContext) -> ft.ResponseReturnValue:
"""Does the request dispatching. Matches the URL and returns the """Does the request dispatching. Matches the URL and returns the
return value of the view or error handler. This does not have to 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 be a response object. In order to convert the return value to a
proper response object, call :func:`make_response`. proper response object, call :func:`make_response`.
.. versionchanged:: 3.0
The request context, ctx, is now a required argument.
.. versionchanged:: 0.7 .. versionchanged:: 0.7
This no longer does the exception handling, this code was This no longer does the exception handling, this code was
moved to the new :meth:`full_dispatch_request`. moved to the new :meth:`full_dispatch_request`.
""" """
req = request_ctx.request if ctx.request.routing_exception is not None:
if req.routing_exception is not None: self.raise_routing_exception(ctx.request)
self.raise_routing_exception(req) rule: Rule = ctx.request.url_rule # type: ignore[assignment]
rule: Rule = req.url_rule # type: ignore[assignment]
# if we provide automatic options for this URL and the # if we provide automatic options for this URL and the
# request came with the OPTIONS method, reply automatically # request came with the OPTIONS method, reply automatically
if ( if (
getattr(rule, "provide_automatic_options", False) getattr(rule, "provide_automatic_options", False)
and req.method == "OPTIONS" and ctx.request.method == "OPTIONS"
): ):
return self.make_default_options_response() return self.make_default_options_response()
# otherwise dispatch to the handler for that endpoint # otherwise dispatch to the handler for that endpoint
view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment] view_args: dict[str, t.Any] = ctx.request.view_args # type: ignore[assignment]
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
def full_dispatch_request(self) -> Response: def full_dispatch_request(self, ctx: RequestContext) -> Response:
"""Dispatches the request and on top of that performs request """Dispatches the request and on top of that performs request
pre and postprocessing as well as HTTP exception catching and pre and postprocessing as well as HTTP exception catching and
error handling. error handling.
.. versionchanged:: 3.0
The request context, ctx, is now a required argument.
.. versionadded:: 0.7 .. versionadded:: 0.7
""" """
self._got_first_request = True self._got_first_request = True
try: try:
request_started.send(self, _async_wrapper=self.ensure_sync) request_started.send(self, _async_wrapper=self.ensure_sync)
rv = self.preprocess_request() rv = self.preprocess_request(ctx)
if rv is None: if rv is None:
rv = self.dispatch_request() rv = self.dispatch_request(ctx)
except Exception as e: except Exception as e:
rv = self.handle_user_exception(e) rv = self.handle_user_exception(e, ctx)
return self.finalize_request(rv) return self.finalize_request(rv, ctx)
def finalize_request( def finalize_request(
self, self,
rv: ft.ResponseReturnValue | HTTPException, rv: ft.ResponseReturnValue | HTTPException,
ctx: RequestContext,
from_error_handler: bool = False, from_error_handler: bool = False,
) -> Response: ) -> Response:
"""Given the return value from a view function this finalizes """Given the return value from a view function this finalizes
@ -884,11 +907,14 @@ class Flask(App):
with the `from_error_handler` flag. If enabled, failures in with the `from_error_handler` flag. If enabled, failures in
response processing will be logged and otherwise ignored. response processing will be logged and otherwise ignored.
.. versionchanged:: 3.0
The request context, ctx, is now a required argument.
:internal: :internal:
""" """
response = self.make_response(rv) response = self.make_response(rv)
try: try:
response = self.process_response(response) response = self.process_response(response, ctx)
request_finished.send( request_finished.send(
self, _async_wrapper=self.ensure_sync, response=response self, _async_wrapper=self.ensure_sync, response=response
) )
@ -1215,7 +1241,7 @@ class Flask(App):
return rv return rv
def preprocess_request(self) -> ft.ResponseReturnValue | None: def preprocess_request(self, ctx: RequestContext) -> ft.ResponseReturnValue | None:
"""Called before the request is dispatched. Calls """Called before the request is dispatched. Calls
:attr:`url_value_preprocessors` registered with the app and the :attr:`url_value_preprocessors` registered with the app and the
current blueprint (if any). Then calls :attr:`before_request_funcs` current blueprint (if any). Then calls :attr:`before_request_funcs`
@ -1224,13 +1250,16 @@ class Flask(App):
If any :meth:`before_request` handler returns a non-None value, the If any :meth:`before_request` handler returns a non-None value, the
value is handled as if it was the return value from the view, and value is handled as if it was the return value from the view, and
further request handling is stopped. further request handling is stopped.
.. versionchanged:: 3.0
The request context, ctx, is now a required argument.
""" """
names = (None, *reversed(request.blueprints)) names = (None, *reversed(ctx.request.blueprints))
for name in names: for name in names:
if name in self.url_value_preprocessors: if name in self.url_value_preprocessors:
for url_func in self.url_value_preprocessors[name]: for url_func in self.url_value_preprocessors[name]:
url_func(request.endpoint, request.view_args) url_func(ctx.request.endpoint, ctx.request.view_args)
for name in names: for name in names:
if name in self.before_request_funcs: if name in self.before_request_funcs:
@ -1242,11 +1271,14 @@ class Flask(App):
return None return None
def process_response(self, response: Response) -> Response: def process_response(self, response: Response, ctx: RequestContext) -> Response:
"""Can be overridden in order to modify the response object """Can be overridden in order to modify the response object
before it's sent to the WSGI server. By default this will before it's sent to the WSGI server. By default this will
call all the :meth:`after_request` decorated functions. call all the :meth:`after_request` decorated functions.
.. versionchanged:: 3.0
The request context, ctx, is now a required argument.
.. versionchanged:: 0.5 .. versionchanged:: 0.5
As of Flask 0.5 the functions registered for after request As of Flask 0.5 the functions registered for after request
execution are called in reverse order of registration. execution are called in reverse order of registration.
@ -1255,23 +1287,25 @@ class Flask(App):
:return: a new response object or the same, has to be an :return: a new response object or the same, has to be an
instance of :attr:`response_class`. instance of :attr:`response_class`.
""" """
ctx = request_ctx._get_current_object() # type: ignore[attr-defined]
for func in ctx._after_request_functions: for func in ctx._after_request_functions:
response = self.ensure_sync(func)(response) response = self.ensure_sync(func)(response)
for name in chain(request.blueprints, (None,)): for name in chain(ctx.request.blueprints, (None,)):
if name in self.after_request_funcs: if name in self.after_request_funcs:
for func in reversed(self.after_request_funcs[name]): for func in reversed(self.after_request_funcs[name]):
response = self.ensure_sync(func)(response) response = self.ensure_sync(func)(response)
if not self.session_interface.is_null_session(ctx.session): if not self.session_interface.is_null_session(ctx.session):
self.session_interface.save_session(self, ctx.session, response) self.session_interface.save_session(
self, ctx.session, response # type: ignore[arg-type]
)
return response return response
def do_teardown_request( def do_teardown_request(
self, exc: BaseException | None = _sentinel # type: ignore self,
ctx: RequestContext,
exc: BaseException | None = _sentinel, # type: ignore
) -> None: ) -> None:
"""Called after the request is dispatched and the response is """Called after the request is dispatched and the response is
returned, right before the request context is popped. returned, right before the request context is popped.
@ -1290,13 +1324,16 @@ class Flask(App):
request. Detected from the current exception information if request. Detected from the current exception information if
not passed. Passed to each teardown function. not passed. Passed to each teardown function.
.. versionchanged:: 3.0
The request context, ctx, is now a required argument.
.. versionchanged:: 0.9 .. versionchanged:: 0.9
Added the ``exc`` argument. Added the ``exc`` argument.
""" """
if exc is _sentinel: if exc is _sentinel:
exc = sys.exc_info()[1] exc = sys.exc_info()[1]
for name in chain(request.blueprints, (None,)): for name in chain(ctx.request.blueprints, (None,)):
if name in self.teardown_request_funcs: if name in self.teardown_request_funcs:
for func in reversed(self.teardown_request_funcs[name]): for func in reversed(self.teardown_request_funcs[name]):
self.ensure_sync(func)(exc) self.ensure_sync(func)(exc)
@ -1451,10 +1488,10 @@ class Flask(App):
try: try:
try: try:
ctx.push() ctx.push()
response = self.full_dispatch_request() response = self.full_dispatch_request(ctx)
except Exception as e: except Exception as e:
error = e error = e
response = self.handle_exception(e) response = self.handle_exception(e, ctx)
except: # noqa: B001 except: # noqa: B001
error = sys.exc_info()[1] error = sys.exc_info()[1]
raise raise

View file

@ -398,7 +398,7 @@ class RequestContext:
if clear_request: if clear_request:
if exc is _sentinel: if exc is _sentinel:
exc = sys.exc_info()[1] exc = sys.exc_info()[1]
self.app.do_teardown_request(exc) self.app.do_teardown_request(self, exc)
request_close = getattr(self.request, "close", None) request_close = getattr(self.request, "close", None)
if request_close is not None: if request_close is not None:

View file

@ -288,8 +288,8 @@ def test_bad_environ_raises_bad_request():
# use a non-printable character in the Host - this is key to this test # use a non-printable character in the Host - this is key to this test
environ["HTTP_HOST"] = "\x8a" environ["HTTP_HOST"] = "\x8a"
with app.request_context(environ): with app.request_context(environ) as ctx:
response = app.full_dispatch_request() response = app.full_dispatch_request(ctx)
assert response.status_code == 400 assert response.status_code == 400
@ -308,8 +308,8 @@ def test_environ_for_valid_idna_completes():
# these characters are all IDNA-compatible # these characters are all IDNA-compatible
environ["HTTP_HOST"] = "ąśźäüжŠßя.com" environ["HTTP_HOST"] = "ąśźäüжŠßя.com"
with app.request_context(environ): with app.request_context(environ) as ctx:
response = app.full_dispatch_request() response = app.full_dispatch_request(ctx)
assert response.status_code == 200 assert response.status_code == 200

View file

@ -5,7 +5,7 @@ import flask
def test_suppressed_exception_logging(): def test_suppressed_exception_logging():
class SuppressedFlask(flask.Flask): class SuppressedFlask(flask.Flask):
def log_exception(self, exc_info): def log_exception(self, exc_info, ctx):
pass pass
out = StringIO() out = StringIO()