new debug/test preserve context implementation

This commit is contained in:
David Lord 2022-06-29 21:02:44 -07:00
parent 3635583ce2
commit 84c722044a
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
10 changed files with 84 additions and 220 deletions

View file

@ -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 @@ class Flask(Scaffold):
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 @@ class Flask(Scaffold):
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

View file

@ -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 @@ class RequestContext:
# 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 @@ class RequestContext:
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 @@ class RequestContext:
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 @@ class RequestContext:
), 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 @@ class RequestContext:
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 (

View file

@ -600,13 +600,6 @@ class Scaffold:
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

View file

@ -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 @@ class FlaskClient(Client):
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 @@ class FlaskClient(Client):
# 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 @@ class FlaskClient(Client):
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):