forked from orbit-oss/flask
new debug/test preserve context implementation
This commit is contained in:
parent
3635583ce2
commit
84c722044a
10 changed files with 84 additions and 220 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue