Repository: lepture/authlib (language: python)

Issue:
httpx_client.oauth2_client.AsyncOAuth2Client.stream signature does not match httpx._client.AsyncClient.stream
**Describe the bug**

The method `httpx._client.AsyncClient.stream` is an asynccontextmanager and can be used like this:
```
client = httpx.AsyncClient()
async with client.stream('GET', 'https://www.example.com/') as response:
    async for chunk in response.aiter_bytes():
        # do something
```

The method `httpx_client.oauth2_client.AsyncOAuth2Client.stream` is not a asynccontextmanager and must be used like this:
```
client = AsyncOAuth2Client(...)
async with await client.stream("GET", 'https://i.b') as stream:
    async for chunk in response.aiter_bytes():
        # do something
```
You can see there is an `await` required as `client.stream()` is not an asynccontextmanager but returns an Awaitable, which has to be awaited to get the asynccontextmanager from internally the called `httpx_client.oauth2_client.AsyncOAuth2Client.stream`.

This breaks interoperability of the clients, which I think should be the case. This also did change with 1.x.x.

**To Reproduce**

Will add a PR to this issue, where I fix it and where you can reproduce it with the tests.

**Expected behavior**

I have an application where I just want to use different clients, e.g. the normal AsyncClient because I use BasicAuth and AsyncOAuth2Client for OAuth2 but wit the sam logic, this is currently not possible.

**Environment:**

 - OS: Debian buster and OS X
 - Python Version: 3.10.x
 - Authlib Version: Authlib==1.0.1




Discussion / hints:

Fix patch:
diff --git a/authlib/integrations/httpx_client/oauth2_client.py b/authlib/integrations/httpx_client/oauth2_client.py
index 39775286..9a441671 100644
--- a/authlib/integrations/httpx_client/oauth2_client.py
+++ b/authlib/integrations/httpx_client/oauth2_client.py
@@ -1,4 +1,5 @@
 import typing
+from contextlib import asynccontextmanager
 
 from httpx import AsyncClient, Auth, Client, Request, Response, USE_CLIENT_DEFAULT
 from anyio import Lock  # Import after httpx so import errors refer to httpx
@@ -91,6 +92,7 @@ async def request(self, method, url, withhold_token=False, auth=USE_CLIENT_DEFAU
         return await super(AsyncOAuth2Client, self).request(
             method, url, auth=auth, **kwargs)
 
+    @asynccontextmanager
     async def stream(self, method, url, withhold_token=False, auth=USE_CLIENT_DEFAULT, **kwargs):
         if not withhold_token and auth is USE_CLIENT_DEFAULT:
             if not self.token:
@@ -100,8 +102,9 @@ async def stream(self, method, url, withhold_token=False, auth=USE_CLIENT_DEFAUL
 
             auth = self.token_auth
 
-        return super(AsyncOAuth2Client, self).stream(
-            method, url, auth=auth, **kwargs)
+        async with super(AsyncOAuth2Client, self).stream(
+            method, url, auth=auth, **kwargs) as resp:
+            yield resp
 
     async def ensure_active_token(self, token):
         async with self._token_refresh_lock:


Test patch:
diff --git a/tests/clients/test_httpx/test_async_oauth2_client.py b/tests/clients/test_httpx/test_async_oauth2_client.py
index eaa50bf1..e57779d9 100644
--- a/tests/clients/test_httpx/test_async_oauth2_client.py
+++ b/tests/clients/test_httpx/test_async_oauth2_client.py
@@ -3,6 +3,9 @@
 import pytest
 from unittest import mock
 from copy import deepcopy
+
+from httpx import AsyncClient
+
 from authlib.common.security import generate_token
 from authlib.common.urls import url_encode
 from authlib.integrations.httpx_client import (
@@ -79,13 +82,28 @@ async def test_add_token_to_streaming_request(assert_func, token_placement):
             token_placement=token_placement,
             app=mock_response
     ) as client:
-        async with await client.stream("GET", 'https://i.b') as stream:
+        async with client.stream("GET", 'https://i.b') as stream:
             await stream.aread()
             data = stream.json()
 
     assert data['a'] == 'a'
 
 
+@pytest.mark.parametrize("client", [
+    AsyncOAuth2Client(
+        'foo',
+        token=default_token,
+        token_placement="header",
+        app=AsyncMockDispatch({'a': 'a'}, assert_func=assert_token_in_header)
+    ),
+    AsyncClient(app=AsyncMockDispatch({'a': 'a'}))
+])
+async def test_httpx_client_stream_match(client):
+    async with client as client_entered:
+        async with client_entered.stream("GET", 'https://i.b') as stream:
+            assert stream.status_code == 200
+
+
 def test_create_authorization_url():
     url = 'https://example.com/authorize?foo=bar'