diff --git a/CHANGES.rst b/CHANGES.rst index 1dbb9605..f7508afd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -61,6 +61,9 @@ Unreleased - Blueprints have a ``cli`` Click group like ``app.cli``. CLI commands registered with a blueprint will be available as a group under the ``flask`` command. :issue:`1357`. +- When using the test client as a context manager (``with client:``), + all preserved request contexts are popped when the block exits, + ensuring nested contexts are cleaned up correctly. :pr:`3157` .. _#2935: https://github.com/pallets/flask/issues/2935 .. _#2957: https://github.com/pallets/flask/issues/2957 diff --git a/flask/testing.py b/flask/testing.py index 6c9de313..f2fbad6c 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -206,12 +206,17 @@ class FlaskClient(Client): def __exit__(self, exc_type, exc_value, tb): self.preserve_context = False - # on exit we want to clean up earlier. Normally the request context - # stays preserved until the next request in the same thread comes - # in. See RequestGlobals.push() for the general behavior. - top = _request_ctx_stack.top - if top is not None and top.preserved: - top.pop() + # 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 class FlaskCliRunner(CliRunner): diff --git a/tests/test_testing.py b/tests/test_testing.py index a6b9d70e..ae0f54d9 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -411,3 +411,18 @@ def test_cli_custom_obj(app): runner = app.test_cli_runner() runner.invoke(hello_command, obj=script_info) assert NS.called + + +def test_client_pop_all_preserved(app, req_ctx, client): + @app.route("/") + def index(): + # stream_with_context pushes a third context, preserved by client + return flask.Response(flask.stream_with_context("hello")) + + # req_ctx fixture pushed an initial context, not marked preserved + with client: + # request pushes a second request context, preserved by client + client.get("/") + + # only req_ctx fixture should still be pushed + assert flask._request_ctx_stack.top is req_ctx