Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-aiohappyeyeballs for
openSUSE:Factory checked in at 2024-10-30 17:33:02
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-aiohappyeyeballs (Old)
and /work/SRC/openSUSE:Factory/.python-aiohappyeyeballs.new.2020 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-aiohappyeyeballs"
Wed Oct 30 17:33:02 2024 rev:3 rq:1219103 version:2.4.3
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-aiohappyeyeballs/python-aiohappyeyeballs.changes
2024-09-03 13:37:03.053531495 +0200
+++
/work/SRC/openSUSE:Factory/.python-aiohappyeyeballs.new.2020/python-aiohappyeyeballs.changes
2024-10-30 17:33:10.117071338 +0100
@@ -1,0 +2,15 @@
+Mon Oct 28 15:43:47 UTC 2024 - Martin Hauke <[email protected]>
+
+- Update to version 2.4.3
+ * fix: rewrite staggered_race to be race safe.
+ * fix: re-raise RuntimeError when uvloop raises RuntimeError
+ during connect (#105).
+- Update to version 2.4.2
+ * fix: copy staggered from standard lib for python 3.12+ (#95).
+- Update to version 2.4.1
+ * fix: avoid passing loop to staggered.staggered_race (#94).
+- Update to version 2.4.0
+ * docs: fix a trivial typo in README.md (#84).
+ * feat: add support for python 3.13 (#86).
+
+-------------------------------------------------------------------
Old:
----
aiohappyeyeballs-2.3.7.tar.gz
New:
----
aiohappyeyeballs-2.4.3.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-aiohappyeyeballs.spec ++++++
--- /var/tmp/diff_new_pack.EJZysU/_old 2024-10-30 17:33:10.789099487 +0100
+++ /var/tmp/diff_new_pack.EJZysU/_new 2024-10-30 17:33:10.789099487 +0100
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-aiohappyeyeballs
-Version: 2.3.7
+Version: 2.4.3
Release: 0
Summary: Happy Eyeballs for asyncio
License: Python-2.0
++++++ aiohappyeyeballs-2.3.7.tar.gz -> aiohappyeyeballs-2.4.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiohappyeyeballs-2.3.7/PKG-INFO
new/aiohappyeyeballs-2.4.3/PKG-INFO
--- old/aiohappyeyeballs-2.3.7/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
+++ new/aiohappyeyeballs-2.4.3/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
@@ -1,9 +1,9 @@
Metadata-Version: 2.1
Name: aiohappyeyeballs
-Version: 2.3.7
+Version: 2.4.3
Summary: Happy Eyeballs for asyncio
Home-page: https://github.com/aio-libs/aiohappyeyeballs
-License: Python-2.0.1
+License: PSF-2.0
Author: J. Nick Koston
Author-email: [email protected]
Requires-Python: >=3.8
@@ -19,6 +19,7 @@
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Project-URL: Bug Tracker, https://github.com/aio-libs/aiohappyeyeballs/issues
Project-URL: Changelog,
https://github.com/aio-libs/aiohappyeyeballs/blob/main/CHANGELOG.md
@@ -43,8 +44,8 @@
<a href="https://python-poetry.org/">
<img
src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcX
KuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
</a>
- <a href="https://github.com/ambv/black">
- <img
src="https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square"
alt="black">
+ <a href="https://github.com/astral-sh/ruff">
+ <img
src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json"
alt="Ruff">
</a>
<a href="https://github.com/pre-commit/pre-commit">
<img
src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square"
alt="pre-commit">
@@ -80,7 +81,7 @@
The stdlib version of `loop.create_connection()`
will only work when you pass in an unresolved name which
is not a good fit when using DNS caching or resolving
-names via another method such was `zeroconf`.
+names via another method such as `zeroconf`.
## Installation
@@ -88,6 +89,10 @@
`pip install aiohappyeyeballs`
+## License
+
+[aiohappyeyeballs is licensed under the same terms as cpython
itself.](https://github.com/python/cpython/blob/main/LICENSE)
+
## Example usage
```python
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiohappyeyeballs-2.3.7/README.md
new/aiohappyeyeballs-2.4.3/README.md
--- old/aiohappyeyeballs-2.3.7/README.md 2024-08-17 15:00:11.435407200
+0200
+++ new/aiohappyeyeballs-2.4.3/README.md 2024-09-30 21:40:43.734043400
+0200
@@ -15,8 +15,8 @@
<a href="https://python-poetry.org/">
<img
src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcX
KuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
</a>
- <a href="https://github.com/ambv/black">
- <img
src="https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square"
alt="black">
+ <a href="https://github.com/astral-sh/ruff">
+ <img
src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json"
alt="Ruff">
</a>
<a href="https://github.com/pre-commit/pre-commit">
<img
src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square"
alt="pre-commit">
@@ -52,7 +52,7 @@
The stdlib version of `loop.create_connection()`
will only work when you pass in an unresolved name which
is not a good fit when using DNS caching or resolving
-names via another method such was `zeroconf`.
+names via another method such as `zeroconf`.
## Installation
@@ -60,6 +60,10 @@
`pip install aiohappyeyeballs`
+## License
+
+[aiohappyeyeballs is licensed under the same terms as cpython
itself.](https://github.com/python/cpython/blob/main/LICENSE)
+
## Example usage
```python
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiohappyeyeballs-2.3.7/pyproject.toml
new/aiohappyeyeballs-2.4.3/pyproject.toml
--- old/aiohappyeyeballs-2.3.7/pyproject.toml 2024-08-17 15:00:12.423407600
+0200
+++ new/aiohappyeyeballs-2.4.3/pyproject.toml 2024-09-30 21:40:44.716034200
+0200
@@ -1,9 +1,9 @@
[tool.poetry]
name = "aiohappyeyeballs"
-version = "2.3.7"
+version = "2.4.3"
description = "Happy Eyeballs for asyncio"
authors = ["J. Nick Koston <[email protected]>"]
-license = "Python-2.0.1"
+license = "PSF-2.0"
readme = "README.md"
repository = "https://github.com/aio-libs/aiohappyeyeballs"
documentation = "https://aiohappyeyeballs.readthedocs.io"
@@ -18,6 +18,7 @@
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"License :: OSI Approved :: Python Software Foundation License"
]
packages = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/aiohappyeyeballs-2.3.7/src/aiohappyeyeballs/__init__.py
new/aiohappyeyeballs-2.4.3/src/aiohappyeyeballs/__init__.py
--- old/aiohappyeyeballs-2.3.7/src/aiohappyeyeballs/__init__.py 2024-08-17
15:00:12.427407500 +0200
+++ new/aiohappyeyeballs-2.4.3/src/aiohappyeyeballs/__init__.py 2024-09-30
21:40:44.716034200 +0200
@@ -1,4 +1,4 @@
-__version__ = "2.3.7"
+__version__ = "2.4.3"
from .impl import start_connection
from .types import AddrInfoType
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/aiohappyeyeballs-2.3.7/src/aiohappyeyeballs/_staggered.py
new/aiohappyeyeballs-2.4.3/src/aiohappyeyeballs/_staggered.py
--- old/aiohappyeyeballs-2.3.7/src/aiohappyeyeballs/_staggered.py
1970-01-01 01:00:00.000000000 +0100
+++ new/aiohappyeyeballs-2.4.3/src/aiohappyeyeballs/_staggered.py
2024-09-30 21:40:43.735043300 +0200
@@ -0,0 +1,202 @@
+import asyncio
+import contextlib
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Awaitable,
+ Callable,
+ Iterable,
+ List,
+ Optional,
+ Set,
+ Tuple,
+ TypeVar,
+ Union,
+)
+
+_T = TypeVar("_T")
+
+
+def _set_result(wait_next: "asyncio.Future[None]") -> None:
+ """Set the result of a future if it is not already done."""
+ if not wait_next.done():
+ wait_next.set_result(None)
+
+
+async def _wait_one(
+ futures: "Iterable[asyncio.Future[Any]]",
+ loop: asyncio.AbstractEventLoop,
+) -> _T:
+ """Wait for the first future to complete."""
+ wait_next = loop.create_future()
+
+ def _on_completion(fut: "asyncio.Future[Any]") -> None:
+ if not wait_next.done():
+ wait_next.set_result(fut)
+
+ for f in futures:
+ f.add_done_callback(_on_completion)
+
+ try:
+ return await wait_next
+ finally:
+ for f in futures:
+ f.remove_done_callback(_on_completion)
+
+
+async def staggered_race(
+ coro_fns: Iterable[Callable[[], Awaitable[_T]]],
+ delay: Optional[float],
+ *,
+ loop: Optional[asyncio.AbstractEventLoop] = None,
+) -> Tuple[Optional[_T], Optional[int], List[Optional[BaseException]]]:
+ """
+ Run coroutines with staggered start times and take the first to finish.
+
+ This method takes an iterable of coroutine functions. The first one is
+ started immediately. From then on, whenever the immediately preceding one
+ fails (raises an exception), or when *delay* seconds has passed, the next
+ coroutine is started. This continues until one of the coroutines complete
+ successfully, in which case all others are cancelled, or until all
+ coroutines fail.
+
+ The coroutines provided should be well-behaved in the following way:
+
+ * They should only ``return`` if completed successfully.
+
+ * They should always raise an exception if they did not complete
+ successfully. In particular, if they handle cancellation, they should
+ probably reraise, like this::
+
+ try:
+ # do work
+ except asyncio.CancelledError:
+ # undo partially completed work
+ raise
+
+ Args:
+ ----
+ coro_fns: an iterable of coroutine functions, i.e. callables that
+ return a coroutine object when called. Use ``functools.partial`` or
+ lambdas to pass arguments.
+
+ delay: amount of time, in seconds, between starting coroutines. If
+ ``None``, the coroutines will run sequentially.
+
+ loop: the event loop to use. If ``None``, the running loop is used.
+
+ Returns:
+ -------
+ tuple *(winner_result, winner_index, exceptions)* where
+
+ - *winner_result*: the result of the winning coroutine, or ``None``
+ if no coroutines won.
+
+ - *winner_index*: the index of the winning coroutine in
+ ``coro_fns``, or ``None`` if no coroutines won. If the winning
+ coroutine may return None on success, *winner_index* can be used
+ to definitively determine whether any coroutine won.
+
+ - *exceptions*: list of exceptions returned by the coroutines.
+ ``len(exceptions)`` is equal to the number of coroutines actually
+ started, and the order is the same as in ``coro_fns``. The winning
+ coroutine's entry is ``None``.
+
+ """
+ loop = loop or asyncio.get_running_loop()
+ exceptions: List[Optional[BaseException]] = []
+ tasks: Set[asyncio.Task[Optional[Tuple[_T, int]]]] = set()
+
+ async def run_one_coro(
+ coro_fn: Callable[[], Awaitable[_T]],
+ this_index: int,
+ start_next: "asyncio.Future[None]",
+ ) -> Optional[Tuple[_T, int]]:
+ """
+ Run a single coroutine.
+
+ If the coroutine fails, set the exception in the exceptions list and
+ start the next coroutine by setting the result of the start_next.
+
+ If the coroutine succeeds, return the result and the index of the
+ coroutine in the coro_fns list.
+
+ If SystemExit or KeyboardInterrupt is raised, re-raise it.
+ """
+ try:
+ result = await coro_fn()
+ except (SystemExit, KeyboardInterrupt):
+ raise
+ except BaseException as e:
+ exceptions[this_index] = e
+ _set_result(start_next) # Kickstart the next coroutine
+ return None
+
+ return result, this_index
+
+ start_next_timer: Optional[asyncio.TimerHandle] = None
+ start_next: Optional[asyncio.Future[None]]
+ task: asyncio.Task[Optional[Tuple[_T, int]]]
+ done: Union[asyncio.Future[None], asyncio.Task[Optional[Tuple[_T, int]]]]
+ coro_iter = iter(coro_fns)
+ this_index = -1
+ try:
+ while True:
+ if coro_fn := next(coro_iter, None):
+ this_index += 1
+ exceptions.append(None)
+ start_next = loop.create_future()
+ task = loop.create_task(run_one_coro(coro_fn, this_index,
start_next))
+ tasks.add(task)
+ start_next_timer = (
+ loop.call_later(delay, _set_result, start_next) if delay
else None
+ )
+ elif not tasks:
+ # We exhausted the coro_fns list and no tasks are running
+ # so we have no winner and all coroutines failed.
+ break
+
+ while tasks:
+ done = await _wait_one(
+ [*tasks, start_next] if start_next else tasks, loop
+ )
+ if done is start_next:
+ # The current task has failed or the timer has expired
+ # so we need to start the next task.
+ start_next = None
+ if start_next_timer:
+ start_next_timer.cancel()
+ start_next_timer = None
+
+ # Break out of the task waiting loop to start the next
+ # task.
+ break
+
+ if TYPE_CHECKING:
+ assert isinstance(done, asyncio.Task)
+
+ tasks.remove(done)
+ if winner := done.result():
+ return *winner, exceptions
+ finally:
+ # We either have:
+ # - a winner
+ # - all tasks failed
+ # - a KeyboardInterrupt or SystemExit.
+
+ #
+ # If the timer is still running, cancel it.
+ #
+ if start_next_timer:
+ start_next_timer.cancel()
+
+ #
+ # If there are any tasks left, cancel them and than
+ # wait them so they fill the exceptions list.
+ #
+ for task in tasks:
+ task.cancel()
+ with contextlib.suppress(asyncio.CancelledError):
+ await task
+
+ return None, None, exceptions
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiohappyeyeballs-2.3.7/src/aiohappyeyeballs/impl.py
new/aiohappyeyeballs-2.4.3/src/aiohappyeyeballs/impl.py
--- old/aiohappyeyeballs-2.3.7/src/aiohappyeyeballs/impl.py 2024-08-17
15:00:11.435407200 +0200
+++ new/aiohappyeyeballs-2.4.3/src/aiohappyeyeballs/impl.py 2024-09-30
21:40:43.735043300 +0200
@@ -6,9 +6,9 @@
import itertools
import socket
import sys
-from asyncio import staggered
-from typing import List, Optional, Sequence
+from typing import List, Optional, Sequence, Union
+from . import _staggered
from .types import AddrInfoType
if sys.version_info < (3, 8, 2): # noqa: UP036
@@ -73,7 +73,8 @@
addr_infos = _interleave_addrinfos(addr_infos, interleave)
sock: Optional[socket.socket] = None
- exceptions: List[List[OSError]] = []
+ # uvloop can raise RuntimeError instead of OSError
+ exceptions: List[List[Union[OSError, RuntimeError]]] = []
if happy_eyeballs_delay is None or single_addr_info:
# not using happy eyeballs
for addrinfo in addr_infos:
@@ -82,10 +83,10 @@
current_loop, exceptions, addrinfo, local_addr_infos
)
break
- except OSError:
+ except (RuntimeError, OSError):
continue
else: # using happy eyeballs
- sock, _, _ = await staggered.staggered_race(
+ sock, _, _ = await _staggered.staggered_race(
(
functools.partial(
_connect_sock, current_loop, exceptions, addrinfo,
local_addr_infos
@@ -93,7 +94,6 @@
for addrinfo in addr_infos
),
happy_eyeballs_delay,
- loop=current_loop,
)
if sock is None:
@@ -114,12 +114,20 @@
)
# If the errno is the same for all exceptions, raise
# an OSError with that errno.
- first_errno = first_exception.errno
- if all(
- isinstance(exc, OSError) and exc.errno == first_errno
- for exc in all_exceptions
+ if isinstance(first_exception, OSError):
+ first_errno = first_exception.errno
+ if all(
+ isinstance(exc, OSError) and exc.errno == first_errno
+ for exc in all_exceptions
+ ):
+ raise OSError(first_errno, msg)
+ elif isinstance(first_exception, RuntimeError) and all(
+ isinstance(exc, RuntimeError) for exc in all_exceptions
):
- raise OSError(first_errno, msg)
+ raise RuntimeError(msg)
+ # We have a mix of OSError and RuntimeError
+ # so we have to pick which one to raise.
+ # and we raise OSError for compatibility
raise OSError(msg)
finally:
all_exceptions = None # type: ignore[assignment]
@@ -130,12 +138,12 @@
async def _connect_sock(
loop: asyncio.AbstractEventLoop,
- exceptions: List[List[OSError]],
+ exceptions: List[List[Union[OSError, RuntimeError]]],
addr_info: AddrInfoType,
local_addr_infos: Optional[Sequence[AddrInfoType]] = None,
) -> socket.socket:
"""Create, bind and connect one socket."""
- my_exceptions: list[OSError] = []
+ my_exceptions: List[Union[OSError, RuntimeError]] = []
exceptions.append(my_exceptions)
family, type_, proto, _, address = addr_info
sock = None
@@ -165,7 +173,7 @@
raise OSError(f"no matching local address with {family=}
found")
await loop.sock_connect(sock, address)
return sock
- except OSError as exc:
+ except (RuntimeError, OSError) as exc:
my_exceptions.append(exc)
if sock is not None:
sock.close()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiohappyeyeballs-2.3.7/src/aiohappyeyeballs/utils.py
new/aiohappyeyeballs-2.4.3/src/aiohappyeyeballs/utils.py
--- old/aiohappyeyeballs-2.3.7/src/aiohappyeyeballs/utils.py 2024-08-17
15:00:11.435407200 +0200
+++ new/aiohappyeyeballs-2.4.3/src/aiohappyeyeballs/utils.py 2024-09-30
21:40:43.735043300 +0200
@@ -10,7 +10,7 @@
def addr_to_addr_infos(
addr: Optional[
Union[Tuple[str, int, int, int], Tuple[str, int, int], Tuple[str, int]]
- ]
+ ],
) -> Optional[List[AddrInfoType]]:
"""Convert an address tuple to a list of addr_info tuples."""
if addr is None:
@@ -59,7 +59,7 @@
def _addr_tuple_to_ip_address(
- addr: Union[Tuple[str, int], Tuple[str, int, int, int]]
+ addr: Union[Tuple[str, int], Tuple[str, int, int, int]],
) -> Union[
Tuple[ipaddress.IPv4Address, int], Tuple[ipaddress.IPv6Address, int, int,
int]
]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiohappyeyeballs-2.3.7/tests/conftest.py
new/aiohappyeyeballs-2.4.3/tests/conftest.py
--- old/aiohappyeyeballs-2.3.7/tests/conftest.py 1970-01-01
01:00:00.000000000 +0100
+++ new/aiohappyeyeballs-2.4.3/tests/conftest.py 2024-09-30
21:40:43.735043300 +0200
@@ -0,0 +1,32 @@
+"""Configuration for the tests."""
+
+import asyncio
+import threading
+from typing import Generator
+
+import pytest
+
+
[email protected](autouse=True)
+def verify_threads_ended():
+ """Verify that the threads are not running after the test."""
+ threads_before = frozenset(threading.enumerate())
+ yield
+ threads = frozenset(threading.enumerate()) - threads_before
+ assert not threads
+
+
[email protected](autouse=True)
+def verify_no_lingering_tasks(
+ event_loop: asyncio.AbstractEventLoop,
+) -> Generator[None, None, None]:
+ """Verify that all tasks are cleaned up."""
+ tasks_before = asyncio.all_tasks(event_loop)
+ yield
+
+ tasks = asyncio.all_tasks(event_loop) - tasks_before
+ for task in tasks:
+ pytest.fail(f"Task still running: {task!r}")
+ task.cancel()
+ if tasks:
+ event_loop.run_until_complete(asyncio.wait(tasks))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiohappyeyeballs-2.3.7/tests/test_impl.py
new/aiohappyeyeballs-2.4.3/tests/test_impl.py
--- old/aiohappyeyeballs-2.3.7/tests/test_impl.py 2024-08-17
15:00:11.435407200 +0200
+++ new/aiohappyeyeballs-2.4.3/tests/test_impl.py 2024-09-30
21:40:43.735043300 +0200
@@ -1368,6 +1368,458 @@
]
+@patch_socket
[email protected]
+async def test_uvloop_runtime_error(
+ m_socket: ModuleType,
+) -> None:
+ """
+ Test RuntimeError is handled when connecting a socket with uvloop.
+
+ Connecting a socket can raise a RuntimeError, OSError or ValueError.
+
+ - OSError: If the address is invalid or the connection fails.
+ - ValueError: if a non-sock it passed (this should never happen).
+
https://github.com/python/cpython/blob/e44eebfc1eccdaaebc219accbfc705c9a9de068d/Lib/asyncio/selector_events.py#L271
+ - RuntimeError: If the file descriptor is already in use by a transport.
+
+ We should never get ValueError since we are using the correct types.
+
+ selector_events.py never seems to raise a RuntimeError, but it is possible
+ with uvloop. This test is to ensure that we handle it correctly.
+ """
+ mock_socket = mock.MagicMock(
+ family=socket.AF_INET,
+ type=socket.SOCK_STREAM,
+ proto=socket.IPPROTO_TCP,
+ fileno=mock.MagicMock(return_value=1),
+ )
+ create_calls = []
+
+ def _socket(*args, **kw):
+ for attr in kw:
+ setattr(mock_socket, attr, kw[attr])
+ return mock_socket
+
+ async def _sock_connect(
+ sock: socket.socket, address: Tuple[str, int, int, int]
+ ) -> None:
+ create_calls.append(address)
+ raise RuntimeError("all fail")
+
+ m_socket.socket = _socket # type: ignore
+ ipv6_addr_info = (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("dead:beef::", 80, 0, 0),
+ )
+ ipv6_addr_info_2 = (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("dead:aaaa::", 80, 0, 0),
+ )
+ ipv4_addr_info = (
+ socket.AF_INET,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("107.6.106.83", 80),
+ )
+ addr_info = [ipv6_addr_info, ipv6_addr_info_2, ipv4_addr_info]
+ local_addr_infos = [
+ (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("::1", 0, 0, 0),
+ ),
+ (
+ socket.AF_INET,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("127.0.0.1", 0),
+ ),
+ ]
+ loop = asyncio.get_running_loop()
+ # We should get the same exception raised if they are all the same
+ with mock.patch.object(loop, "sock_connect", _sock_connect), pytest.raises(
+ RuntimeError, match="all fail"
+ ):
+ assert (
+ await start_connection(
+ addr_info,
+ happy_eyeballs_delay=0.3,
+ interleave=2,
+ local_addr_infos=local_addr_infos,
+ )
+ == mock_socket
+ )
+
+ # All calls failed
+ assert create_calls == [
+ ("dead:beef::", 80, 0, 0),
+ ("dead:aaaa::", 80, 0, 0),
+ ("107.6.106.83", 80),
+ ]
+
+
+@patch_socket
[email protected]
+async def test_uvloop_different_runtime_error(
+ m_socket: ModuleType,
+) -> None:
+ """Test different RuntimeErrors are handled when connecting a socket with
uvloop."""
+ mock_socket = mock.MagicMock(
+ family=socket.AF_INET,
+ type=socket.SOCK_STREAM,
+ proto=socket.IPPROTO_TCP,
+ fileno=mock.MagicMock(return_value=1),
+ )
+ create_calls = []
+ counter = 0
+
+ def _socket(*args, **kw):
+ for attr in kw:
+ setattr(mock_socket, attr, kw[attr])
+ return mock_socket
+
+ async def _sock_connect(
+ sock: socket.socket, address: Tuple[str, int, int, int]
+ ) -> None:
+ create_calls.append(address)
+ nonlocal counter
+ counter += 1
+ raise RuntimeError(counter)
+
+ m_socket.socket = _socket # type: ignore
+ ipv6_addr_info = (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("dead:beef::", 80, 0, 0),
+ )
+ ipv6_addr_info_2 = (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("dead:aaaa::", 80, 0, 0),
+ )
+ ipv4_addr_info = (
+ socket.AF_INET,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("107.6.106.83", 80),
+ )
+ addr_info = [ipv6_addr_info, ipv6_addr_info_2, ipv4_addr_info]
+ local_addr_infos = [
+ (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("::1", 0, 0, 0),
+ ),
+ (
+ socket.AF_INET,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("127.0.0.1", 0),
+ ),
+ ]
+ loop = asyncio.get_running_loop()
+ # We should get the same exception raised if they are all the same
+ with mock.patch.object(loop, "sock_connect", _sock_connect), pytest.raises(
+ RuntimeError, match="Multiple exceptions: 1, 2, 3"
+ ):
+ assert (
+ await start_connection(
+ addr_info,
+ happy_eyeballs_delay=0.3,
+ interleave=2,
+ local_addr_infos=local_addr_infos,
+ )
+ == mock_socket
+ )
+
+ # All calls failed
+ assert create_calls == [
+ ("dead:beef::", 80, 0, 0),
+ ("dead:aaaa::", 80, 0, 0),
+ ("107.6.106.83", 80),
+ ]
+
+
+@patch_socket
[email protected]
+async def test_uvloop_mixing_os_and_runtime_error(
+ m_socket: ModuleType,
+) -> None:
+ """Test uvloop raising OSError and RuntimeError."""
+ mock_socket = mock.MagicMock(
+ family=socket.AF_INET,
+ type=socket.SOCK_STREAM,
+ proto=socket.IPPROTO_TCP,
+ fileno=mock.MagicMock(return_value=1),
+ )
+ create_calls = []
+ counter = 0
+
+ def _socket(*args, **kw):
+ for attr in kw:
+ setattr(mock_socket, attr, kw[attr])
+ return mock_socket
+
+ async def _sock_connect(
+ sock: socket.socket, address: Tuple[str, int, int, int]
+ ) -> None:
+ create_calls.append(address)
+ nonlocal counter
+ counter += 1
+ if counter == 1:
+ raise RuntimeError(counter)
+ raise OSError(counter, f"all fail {counter}")
+
+ m_socket.socket = _socket # type: ignore
+ ipv6_addr_info = (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("dead:beef::", 80, 0, 0),
+ )
+ ipv6_addr_info_2 = (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("dead:aaaa::", 80, 0, 0),
+ )
+ ipv4_addr_info = (
+ socket.AF_INET,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("107.6.106.83", 80),
+ )
+ addr_info = [ipv6_addr_info, ipv6_addr_info_2, ipv4_addr_info]
+ local_addr_infos = [
+ (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("::1", 0, 0, 0),
+ ),
+ (
+ socket.AF_INET,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("127.0.0.1", 0),
+ ),
+ ]
+ loop = asyncio.get_running_loop()
+ # We should get the same exception raised if they are all the same
+ with mock.patch.object(loop, "sock_connect", _sock_connect), pytest.raises(
+ OSError, match="Multiple exceptions: 1"
+ ):
+ assert (
+ await start_connection(
+ addr_info,
+ happy_eyeballs_delay=0.3,
+ interleave=2,
+ local_addr_infos=local_addr_infos,
+ )
+ == mock_socket
+ )
+
+ # All calls failed
+ assert create_calls == [
+ ("dead:beef::", 80, 0, 0),
+ ("dead:aaaa::", 80, 0, 0),
+ ("107.6.106.83", 80),
+ ]
+
+
+@patch_socket
[email protected]
[email protected](reason="raises RuntimeError: coroutine ignored
GeneratorExit")
+async def test_handling_system_exit(
+ m_socket: ModuleType,
+) -> None:
+ """Test handling SystemExit."""
+ mock_socket = mock.MagicMock(
+ family=socket.AF_INET,
+ type=socket.SOCK_STREAM,
+ proto=socket.IPPROTO_TCP,
+ fileno=mock.MagicMock(return_value=1),
+ )
+ create_calls = []
+
+ def _socket(*args, **kw):
+ for attr in kw:
+ setattr(mock_socket, attr, kw[attr])
+ return mock_socket
+
+ async def _sock_connect(
+ sock: socket.socket, address: Tuple[str, int, int, int]
+ ) -> None:
+ create_calls.append(address)
+ raise SystemExit
+
+ m_socket.socket = _socket # type: ignore
+ ipv6_addr_info = (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("dead:beef::", 80, 0, 0),
+ )
+ ipv6_addr_info_2 = (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("dead:aaaa::", 80, 0, 0),
+ )
+ ipv4_addr_info = (
+ socket.AF_INET,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("107.6.106.83", 80),
+ )
+ addr_info = [ipv6_addr_info, ipv6_addr_info_2, ipv4_addr_info]
+ local_addr_infos = [
+ (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("::1", 0, 0, 0),
+ ),
+ (
+ socket.AF_INET,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("127.0.0.1", 0),
+ ),
+ ]
+ loop = asyncio.get_running_loop()
+ with pytest.raises(SystemExit), mock.patch.object(
+ loop, "sock_connect", _sock_connect
+ ):
+ await start_connection(
+ addr_info,
+ happy_eyeballs_delay=0.3,
+ interleave=2,
+ local_addr_infos=local_addr_infos,
+ )
+
+ # Stopped after the first call
+ assert create_calls == [
+ ("dead:beef::", 80, 0, 0),
+ ]
+
+
+@patch_socket
[email protected]
+async def test_cancellation_is_not_swallowed(
+ m_socket: ModuleType,
+) -> None:
+ """Test that cancellation is not swallowed."""
+ mock_socket = mock.MagicMock(
+ family=socket.AF_INET,
+ type=socket.SOCK_STREAM,
+ proto=socket.IPPROTO_TCP,
+ fileno=mock.MagicMock(return_value=1),
+ )
+ create_calls = []
+
+ def _socket(*args, **kw):
+ for attr in kw:
+ setattr(mock_socket, attr, kw[attr])
+ return mock_socket
+
+ async def _sock_connect(
+ sock: socket.socket, address: Tuple[str, int, int, int]
+ ) -> None:
+ create_calls.append(address)
+ await asyncio.sleep(1000)
+
+ m_socket.socket = _socket # type: ignore
+ ipv6_addr_info = (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("dead:beef::", 80, 0, 0),
+ )
+ ipv6_addr_info_2 = (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("dead:aaaa::", 80, 0, 0),
+ )
+ ipv4_addr_info = (
+ socket.AF_INET,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("107.6.106.83", 80),
+ )
+ addr_info = [ipv6_addr_info, ipv6_addr_info_2, ipv4_addr_info]
+ local_addr_infos = [
+ (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("::1", 0, 0, 0),
+ ),
+ (
+ socket.AF_INET,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ "",
+ ("127.0.0.1", 0),
+ ),
+ ]
+ loop = asyncio.get_running_loop()
+ # We should get the same exception raised if they are all the same
+ with mock.patch.object(loop, "sock_connect", _sock_connect), pytest.raises(
+ asyncio.CancelledError
+ ):
+ task = asyncio.create_task(
+ start_connection(
+ addr_info,
+ happy_eyeballs_delay=0.3,
+ interleave=2,
+ local_addr_infos=local_addr_infos,
+ )
+ )
+ await asyncio.sleep(0)
+ task.cancel()
+ await task
+
+ # After calls are cancelled now more are made
+ assert create_calls == [
+ ("dead:beef::", 80, 0, 0),
+ ]
+
+
@pytest.mark.asyncio
@pytest.mark.skipif(sys.version_info >= (3, 8, 2), reason="requires < python
3.8.2")
def test_python_38_compat() -> None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/aiohappyeyeballs-2.3.7/tests/test_staggered.py
new/aiohappyeyeballs-2.4.3/tests/test_staggered.py
--- old/aiohappyeyeballs-2.3.7/tests/test_staggered.py 1970-01-01
01:00:00.000000000 +0100
+++ new/aiohappyeyeballs-2.4.3/tests/test_staggered.py 2024-09-30
21:40:43.735043300 +0200
@@ -0,0 +1,86 @@
+import asyncio
+import sys
+from functools import partial
+
+import pytest
+
+from aiohappyeyeballs._staggered import staggered_race
+
+
[email protected]
+async def test_one_winners():
+ """Test that there is only one winner when there is no await in the
coro."""
+ winners = []
+
+ async def coro(idx):
+ winners.append(idx)
+ return idx
+
+ coros = [partial(coro, idx) for idx in range(4)]
+
+ winner, index, excs = await staggered_race(
+ coros,
+ delay=None,
+ )
+ assert len(winners) == 1
+ assert winners == [0]
+ assert winner == 0
+ assert index == 0
+ assert excs == [None]
+
+
[email protected]
+async def test_multiple_winners():
+ """Test multiple winners are handled correctly."""
+ loop = asyncio.get_running_loop()
+ winners = []
+ finish = loop.create_future()
+
+ async def coro(idx):
+ await finish
+ winners.append(idx)
+ return idx
+
+ coros = [partial(coro, idx) for idx in range(4)]
+
+ task = loop.create_task(staggered_race(coros, delay=0.00001))
+ await asyncio.sleep(0.1)
+ loop.call_soon(finish.set_result, None)
+ winner, index, excs = await task
+ assert len(winners) == 4
+ assert winners == [0, 1, 2, 3]
+ assert winner == 0
+ assert index == 0
+ assert excs == [None, None, None, None]
+
+
[email protected](sys.version_info < (3, 12), reason="requires python3.12 or
higher")
+def test_multiple_winners_eager_task_factory():
+ """Test multiple winners are handled correctly."""
+ loop = asyncio.new_event_loop()
+ eager_task_factory = asyncio.create_eager_task_factory(asyncio.Task)
+ loop.set_task_factory(eager_task_factory)
+ asyncio.set_event_loop(None)
+
+ async def run():
+ winners = []
+ finish = loop.create_future()
+
+ async def coro(idx):
+ await finish
+ winners.append(idx)
+ return idx
+
+ coros = [partial(coro, idx) for idx in range(4)]
+
+ task = loop.create_task(staggered_race(coros, delay=0.00001))
+ await asyncio.sleep(0.1)
+ loop.call_soon(finish.set_result, None)
+ winner, index, excs = await task
+ assert len(winners) == 4
+ assert winners == [0, 1, 2, 3]
+ assert winner == 0
+ assert index == 0
+ assert excs == [None, None, None, None]
+
+ loop.run_until_complete(run())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/aiohappyeyeballs-2.3.7/tests/test_staggered_cpython.py
new/aiohappyeyeballs-2.4.3/tests/test_staggered_cpython.py
--- old/aiohappyeyeballs-2.3.7/tests/test_staggered_cpython.py 1970-01-01
01:00:00.000000000 +0100
+++ new/aiohappyeyeballs-2.4.3/tests/test_staggered_cpython.py 2024-09-30
21:40:43.735043300 +0200
@@ -0,0 +1,146 @@
+"""
+Tests for staggered_race.
+
+These tests are copied from cpython to ensure our implementation is
+compatible with the one in cpython.
+"""
+
+import asyncio
+import unittest
+
+from aiohappyeyeballs._staggered import staggered_race
+
+
+def tearDownModule():
+ asyncio.set_event_loop_policy(None)
+
+
+class StaggeredTests(unittest.IsolatedAsyncioTestCase):
+ async def test_empty(self):
+ winner, index, excs = await staggered_race(
+ [],
+ delay=None,
+ )
+
+ self.assertIs(winner, None)
+ self.assertIs(index, None)
+ self.assertEqual(excs, [])
+
+ async def test_one_successful(self):
+ async def coro(index):
+ return f"Res: {index}"
+
+ winner, index, excs = await staggered_race(
+ [
+ lambda: coro(0),
+ lambda: coro(1),
+ ],
+ delay=None,
+ )
+
+ self.assertEqual(winner, "Res: 0")
+ self.assertEqual(index, 0)
+ self.assertEqual(excs, [None])
+
+ async def test_first_error_second_successful(self):
+ async def coro(index):
+ if index == 0:
+ raise ValueError(index)
+ return f"Res: {index}"
+
+ winner, index, excs = await staggered_race(
+ [
+ lambda: coro(0),
+ lambda: coro(1),
+ ],
+ delay=None,
+ )
+
+ self.assertEqual(winner, "Res: 1")
+ self.assertEqual(index, 1)
+ self.assertEqual(len(excs), 2)
+ self.assertIsInstance(excs[0], ValueError)
+ self.assertIs(excs[1], None)
+
+ async def test_first_timeout_second_successful(self):
+ async def coro(index):
+ if index == 0:
+ await asyncio.sleep(10) # much bigger than delay
+ return f"Res: {index}"
+
+ winner, index, excs = await staggered_race(
+ [
+ lambda: coro(0),
+ lambda: coro(1),
+ ],
+ delay=0.1,
+ )
+
+ self.assertEqual(winner, "Res: 1")
+ self.assertEqual(index, 1)
+ self.assertEqual(len(excs), 2)
+ self.assertIsInstance(excs[0], asyncio.CancelledError)
+ self.assertIs(excs[1], None)
+
+ async def test_none_successful(self):
+ async def coro(index):
+ raise ValueError(index)
+
+ for delay in [None, 0, 0.1, 1]:
+ with self.subTest(delay=delay):
+ winner, index, excs = await staggered_race(
+ [
+ lambda: coro(0),
+ lambda: coro(1),
+ ],
+ delay=delay,
+ )
+
+ self.assertIs(winner, None)
+ self.assertIs(index, None)
+ self.assertEqual(len(excs), 2)
+ self.assertIsInstance(excs[0], ValueError)
+ self.assertIsInstance(excs[1], ValueError)
+
+ async def test_long_delay_early_failure(self):
+ async def coro(index):
+ await asyncio.sleep(0) # Dummy coroutine for the 1 case
+ if index == 0:
+ await asyncio.sleep(0.1) # Dummy coroutine
+ raise ValueError(index)
+
+ return f"Res: {index}"
+
+ winner, index, excs = await staggered_race(
+ [
+ lambda: coro(0),
+ lambda: coro(1),
+ ],
+ delay=10,
+ )
+
+ self.assertEqual(winner, "Res: 1")
+ self.assertEqual(index, 1)
+ self.assertEqual(len(excs), 2)
+ self.assertIsInstance(excs[0], ValueError)
+ self.assertIsNone(excs[1])
+
+ def test_loop_argument(self):
+ loop = asyncio.new_event_loop()
+
+ async def coro():
+ self.assertEqual(loop, asyncio.get_running_loop())
+ return "coro"
+
+ async def main():
+ winner, index, excs = await staggered_race([coro], delay=0.1,
loop=loop)
+
+ self.assertEqual(winner, "coro")
+ self.assertEqual(index, 0)
+
+ loop.run_until_complete(main())
+ loop.close()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/aiohappyeyeballs-2.3.7/tests/test_staggered_cpython_eager_task_factory.py
new/aiohappyeyeballs-2.4.3/tests/test_staggered_cpython_eager_task_factory.py
---
old/aiohappyeyeballs-2.3.7/tests/test_staggered_cpython_eager_task_factory.py
1970-01-01 01:00:00.000000000 +0100
+++
new/aiohappyeyeballs-2.4.3/tests/test_staggered_cpython_eager_task_factory.py
2024-09-30 21:40:43.735043300 +0200
@@ -0,0 +1,96 @@
+"""
+Tests staggered_race and eager_task_factory with asyncio.Task.
+
+These tests are copied from cpython to ensure our implementation is
+compatible with the one in cpython.
+"""
+
+import asyncio
+import sys
+import unittest
+
+from aiohappyeyeballs._staggered import staggered_race
+
+
+def tearDownModule():
+ asyncio.set_event_loop_policy(None)
+
+
+class EagerTaskFactoryLoopTests(unittest.TestCase):
+ def close_loop(self, loop):
+ loop.close()
+
+ def set_event_loop(self, loop, *, cleanup=True):
+ if loop is None:
+ raise AssertionError("loop is None")
+ # ensure that the event loop is passed explicitly in asyncio
+ asyncio.set_event_loop(None)
+ if cleanup:
+ self.addCleanup(self.close_loop, loop)
+
+ def tearDown(self):
+ asyncio.set_event_loop(None)
+ self.doCleanups()
+
+ def setUp(self):
+ if sys.version_info < (3, 12):
+ self.skipTest("eager_task_factory is only available in Python
3.12+")
+
+ super().setUp()
+ self.loop = asyncio.new_event_loop()
+ self.eager_task_factory =
asyncio.create_eager_task_factory(asyncio.Task)
+ self.loop.set_task_factory(self.eager_task_factory)
+ self.set_event_loop(self.loop)
+
+ def test_staggered_race_with_eager_tasks(self):
+ # See https://github.com/python/cpython/issues/124309
+
+ async def fail():
+ await asyncio.sleep(0)
+ raise ValueError("no good")
+
+ async def run():
+ winner, index, excs = await staggered_race(
+ [
+ lambda: asyncio.sleep(2, result="sleep2"),
+ lambda: asyncio.sleep(1, result="sleep1"),
+ lambda: fail(),
+ ],
+ delay=0.25,
+ )
+ self.assertEqual(winner, "sleep1")
+ self.assertEqual(index, 1)
+ assert index is not None
+ self.assertIsNone(excs[index])
+ self.assertIsInstance(excs[0], asyncio.CancelledError)
+ self.assertIsInstance(excs[2], ValueError)
+
+ self.loop.run_until_complete(run())
+
+ def test_staggered_race_with_eager_tasks_no_delay(self):
+ # See https://github.com/python/cpython/issues/124309
+ async def fail():
+ raise ValueError("no good")
+
+ async def run():
+ winner, index, excs = await staggered_race(
+ [
+ lambda: fail(),
+ lambda: asyncio.sleep(1, result="sleep1"),
+ lambda: asyncio.sleep(0, result="sleep0"),
+ ],
+ delay=None,
+ )
+ self.assertEqual(winner, "sleep1")
+ self.assertEqual(index, 1)
+ assert index is not None
+ self.assertIsNone(excs[index])
+ self.assertIsInstance(excs[0], ValueError)
+ self.assertEqual(len(excs), 2)
+
+ self.loop.run_until_complete(run())
+
+
+if __name__ == "__main__":
+ if sys.version_info >= (3, 12):
+ unittest.main()