kevin1024 / vcrpy

Automatically mock your HTTP interactions to simplify and speed up testing
MIT License
2.72k stars 389 forks source link

record creation failed when using async #817

Open finswimmer opened 9 months ago

finswimmer commented 9 months ago

Hello,

this one took me a while to find out what's going on and how to reproduce.

To reproduce this I'm using:

The following test works if vcrpy in version 5.1.0 is installed as a dependency for pytest-recording:

import httpx
import pytest
from httpx import Response

async def get() -> Response:
    client = httpx.Client(base_url="https://jsonplaceholder.typicode.com")
    result = client.get("/todos/1")

    return result

@pytest.mark.asyncio()
@pytest.mark.vcr(record_mode="once")
async def test_get() -> None:
    response = await get()

    assert response.status_code == 200

But it will fail if version 6.0.1 of vcrpy is used instead.

pytest tests         
================================================================================================================ test session starts =================================================================================================================
platform linux -- Python 3.8.10, pytest-7.4.4, pluggy-1.4.0
rootdir: /home/finswimmer/tmp/vcrpbug
plugins: recording-0.13.1, anyio-4.2.0, asyncio-0.23.4
asyncio: mode=strict
collected 1 item                                                                                                                                                                                                                                     

tests/test_minimal.py F                                                                                                                                                                                                                        [100%]

====================================================================================================================== FAILURES ======================================================================================================================
______________________________________________________________________________________________________________________ test_get ______________________________________________________________________________________________________________________

    @pytest.mark.asyncio()
    @pytest.mark.vcr(record_mode="once")
    async def test_get() -> None:
>       response = await get()

tests/test_minimal.py:16: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
tests/test_minimal.py:8: in get
    result = client.get("/todos/1")
.venv/lib/python3.8/site-packages/httpx/_client.py:1055: in get
    return self.request(
.venv/lib/python3.8/site-packages/httpx/_client.py:828: in request
    return self.send(request, auth=auth, follow_redirects=follow_redirects)
.venv/lib/python3.8/site-packages/httpx/_client.py:915: in send
    response = self._send_handling_auth(
.venv/lib/python3.8/site-packages/httpx/_client.py:943: in _send_handling_auth
    response = self._send_handling_redirects(
.venv/lib/python3.8/site-packages/httpx/_client.py:980: in _send_handling_redirects
    response = self._send_single_request(request)
.venv/lib/python3.8/site-packages/vcr/stubs/httpx_stubs.py:184: in _inner_send
    return _sync_vcr_send(cassette, real_send, *args, **kwargs)
.venv/lib/python3.8/site-packages/vcr/stubs/httpx_stubs.py:177: in _sync_vcr_send
    asyncio.run(_record_responses(cassette, vcr_request, real_response, aread=False))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

main = <coroutine object _record_responses at 0x7ffb9e25a6c0>

    def run(main, *, debug=None):
        """Execute the coroutine and return the result.

        This function runs the passed coroutine, taking care of
        managing the asyncio event loop and finalizing asynchronous
        generators.

        This function cannot be called when another asyncio event loop is
        running in the same thread.

        If debug is True, the event loop will be run in debug mode.

        This function always creates a new event loop and closes it at the end.
        It should be used as a main entry point for asyncio programs, and should
        ideally only be called once.

        Example:

            async def main():
                await asyncio.sleep(1)
                print('hello')

            asyncio.run(main())
        """
        if events._get_running_loop() is not None:
>           raise RuntimeError(
                "asyncio.run() cannot be called from a running event loop")
E           RuntimeError: asyncio.run() cannot be called from a running event loop

/usr/lib/python3.8/asyncio/runners.py:33: RuntimeError
============================================================================================================== short test summary info ===============================================================================================================
FAILED tests/test_minimal.py::test_get - RuntimeError: asyncio.run() cannot be called from a running event loop
================================================================================================================= 1 failed in 0.46s ==================================================================================================================
sys:1: RuntimeWarning: coroutine '_record_responses' was never awaited

The error only occurs if there is no record already. Once there is one (e.g. created by an older version) the replay works without issues.

JSv4 commented 9 months ago

I had a very similar issue attempting to use VCR to record the https requests from the OpenAI API client in an async function. The same fix worked for me - downgraded to 5.1.0, recorded the https calls, and then bumped back to 6.0.1. Let me know if there specific information I can provide here.

dbrewster commented 9 months ago

So I think I've figured out what exactly is going on.

If you use the httpx AsyncClient from an async method where the client is created in the call chain (arbitrarily deep) then it works just fine.

If you use the non-async httpx client from a sync method call chain it works fine.

If you use the sync httpx client from an async method, it fails.

This all makes sense and likely the code just needs to check if it is currently running in an event loop and do the right thing accordingly.

finswimmer commented 3 months ago

Hey,

just a gently reminder about this issue :smiley:

fin swimmer