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()

Reply via email to