From 609c6e331d2d262514b7fc4ca821264d37faa0e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Sep 2024 17:33:40 -0500 Subject: [PATCH 1/7] Increment version to 3.10.8.dev0 --- aiohttp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index cd3834cd3f..6a1382e567 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.10.7" +__version__ = "3.10.8.dev0" from typing import TYPE_CHECKING, Tuple From 4e3797a800496e28d87ddea59ff70f3b129dd794 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 28 Sep 2024 02:38:08 +0100 Subject: [PATCH 2/7] Fix custom cookies example (#9321) (#9323) (cherry picked from commit a4b148e84dacfaac3b17c6cb5f5ca3025b0e4914) --- docs/client_advanced.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/client_advanced.rst b/docs/client_advanced.rst index 26594a21b1..524b087745 100644 --- a/docs/client_advanced.rst +++ b/docs/client_advanced.rst @@ -82,14 +82,14 @@ parameter of :class:`ClientSession` constructor:: between multiple requests:: async with aiohttp.ClientSession() as session: - await session.get( - 'http://httpbin.org/cookies/set?my_cookie=my_value') - filtered = session.cookie_jar.filter_cookies( - 'http://httpbin.org') - assert filtered['my_cookie'].value == 'my_value' - async with session.get('http://httpbin.org/cookies') as r: + async with session.get( + "http://httpbin.org/cookies/set?my_cookie=my_value", + allow_redirects=False + ) as resp: + assert resp.cookies["my_cookie"].value == "my_value" + async with session.get("http://httpbin.org/cookies") as r: json_body = await r.json() - assert json_body['cookies']['my_cookie'] == 'my_value' + assert json_body["cookies"]["my_cookie"] == "my_value" Response Headers and Cookies ---------------------------- From 52e0b917ddbba93cececb32587928583d66f5f61 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 28 Sep 2024 02:38:21 +0100 Subject: [PATCH 3/7] Fix custom cookies example (#9321) (#9324) (cherry picked from commit a4b148e84dacfaac3b17c6cb5f5ca3025b0e4914) --- docs/client_advanced.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/client_advanced.rst b/docs/client_advanced.rst index 26594a21b1..524b087745 100644 --- a/docs/client_advanced.rst +++ b/docs/client_advanced.rst @@ -82,14 +82,14 @@ parameter of :class:`ClientSession` constructor:: between multiple requests:: async with aiohttp.ClientSession() as session: - await session.get( - 'http://httpbin.org/cookies/set?my_cookie=my_value') - filtered = session.cookie_jar.filter_cookies( - 'http://httpbin.org') - assert filtered['my_cookie'].value == 'my_value' - async with session.get('http://httpbin.org/cookies') as r: + async with session.get( + "http://httpbin.org/cookies/set?my_cookie=my_value", + allow_redirects=False + ) as resp: + assert resp.cookies["my_cookie"].value == "my_value" + async with session.get("http://httpbin.org/cookies") as r: json_body = await r.json() - assert json_body['cookies']['my_cookie'] == 'my_value' + assert json_body["cookies"]["my_cookie"] == "my_value" Response Headers and Cookies ---------------------------- From a308f748de85708735a0a07f30551ebee77c75e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Sep 2024 14:11:18 -0500 Subject: [PATCH 4/7] [PR #9326/fe26ae2 backport][3.10] Fix TimerContext not uncancelling the current task (#9328) --- CHANGES/9326.bugfix.rst | 1 + aiohttp/helpers.py | 23 ++++++++++++++--- tests/test_helpers.py | 56 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 CHANGES/9326.bugfix.rst diff --git a/CHANGES/9326.bugfix.rst b/CHANGES/9326.bugfix.rst new file mode 100644 index 0000000000..4689941708 --- /dev/null +++ b/CHANGES/9326.bugfix.rst @@ -0,0 +1 @@ +Fixed cancellation leaking upwards on timeout -- by :user:`bdraco`. diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 40705b16d7..ee2a91cec4 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -686,6 +686,7 @@ def __init__(self, loop: asyncio.AbstractEventLoop) -> None: self._loop = loop self._tasks: List[asyncio.Task[Any]] = [] self._cancelled = False + self._cancelling = 0 def assert_timeout(self) -> None: """Raise TimeoutError if timer has already been cancelled.""" @@ -694,12 +695,17 @@ def assert_timeout(self) -> None: def __enter__(self) -> BaseTimerContext: task = asyncio.current_task(loop=self._loop) - if task is None: raise RuntimeError( "Timeout context manager should be used " "inside a task" ) + if sys.version_info >= (3, 11): + # Remember if the task was already cancelling + # so when we __exit__ we can decide if we should + # raise asyncio.TimeoutError or let the cancellation propagate + self._cancelling = task.cancelling() + if self._cancelled: raise asyncio.TimeoutError from None @@ -712,11 +718,22 @@ def __exit__( exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Optional[bool]: + enter_task: Optional[asyncio.Task[Any]] = None if self._tasks: - self._tasks.pop() + enter_task = self._tasks.pop() if exc_type is asyncio.CancelledError and self._cancelled: - raise asyncio.TimeoutError from None + assert enter_task is not None + # The timeout was hit, and the task was cancelled + # so we need to uncancel the last task that entered the context manager + # since the cancellation should not leak out of the context manager + if sys.version_info >= (3, 11): + # If the task was already cancelling don't raise + # asyncio.TimeoutError and instead return None + # to allow the cancellation to propagate + if enter_task.uncancel() > self._cancelling: + return None + raise asyncio.TimeoutError from exc_val return None def timeout(self) -> None: diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 2d6e098aae..f79f9bebe0 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -397,7 +397,61 @@ def test_timer_context_not_cancelled() -> None: assert not m_asyncio.current_task.return_value.cancel.called -def test_timer_context_no_task(loop) -> None: +@pytest.mark.skipif( + sys.version_info < (3, 11), reason="Python 3.11+ is required for .cancelling()" +) +async def test_timer_context_timeout_does_not_leak_upward() -> None: + """Verify that the TimerContext does not leak cancellation outside the context manager.""" + loop = asyncio.get_running_loop() + ctx = helpers.TimerContext(loop) + current_task = asyncio.current_task() + assert current_task is not None + with pytest.raises(asyncio.TimeoutError): + with ctx: + assert current_task.cancelling() == 0 + loop.call_soon(ctx.timeout) + await asyncio.sleep(1) + + # After the context manager exits, the task should no longer be cancelling + assert current_task.cancelling() == 0 + + +@pytest.mark.skipif( + sys.version_info < (3, 11), reason="Python 3.11+ is required for .cancelling()" +) +async def test_timer_context_timeout_does_swallow_cancellation() -> None: + """Verify that the TimerContext does not swallow cancellation.""" + loop = asyncio.get_running_loop() + current_task = asyncio.current_task() + assert current_task is not None + ctx = helpers.TimerContext(loop) + + async def task_with_timeout() -> None: + nonlocal ctx + new_task = asyncio.current_task() + assert new_task is not None + with pytest.raises(asyncio.TimeoutError): + with ctx: + assert new_task.cancelling() == 0 + await asyncio.sleep(1) + + task = asyncio.create_task(task_with_timeout()) + await asyncio.sleep(0) + task.cancel() + assert task.cancelling() == 1 + ctx.timeout() + + # Cancellation should not leak into the current task + assert current_task.cancelling() == 0 + # Cancellation should not be swallowed if the task is cancelled + # and it also times out + await asyncio.sleep(0) + with pytest.raises(asyncio.CancelledError): + await task + assert task.cancelling() == 1 + + +def test_timer_context_no_task(loop: asyncio.AbstractEventLoop) -> None: with pytest.raises(RuntimeError): with helpers.TimerContext(loop): pass From 4187b8776ff68dfe55d3f3aa92600b12b728ce27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Sep 2024 14:11:28 -0500 Subject: [PATCH 5/7] [PR #9326/fe26ae2 backport][3.11] Fix TimerContext not uncancelling the current task (#9329) --- CHANGES/9326.bugfix.rst | 1 + aiohttp/helpers.py | 23 ++++++++++++++--- tests/test_helpers.py | 56 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 CHANGES/9326.bugfix.rst diff --git a/CHANGES/9326.bugfix.rst b/CHANGES/9326.bugfix.rst new file mode 100644 index 0000000000..4689941708 --- /dev/null +++ b/CHANGES/9326.bugfix.rst @@ -0,0 +1 @@ +Fixed cancellation leaking upwards on timeout -- by :user:`bdraco`. diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index f5540a1966..5e83f40a31 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -684,6 +684,7 @@ def __init__(self, loop: asyncio.AbstractEventLoop) -> None: self._loop = loop self._tasks: List[asyncio.Task[Any]] = [] self._cancelled = False + self._cancelling = 0 def assert_timeout(self) -> None: """Raise TimeoutError if timer has already been cancelled.""" @@ -692,10 +693,15 @@ def assert_timeout(self) -> None: def __enter__(self) -> BaseTimerContext: task = asyncio.current_task(loop=self._loop) - if task is None: raise RuntimeError("Timeout context manager should be used inside a task") + if sys.version_info >= (3, 11): + # Remember if the task was already cancelling + # so when we __exit__ we can decide if we should + # raise asyncio.TimeoutError or let the cancellation propagate + self._cancelling = task.cancelling() + if self._cancelled: raise asyncio.TimeoutError from None @@ -708,11 +714,22 @@ def __exit__( exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Optional[bool]: + enter_task: Optional[asyncio.Task[Any]] = None if self._tasks: - self._tasks.pop() + enter_task = self._tasks.pop() if exc_type is asyncio.CancelledError and self._cancelled: - raise asyncio.TimeoutError from None + assert enter_task is not None + # The timeout was hit, and the task was cancelled + # so we need to uncancel the last task that entered the context manager + # since the cancellation should not leak out of the context manager + if sys.version_info >= (3, 11): + # If the task was already cancelling don't raise + # asyncio.TimeoutError and instead return None + # to allow the cancellation to propagate + if enter_task.uncancel() > self._cancelling: + return None + raise asyncio.TimeoutError from exc_val return None def timeout(self) -> None: diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 13d73a312f..3ccca3a7e1 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -398,7 +398,61 @@ def test_timer_context_not_cancelled() -> None: assert not m_asyncio.current_task.return_value.cancel.called -def test_timer_context_no_task(loop) -> None: +@pytest.mark.skipif( + sys.version_info < (3, 11), reason="Python 3.11+ is required for .cancelling()" +) +async def test_timer_context_timeout_does_not_leak_upward() -> None: + """Verify that the TimerContext does not leak cancellation outside the context manager.""" + loop = asyncio.get_running_loop() + ctx = helpers.TimerContext(loop) + current_task = asyncio.current_task() + assert current_task is not None + with pytest.raises(asyncio.TimeoutError): + with ctx: + assert current_task.cancelling() == 0 + loop.call_soon(ctx.timeout) + await asyncio.sleep(1) + + # After the context manager exits, the task should no longer be cancelling + assert current_task.cancelling() == 0 + + +@pytest.mark.skipif( + sys.version_info < (3, 11), reason="Python 3.11+ is required for .cancelling()" +) +async def test_timer_context_timeout_does_swallow_cancellation() -> None: + """Verify that the TimerContext does not swallow cancellation.""" + loop = asyncio.get_running_loop() + current_task = asyncio.current_task() + assert current_task is not None + ctx = helpers.TimerContext(loop) + + async def task_with_timeout() -> None: + nonlocal ctx + new_task = asyncio.current_task() + assert new_task is not None + with pytest.raises(asyncio.TimeoutError): + with ctx: + assert new_task.cancelling() == 0 + await asyncio.sleep(1) + + task = asyncio.create_task(task_with_timeout()) + await asyncio.sleep(0) + task.cancel() + assert task.cancelling() == 1 + ctx.timeout() + + # Cancellation should not leak into the current task + assert current_task.cancelling() == 0 + # Cancellation should not be swallowed if the task is cancelled + # and it also times out + await asyncio.sleep(0) + with pytest.raises(asyncio.CancelledError): + await task + assert task.cancelling() == 1 + + +def test_timer_context_no_task(loop: asyncio.AbstractEventLoop) -> None: with pytest.raises(RuntimeError): with helpers.TimerContext(loop): pass From ce12066a228b857a95f2869f017c8b9ac08bda8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Sep 2024 19:43:01 +0000 Subject: [PATCH 6/7] Bump aiohappyeyeballs from 2.4.0 to 2.4.2 (#9310) Bumps [aiohappyeyeballs](https://github.com/aio-libs/aiohappyeyeballs) from 2.4.0 to 2.4.2.
Release notes

Sourced from aiohappyeyeballs's releases.

v2.4.2 (2024-09-27)

Fix

  • fix: copy staggered from standard lib for python 3.12+ (#95) (c5a4023)

v2.4.1 (2024-09-26)

Fix

  • fix: avoid passing loop to staggered.staggered_race (#94) (5f80b79)
Changelog

Sourced from aiohappyeyeballs's changelog.

v2.4.2 (2024-09-27)

Fix

  • Copy staggered from standard lib for python 3.12+ (#95) (c5a4023)

v2.4.1 (2024-09-26)

Fix

  • Avoid passing loop to staggered.staggered_race (#94) (5f80b79)
Commits
  • 04dbbe5 2.4.2
  • c5a4023 fix: copy staggered from standard lib for python 3.12+ (#95)
  • 04c42b4 2.4.1
  • 5f80b79 fix: avoid passing loop to staggered.staggered_race (#94)
  • b5192ad chore(pre-commit.ci): pre-commit autoupdate (#92)
  • 097c9fa chore(pre-commit.ci): pre-commit autoupdate (#91)
  • fdd35ac chore: fix trivial typo in readme
  • 7038b2d chore: update readme to include license information (#90)
  • e45003f chore(pre-commit.ci): pre-commit autoupdate (#88)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=aiohappyeyeballs&package-manager=pip&previous-version=2.4.0&new-version=2.4.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/runtime-deps.txt | 2 +- requirements/test.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 89e2862252..5e33e09622 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,7 +6,7 @@ # aiodns==3.2.0 ; sys_platform == "linux" or sys_platform == "darwin" # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.4.0 +aiohappyeyeballs==2.4.2 # via -r requirements/runtime-deps.in aiosignal==1.3.1 # via -r requirements/runtime-deps.in diff --git a/requirements/constraints.txt b/requirements/constraints.txt index ae130c0903..b1991f84a6 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -8,7 +8,7 @@ aiodns==3.2.0 ; sys_platform == "linux" or sys_platform == "darwin" # via # -r requirements/lint.in # -r requirements/runtime-deps.in -aiohappyeyeballs==2.4.0 +aiohappyeyeballs==2.4.2 # via -r requirements/runtime-deps.in aiohttp-theme==0.1.7 # via -r requirements/doc.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 80b486f076..7ef54a516d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,7 +8,7 @@ aiodns==3.2.0 ; sys_platform == "linux" or sys_platform == "darwin" # via # -r requirements/lint.in # -r requirements/runtime-deps.in -aiohappyeyeballs==2.4.0 +aiohappyeyeballs==2.4.2 # via -r requirements/runtime-deps.in aiohttp-theme==0.1.7 # via -r requirements/doc.in diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 14b6c85f48..62e5eacb4f 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -6,7 +6,7 @@ # aiodns==3.2.0 ; sys_platform == "linux" or sys_platform == "darwin" # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.4.0 +aiohappyeyeballs==2.4.2 # via -r requirements/runtime-deps.in aiosignal==1.3.1 # via -r requirements/runtime-deps.in diff --git a/requirements/test.txt b/requirements/test.txt index 7137147708..11ce369747 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -6,7 +6,7 @@ # aiodns==3.2.0 ; sys_platform == "linux" or sys_platform == "darwin" # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.4.0 +aiohappyeyeballs==2.4.2 # via -r requirements/runtime-deps.in aiosignal==1.3.1 # via -r requirements/runtime-deps.in From 8a7ce946650a24198c10da61059f4d38bd30708e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Sep 2024 15:08:03 -0500 Subject: [PATCH 7/7] Release 3.10.8 (#9330) --- CHANGES.rst | 18 ++++++++++++++++++ CHANGES/9326.bugfix.rst | 1 - aiohttp/__init__.py | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) delete mode 100644 CHANGES/9326.bugfix.rst diff --git a/CHANGES.rst b/CHANGES.rst index 443c62a184..0cf93a5887 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,24 @@ .. towncrier release notes start +3.10.8 (2024-09-28) +=================== + +Bug fixes +--------- + +- Fixed cancellation leaking upwards on timeout -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`9326`. + + + + +---- + + 3.10.7 (2024-09-27) =================== diff --git a/CHANGES/9326.bugfix.rst b/CHANGES/9326.bugfix.rst deleted file mode 100644 index 4689941708..0000000000 --- a/CHANGES/9326.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed cancellation leaking upwards on timeout -- by :user:`bdraco`. diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index 6a1382e567..dfa44f798c 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.10.8.dev0" +__version__ = "3.10.8" from typing import TYPE_CHECKING, Tuple