Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flask 2.2.2 crashes with pytest-asyncio 0.19.0 #4773

Closed
mhils opened this issue Aug 17, 2022 · 2 comments
Closed

Flask 2.2.2 crashes with pytest-asyncio 0.19.0 #4773

mhils opened this issue Aug 17, 2022 · 2 comments

Comments

@mhils
Copy link
Contributor

mhils commented Aug 17, 2022

Flask 2.2 crashes with a LookupError on async test cases when using pytest-asyncio:

pip install flask pytest pytest-asyncio
import pytest
from flask import Flask

app = Flask(__name__)


@pytest.fixture
def client():
    with app.test_client() as client:
        yield client


# note this test is async
async def test_foo(client):
    assert client.get("/").status_code == 404
$ pytest repro.py
========================================= test session starts ==========================================
platform linux -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: /mnt/c/Users/user/git/mitmproxy, configfile: setup.cfg
plugins: asyncio-0.19.0
asyncio: mode=auto
collected 1 item

repro.py .E

================================================ ERRORS ================================================
____________________________________ ERROR at teardown of test_foo _____________________________________

self = <RequestContext 'http://localhost/' [GET] of repro>
exc = LookupError(<ContextVar name='flask.app_ctx' at 0x7f7e5a5599e0>)

    def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None:  # type: ignore
        """Pops the request context and unbinds it by doing that.  This will
        also trigger the execution of functions registered by the
        :meth:`~flask.Flask.teardown_request` decorator.

        .. versionchanged:: 0.9
           Added the `exc` argument.
        """
        clear_request = len(self._cv_tokens) == 1

        try:
            if clear_request:
                if exc is _sentinel:
                    exc = sys.exc_info()[1]
>               self.app.do_teardown_request(exc)

repro/lib/python3.10/site-packages/flask/ctx.py:399:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <Flask 'repro'>, exc = LookupError(<ContextVar name='flask.app_ctx' at 0x7f7e5a5599e0>)

    def do_teardown_request(
        self, exc: t.Optional[BaseException] = _sentinel  # type: ignore
    ) -> None:
        """Called after the request is dispatched and the response is
        returned, right before the request context is popped.

        This calls all functions decorated with
        :meth:`teardown_request`, and :meth:`Blueprint.teardown_request`
        if a blueprint handled the request. Finally, the
        :data:`request_tearing_down` signal is sent.

        This is called by
        :meth:`RequestContext.pop() <flask.ctx.RequestContext.pop>`,
        which may be delayed during testing to maintain access to
        resources.

        :param exc: An unhandled exception raised while dispatching the
            request. Detected from the current exception information if
            not passed. Passed to each teardown function.

        .. versionchanged:: 0.9
            Added the ``exc`` argument.
        """
        if exc is _sentinel:
            exc = sys.exc_info()[1]

>       for name in chain(request.blueprints, (None,)):

repro/lib/python3.10/site-packages/flask/app.py:2370:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = proxy __getattr__, instance = <LocalProxy unbound>, owner = <class 'werkzeug.local.LocalProxy'>

    def __get__(self, instance: "LocalProxy", owner: t.Optional[type] = None) -> t.Any:
        if instance is None:
            if self.class_value is not None:
                return self.class_value

            return self

        try:
>           obj = instance._get_current_object()  # type: ignore[misc]

repro/lib/python3.10/site-packages/werkzeug/local.py:316:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    def _get_current_object() -> T:
        try:
            obj = local.get()  # type: ignore[union-attr]
        except LookupError:
>           raise RuntimeError(unbound_message) from None
E           RuntimeError: Working outside of request context.
E
E           This typically means that you attempted to use functionality that needed
E           an active HTTP request. Consult the documentation on testing for
E           information about how to avoid this problem.

repro/lib/python3.10/site-packages/werkzeug/local.py:513: RuntimeError

During handling of the above exception, another exception occurred:

    @pytest.fixture
    def client():
>       with app.test_client() as client:

repro.py:9:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
repro/lib/python3.10/site-packages/flask/testing.py:250: in __exit__
    self._context_stack.close()
/usr/lib/python3.10/contextlib.py:584: in close
    self.__exit__(None, None, None)
/usr/lib/python3.10/contextlib.py:576: in __exit__
    raise exc_details[1]
/usr/lib/python3.10/contextlib.py:561: in __exit__
    if cb(*exc_details):
repro/lib/python3.10/site-packages/flask/ctx.py:432: in __exit__
    self.pop(exc_value)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <RequestContext 'http://localhost/' [GET] of repro>
exc = LookupError(<ContextVar name='flask.app_ctx' at 0x7f7e5a5599e0>)

    def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None:  # type: ignore
        """Pops the request context and unbinds it by doing that.  This will
        also trigger the execution of functions registered by the
        :meth:`~flask.Flask.teardown_request` decorator.

        .. versionchanged:: 0.9
           Added the `exc` argument.
        """
        clear_request = len(self._cv_tokens) == 1

        try:
            if clear_request:
                if exc is _sentinel:
                    exc = sys.exc_info()[1]
                self.app.do_teardown_request(exc)

                request_close = getattr(self.request, "close", None)
                if request_close is not None:
                    request_close()
        finally:
>           ctx = _cv_request.get()
E           LookupError: <ContextVar name='flask.request_ctx' at 0x7f7e5a559670>

repro/lib/python3.10/site-packages/flask/ctx.py:405: LookupError
======================================= short test summary info ========================================
ERROR repro.py::test_foo - LookupError: <ContextVar name='flask.request_ctx' at 0x7f7e5a559670>
====================================== 1 passed, 1 error in 0.37s ======================================

The same code works fine with Flask 2.1. Thank you for your maintenance efforts! 😃

mhils added a commit to mitmproxy/mitmproxy that referenced this issue Aug 17, 2022
Flask 2.2 has compatibility issues with pytest-asyncio (pallets/flask#4773),
but it turns out we don't even need our tests to be async here. So we just use sync tests to fix the CI issues.
@davidism
Copy link
Member

davidism commented Aug 17, 2022

Don't do this, this is an invalid pattern.

@pytest.fixture
def client():
    with app.test_client() as client:
        yield client

Instead, push a test client only for exactly as long as you need to preserve the context for a request from that client. Note that you only need with client if you want to preserve the context so you can inspect flask.request after the client request. In most cases, you should not need to do that.

@pytest.fixture
def client(app):
    return app.test_client()

def test_foo(client):
    with client:
        response = client.post("/api")
        assert flask.request.method == "POST"

Other than that, I have no idea how pytest-asyncio operates. If the above is not the cause of the issue then it sounds like they're doing something unexpected with context vars. Please report it there first to determine what we need to fix if so.

@pgjones
Copy link
Member

pgjones commented Aug 17, 2022

There is a known issue with pytest-asyncio whereby contextvars are not propagated from a fixture, see pytest-dev/pytest-asyncio#127 and pallets/quart#149

dvmarkusvogl pushed a commit to rnbwdsh/mitmproxy that referenced this issue Aug 25, 2022
Flask 2.2 has compatibility issues with pytest-asyncio (pallets/flask#4773),
but it turns out we don't even need our tests to be async here. So we just use sync tests to fix the CI issues.
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 1, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants