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 <mardnh@gmx.de> + +- 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: nick@koston.org 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..." 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..." 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 <nick@koston.org>"] -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 + + +@pytest.fixture(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 + + +@pytest.fixture(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 +@pytest.mark.asyncio +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/e44eebfc1eccdaaebc219accbfc705c9a9de0... + - 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 +@pytest.mark.asyncio +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 +@pytest.mark.asyncio +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 +@pytest.mark.asyncio +@pytest.mark.xfail(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 +@pytest.mark.asyncio +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 + + +@pytest.mark.asyncio +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] + + +@pytest.mark.asyncio +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] + + +@pytest.mark.skipif(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()