Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ For a full list of triaged issues, bugs and PRs and what release they are target

All help in providing PRs to close out bug issues is appreciated. Even if that is providing a repo that fully replicates issues. We have very generous contributors that have added these to bug issues which meant another contributor picked up the bug and closed it out.

- 8.1.2
- Fix aiohttp 3.14 compatibility: ``AsyncStreamReaderMixin`` removed and ``ClientResponse`` now requires ``stream_writer`` (#995) - thanks @dsfaccini

- 8.1.1
- Fix sync requests in async contexts for HTTPX (#965) - thanks @seowalex
- CI: bump peter-evans/create-pull-request from 7 to 8 (#969)
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/aiohttp_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ async def aiohttp_request(loop, method, url, output="text", encoding="utf-8", co
content = await response.read()
elif output == "stream":
content = await response.content.read()
elif output == "stream_chunked":
content = b"".join([chunk async for chunk in response.content.iter_chunked(1024)])

response_ctx._resp.close()
await session.close()
Expand Down
15 changes: 15 additions & 0 deletions tests/integration/test_aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,21 @@ def test_stream(tmpdir, httpbin):
assert cassette.play_count == 1


@pytest.mark.online
def test_stream_chunked(tmpdir, httpbin):
# Exercises the async-iteration surface (``content.iter_chunked``) that
# ``MockStream`` must keep providing across aiohttp versions.
url = httpbin.url

with vcr.use_cassette(str(tmpdir.join("stream.yaml"))):
_, body = get(url, output="raw") # Do not use stream here, as the stream is exhausted by vcr

with vcr.use_cassette(str(tmpdir.join("stream.yaml"))) as cassette:
_, cassette_body = get(url, output="stream_chunked")
assert cassette_body == body
assert cassette.play_count == 1


POST_DATA = {"key1": "value1", "key2": "value2"}


Expand Down
29 changes: 27 additions & 2 deletions vcr/stubs/aiohttp_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import asyncio
import functools
import inspect
import json
import logging
from collections.abc import Mapping
from http.cookies import CookieError, Morsel, SimpleCookie
from types import SimpleNamespace

from aiohttp import ClientConnectionError, ClientResponse, CookieJar, RequestInfo, hdrs, streams
from aiohttp.helpers import strip_auth_from_url
Expand All @@ -17,13 +19,35 @@

log = logging.getLogger(__name__)

# aiohttp 3.14 made ``stream_writer`` a required ClientResponse argument and,
# when ``writer`` is None, reads ``stream_writer.output_size``. A replayed
# response has already been "sent", so a zero-sized writer is accurate. Older
# aiohttp doesn't accept the argument at all.
_CLIENT_RESPONSE_PARAMS = inspect.signature(ClientResponse.__init__).parameters
_CLIENT_RESPONSE_ACCEPTS_STREAM_WRITER = "stream_writer" in _CLIENT_RESPONSE_PARAMS

class MockStream(asyncio.StreamReader, streams.AsyncStreamReaderMixin):
pass

class MockStream(asyncio.StreamReader):
# aiohttp added the async-iteration helpers below to its response stream via
# ``streams.AsyncStreamReaderMixin``, which aiohttp 3.14 removed (folding the
# helpers into its own ``StreamReader``). This stub builds on
# ``asyncio.StreamReader`` instead, so provide the helpers directly to keep the
# streaming surface stable across aiohttp versions.
def iter_chunked(self, n):
return streams.AsyncStreamIterator(lambda: self.read(n))

def iter_any(self):
return streams.AsyncStreamIterator(self.readany)

def iter_chunks(self):
return streams.ChunkTupleAsyncStreamIterator(self)


class MockClientResponse(ClientResponse):
def __init__(self, method, url, request_info=None):
extra = {}
if _CLIENT_RESPONSE_ACCEPTS_STREAM_WRITER:
extra["stream_writer"] = SimpleNamespace(output_size=0)
super().__init__(
method=method,
url=url,
Expand All @@ -34,6 +58,7 @@ def __init__(self, method, url, request_info=None):
traces=None,
loop=asyncio.get_event_loop(),
session=None,
**extra,
)

async def json(self, *, encoding="utf-8", loads=json.loads, **kwargs):
Expand Down