Repository: aio-libs/aiohttp
Pull request: Only install async-timeout for Python < 3.11
## What do these changes do?
Replace `async-timeout` with `asyncio.timeout` for Python 3.11+.

Overall the change isn't as clean as I would have liked it to be. That's mostly a result of the name conflict with `timeout`. I'm open for suggestions on how to improve it.

## Are there changes in behavior for the user?
`async-timeout` won't be a dependency for Python 3.11+ any longer.

## Related issue number

Fixes: #7502


Problem statement:
Switch to asyncio.timeout()
We should look at whether it is easy to start using `asyncio.timeout()` in Python 3.11+ and drop the async-timeout dependency on newer Python releases.

We could always make changes to async-timeout if it is not trivial to switch between them.

Hints from issue discussion:

Commit message: Only install async-timeout for Python < 3.11

Patch to review:
diff --git a/CHANGES/7502.feature b/CHANGES/7502.feature
new file mode 100644
index 00000000000..2ab8c0987f0
--- /dev/null
+++ b/CHANGES/7502.feature
@@ -0,0 +1 @@
+Only install async-timeout for Python < 3.11.
diff --git a/aiohttp/client_ws.py b/aiohttp/client_ws.py
index 3e25c045967..0a010fa7920 100644
--- a/aiohttp/client_ws.py
+++ b/aiohttp/client_ws.py
@@ -2,10 +2,9 @@
 
 import asyncio
 import dataclasses
+import sys
 from typing import Any, Final, Optional, cast
 
-import async_timeout
-
 from .client_exceptions import ClientError
 from .client_reqrep import ClientResponse
 from .helpers import call_later, set_result
@@ -26,6 +25,11 @@
     JSONEncoder,
 )
 
+if sys.version_info >= (3, 11):
+    import asyncio as async_timeout
+else:
+    import async_timeout
+
 
 @dataclasses.dataclass(frozen=True)
 class ClientWSTimeout:
diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py
index 7dbbbe5ef7e..73396518fc3 100644
--- a/aiohttp/helpers.py
+++ b/aiohttp/helpers.py
@@ -50,7 +50,6 @@
 from urllib.parse import quote
 from urllib.request import getproxies, proxy_bypass
 
-import async_timeout
 from multidict import CIMultiDict, MultiDict, MultiDictProxy
 from yarl import URL
 
@@ -58,9 +57,15 @@
 from .log import client_logger
 from .typedefs import PathLike  # noqa
 
+if sys.version_info >= (3, 11):
+    import asyncio as async_timeout
+else:
+    import async_timeout
+
 __all__ = ("BasicAuth", "ChainMapProxy", "ETag")
 
 PY_310 = sys.version_info >= (3, 10)
+PY_311 = sys.version_info >= (3, 11)
 
 COOKIE_MAX_LENGTH = 4096
 
diff --git a/aiohttp/web_ws.py b/aiohttp/web_ws.py
index 1592230ebe7..9fcdc4bdd53 100644
--- a/aiohttp/web_ws.py
+++ b/aiohttp/web_ws.py
@@ -4,9 +4,9 @@
 import dataclasses
 import hashlib
 import json
+import sys
 from typing import Any, Final, Iterable, Optional, Tuple, cast
 
-import async_timeout
 from multidict import CIMultiDict
 
 from . import hdrs
@@ -32,6 +32,11 @@
 from .web_request import BaseRequest
 from .web_response import StreamResponse
 
+if sys.version_info >= (3, 11):
+    import asyncio as async_timeout
+else:
+    import async_timeout
+
 __all__ = (
     "WebSocketResponse",
     "WebSocketReady",
diff --git a/requirements/base.txt b/requirements/base.txt
index 76fc9aa7942..8043da412f0 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -8,7 +8,7 @@ aiodns==3.0.0 ; sys_platform == "linux" or sys_platform == "darwin"
     # via -r requirements/runtime-deps.in
 aiosignal==1.3.1
     # via -r requirements/runtime-deps.in
-async-timeout==4.0.3
+async-timeout==4.0.3 ; python_version < "3.11"
     # via -r requirements/runtime-deps.in
 brotli==1.0.9
     # via -r requirements/runtime-deps.in
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 4dc839da0f3..17e97107f60 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -14,7 +14,7 @@ aiosignal==1.3.1
     # via -r requirements/runtime-deps.in
 alabaster==0.7.13
     # via sphinx
-async-timeout==4.0.3
+async-timeout==4.0.3 ; python_version < "3.11"
     # via
     #   -r requirements/runtime-deps.in
     #   aioredis
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 7af30cfcccf..0c53aafc536 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -14,7 +14,7 @@ aiosignal==1.3.1
     # via -r requirements/runtime-deps.in
 alabaster==0.7.13
     # via sphinx
-async-timeout==4.0.3
+async-timeout==4.0.3 ; python_version < "3.11"
     # via
     #   -r requirements/runtime-deps.in
     #   aioredis
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 88185c1177d..2ffd5fdcdc1 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -6,7 +6,7 @@
 #
 aioredis==2.0.1
     # via -r requirements/lint.in
-async-timeout==4.0.3
+async-timeout==4.0.3 ; python_version < "3.11"
     # via aioredis
 cfgv==3.3.1
     # via pre-commit
diff --git a/requirements/runtime-deps.in b/requirements/runtime-deps.in
index 711a3958020..3a751d0911f 100644
--- a/requirements/runtime-deps.in
+++ b/requirements/runtime-deps.in
@@ -2,7 +2,7 @@
 
 charset-normalizer >=2.0, < 4.0
 multidict >=4.5, < 7.0
-async-timeout >= 4.0, < 5.0
+async-timeout >= 4.0, < 5.0 ; python_version < "3.11"
 yarl >= 1.0, < 2.0
 frozenlist >= 1.1.1
 aiosignal >= 1.1.2
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index b922364939a..d0de0b4f094 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -8,7 +8,7 @@ aiodns==3.0.0 ; sys_platform == "linux" or sys_platform == "darwin"
     # via -r requirements/runtime-deps.in
 aiosignal==1.3.1
     # via -r requirements/runtime-deps.in
-async-timeout==4.0.3
+async-timeout==4.0.3 ; python_version < "3.11"
     # via -r requirements/runtime-deps.in
 brotli==1.0.9
     # via -r requirements/runtime-deps.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 0929659e8e3..ceb71ab1ed8 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -8,7 +8,7 @@ aiodns==3.0.0 ; sys_platform == "linux" or sys_platform == "darwin"
     # via -r requirements/runtime-deps.in
 aiosignal==1.3.1
     # via -r requirements/runtime-deps.in
-async-timeout==4.0.3
+async-timeout==4.0.3 ; python_version < "3.11"
     # via -r requirements/runtime-deps.in
 brotli==1.0.9
     # via -r requirements/runtime-deps.in
diff --git a/setup.cfg b/setup.cfg
index 74e0b63a7ae..819815cd148 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -49,7 +49,7 @@ include_package_data = True
 install_requires =
   charset-normalizer >=2.0, < 4.0
   multidict >=4.5, < 7.0
-  async-timeout >= 4.0, < 5.0
+  async-timeout >= 4.0, < 5.0 ; python_version < "3.11"
   yarl >= 1.0, < 2.0
   frozenlist >= 1.1.1
   aiosignal >= 1.1.2
diff --git a/tests/test_client_ws_functional.py b/tests/test_client_ws_functional.py
index ffd140c9447..205f4d50a6e 100644
--- a/tests/test_client_ws_functional.py
+++ b/tests/test_client_ws_functional.py
@@ -1,14 +1,19 @@
 # type: ignore
 import asyncio
+import sys
 from typing import Any
 
-import async_timeout
 import pytest
 
 import aiohttp
 from aiohttp import hdrs, web
 from aiohttp.client_ws import ClientWSTimeout
 
+if sys.version_info >= (3, 11):
+    import asyncio as async_timeout
+else:
+    import async_timeout
+
 
 async def test_send_recv_text(aiohttp_client: Any) -> None:
     async def handler(request):
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 0f37089d59f..2256363272b 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -16,7 +16,7 @@
 from yarl import URL
 
 from aiohttp import helpers
-from aiohttp.helpers import is_expected_content_type, parse_http_date
+from aiohttp.helpers import PY_311, is_expected_content_type, parse_http_date
 
 IS_PYPY = platform.python_implementation() == "PyPy"
 
@@ -448,25 +448,37 @@ def test_ceil_call_later_no_timeout() -> None:
 
 async def test_ceil_timeout_none(loop) -> None:
     async with helpers.ceil_timeout(None) as cm:
-        assert cm.deadline is None
+        if PY_311:
+            assert cm.when() is None
+        else:
+            assert cm.deadline is None
 
 
 async def test_ceil_timeout_round(loop) -> None:
     async with helpers.ceil_timeout(7.5) as cm:
-        frac, integer = modf(cm.deadline)
+        if PY_311:
+            frac, integer = modf(cm.when())
+        else:
+            frac, integer = modf(cm.deadline)
         assert frac == 0
 
 
 async def test_ceil_timeout_small(loop) -> None:
     async with helpers.ceil_timeout(1.1) as cm:
-        frac, integer = modf(cm.deadline)
+        if PY_311:
+            frac, integer = modf(cm.when())
+        else:
+            frac, integer = modf(cm.deadline)
         # a chance for exact integer with zero fraction is negligible
         assert frac != 0
 
 
 async def test_ceil_timeout_small_with_overriden_threshold(loop) -> None:
     async with helpers.ceil_timeout(1.5, ceil_threshold=1) as cm:
-        frac, integer = modf(cm.deadline)
+        if PY_311:
+            frac, integer = modf(cm.when())
+        else:
+            frac, integer = modf(cm.deadline)
         assert frac == 0
 
 


Code review comments:
diff_hunk: @@ -50,17 +50,22 @@
 from urllib.parse import quote
 from urllib.request import getproxies, proxy_bypass
 
-import async_timeout
 from multidict import CIMultiDict, MultiDict, MultiDictProxy
 from yarl import URL
 
 from . import hdrs
 from .log import client_logger
 from .typedefs import PathLike  # noqa
 
+if sys.version_info >= (3, 11):
+    import asyncio as async_timeout
+else:
+    import async_timeout
+
 __all__ = ("BasicAuth", "ChainMapProxy", "ETag")
 
 PY_310 = sys.version_info >= (3, 10)
+PY_311 = sys.version_info >= (3, 11)
path: aiohttp/helpers.py
text: @user1:
Personally, I'm in favour of removing these variables (or atleast not adding new ones). mypy doesn't seem to work well with them, and they seem to actually end up being more work for me when I come to remove them after dropping support for a given Python version.

@author:
Works for me! Yeah, mypy was the reason I used `sys.version_info` for the import guard already. Just wasn't sure which you prefer.