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 2025-03-05 13:39:29 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-aiohappyeyeballs (Old) and /work/SRC/openSUSE:Factory/.python-aiohappyeyeballs.new.19136 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-aiohappyeyeballs" Wed Mar 5 13:39:29 2025 rev:6 rq:1250024 version:2.4.8 Changes: -------- --- /work/SRC/openSUSE:Factory/python-aiohappyeyeballs/python-aiohappyeyeballs.changes 2025-02-11 21:20:47.900765945 +0100 +++ /work/SRC/openSUSE:Factory/.python-aiohappyeyeballs.new.19136/python-aiohappyeyeballs.changes 2025-03-05 13:39:58.627183084 +0100 @@ -1,0 +2,13 @@ +Tue Mar 4 10:07:59 UTC 2025 - Martin Hauke <mar...@gmx.de> + +- Update to version 2.4.8 + Bug Fixes + * Close runner up sockets in the event there are multiple winners +- Update to version 2.4.7 + Bug Fixes + * Resolve warnings when running tests. + * Instead of raising SystemExit which causes a RuntimeError, + mock out SystemExit to a new exception. + * Make sure the event loop is closed in tests. + +------------------------------------------------------------------- Old: ---- aiohappyeyeballs-2.4.6.tar.gz New: ---- aiohappyeyeballs-2.4.8.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-aiohappyeyeballs.spec ++++++ --- /var/tmp/diff_new_pack.X0FVam/_old 2025-03-05 13:39:59.207207416 +0100 +++ /var/tmp/diff_new_pack.X0FVam/_new 2025-03-05 13:39:59.211207584 +0100 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-aiohappyeyeballs -Version: 2.4.6 +Version: 2.4.8 Release: 0 Summary: Happy Eyeballs for asyncio License: Python-2.0 ++++++ aiohappyeyeballs-2.4.6.tar.gz -> aiohappyeyeballs-2.4.8.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiohappyeyeballs-2.4.6/PKG-INFO new/aiohappyeyeballs-2.4.8/PKG-INFO --- old/aiohappyeyeballs-2.4.6/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 +++ new/aiohappyeyeballs-2.4.8/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.3 Name: aiohappyeyeballs -Version: 2.4.6 +Version: 2.4.8 Summary: Happy Eyeballs for asyncio License: PSF-2.0 Author: J. Nick Koston diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiohappyeyeballs-2.4.6/pyproject.toml new/aiohappyeyeballs-2.4.8/pyproject.toml --- old/aiohappyeyeballs-2.4.6/pyproject.toml 1970-01-01 01:00:00.000000000 +0100 +++ new/aiohappyeyeballs-2.4.8/pyproject.toml 1970-01-01 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ [project] name = "aiohappyeyeballs" -version = "2.4.6" +version = "2.4.8" description = "Happy Eyeballs for asyncio" authors = [{ name = "J. Nick Koston", email = "n...@koston.org" }] readme = "README.md" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiohappyeyeballs-2.4.6/src/aiohappyeyeballs/__init__.py new/aiohappyeyeballs-2.4.8/src/aiohappyeyeballs/__init__.py --- old/aiohappyeyeballs-2.4.6/src/aiohappyeyeballs/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ new/aiohappyeyeballs-2.4.8/src/aiohappyeyeballs/__init__.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,4 +1,4 @@ -__version__ = "2.4.6" +__version__ = "2.4.8" from .impl import start_connection from .types import AddrInfoType diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiohappyeyeballs-2.4.6/src/aiohappyeyeballs/_staggered.py new/aiohappyeyeballs-2.4.8/src/aiohappyeyeballs/_staggered.py --- old/aiohappyeyeballs-2.4.6/src/aiohappyeyeballs/_staggered.py 1970-01-01 01:00:00.000000000 +0100 +++ new/aiohappyeyeballs-2.4.8/src/aiohappyeyeballs/_staggered.py 1970-01-01 01:00:00.000000000 +0100 @@ -16,6 +16,8 @@ _T = TypeVar("_T") +RE_RAISE_EXCEPTIONS = (SystemExit, KeyboardInterrupt) + def _set_result(wait_next: "asyncio.Future[None]") -> None: """Set the result of a future if it is not already done.""" @@ -125,7 +127,7 @@ """ try: result = await coro_fn() - except (SystemExit, KeyboardInterrupt): + except RE_RAISE_EXCEPTIONS: raise except BaseException as e: exceptions[this_index] = e diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiohappyeyeballs-2.4.6/src/aiohappyeyeballs/impl.py new/aiohappyeyeballs-2.4.8/src/aiohappyeyeballs/impl.py --- old/aiohappyeyeballs-2.4.6/src/aiohappyeyeballs/impl.py 1970-01-01 01:00:00.000000000 +0100 +++ new/aiohappyeyeballs-2.4.8/src/aiohappyeyeballs/impl.py 1970-01-01 01:00:00.000000000 +0100 @@ -2,10 +2,11 @@ import asyncio import collections +import contextlib import functools import itertools import socket -from typing import List, Optional, Sequence, Union +from typing import List, Optional, Sequence, Set, Union from . import _staggered from .types import AddrInfoType @@ -75,15 +76,36 @@ except (RuntimeError, OSError): continue else: # using happy eyeballs - sock, _, _ = await _staggered.staggered_race( - ( - functools.partial( - _connect_sock, current_loop, exceptions, addrinfo, local_addr_infos - ) - for addrinfo in addr_infos - ), - happy_eyeballs_delay, - ) + open_sockets: Set[socket.socket] = set() + try: + sock, _, _ = await _staggered.staggered_race( + ( + functools.partial( + _connect_sock, + current_loop, + exceptions, + addrinfo, + local_addr_infos, + open_sockets, + ) + for addrinfo in addr_infos + ), + happy_eyeballs_delay, + ) + finally: + # If we have a winner, staggered_race will + # cancel the other tasks, however there is a + # small race window where any of the other tasks + # can be done before they are cancelled which + # will leave the socket open. To avoid this problem + # we pass a set to _connect_sock to keep track of + # the open sockets and close them here if there + # are any "runner up" sockets. + for s in open_sockets: + if s is not sock: + with contextlib.suppress(OSError): + s.close() + open_sockets = None # type: ignore[assignment] if sock is None: all_exceptions = [exc for sub in exceptions for exc in sub] @@ -130,14 +152,26 @@ exceptions: List[List[Union[OSError, RuntimeError]]], addr_info: AddrInfoType, local_addr_infos: Optional[Sequence[AddrInfoType]] = None, + open_sockets: Optional[Set[socket.socket]] = None, ) -> socket.socket: - """Create, bind and connect one socket.""" + """ + Create, bind and connect one socket. + + If open_sockets is passed, add the socket to the set of open sockets. + Any failure caught here will remove the socket from the set and close it. + + Callers can use this set to close any sockets that are not the winner + of all staggered tasks in the result there are runner up sockets aka + multiple winners. + """ my_exceptions: List[Union[OSError, RuntimeError]] = [] exceptions.append(my_exceptions) family, type_, proto, _, address = addr_info sock = None try: sock = socket.socket(family=family, type=type_, proto=proto) + if open_sockets is not None: + open_sockets.add(sock) sock.setblocking(False) if local_addr_infos is not None: for lfamily, _, _, _, laddr in local_addr_infos: @@ -165,6 +199,8 @@ except (RuntimeError, OSError) as exc: my_exceptions.append(exc) if sock is not None: + if open_sockets is not None: + open_sockets.remove(sock) try: sock.close() except OSError as e: @@ -173,6 +209,8 @@ raise except: if sock is not None: + if open_sockets is not None: + open_sockets.remove(sock) try: sock.close() except OSError as e: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiohappyeyeballs-2.4.6/tests/test_impl.py new/aiohappyeyeballs-2.4.8/tests/test_impl.py --- old/aiohappyeyeballs-2.4.6/tests/test_impl.py 1970-01-01 01:00:00.000000000 +0100 +++ new/aiohappyeyeballs-2.4.8/tests/test_impl.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,12 +1,12 @@ import asyncio import socket from types import ModuleType -from typing import Tuple +from typing import List, Optional, Sequence, Set, Tuple, Union from unittest import mock import pytest -from aiohappyeyeballs import start_connection +from aiohappyeyeballs import AddrInfoType, _staggered, impl, start_connection def mock_socket_module(): @@ -181,6 +181,75 @@ @pytest.mark.asyncio @patch_socket +async def test_multiple_winners_cleaned_up( + m_socket: ModuleType, +) -> None: + loop = asyncio.get_running_loop() + finish = loop.create_future() + + def _socket(*args, **kw): + return mock.MagicMock( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + fileno=mock.MagicMock(return_value=1), + ) + + async def _connect_sock( + loop: asyncio.AbstractEventLoop, + exceptions: List[List[Union[OSError, RuntimeError]]], + addr_info: AddrInfoType, + local_addr_infos: Optional[Sequence[AddrInfoType]] = None, + sockets: Optional[Set[socket.socket]] = None, + ) -> socket.socket: + await finish + sock = _socket() + assert sockets is not None + sockets.add(sock) + return sock + + m_socket.socket = _socket # type: ignore + addr_info = [ + ( + socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("107.6.106.82", 80), + ), + ( + socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("107.6.106.83", 80), + ), + ( + socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("107.6.106.84", 80), + ), + ( + socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("107.6.106.85", 80), + ), + ] + with mock.patch.object(impl, "_connect_sock", _connect_sock): + task = loop.create_task( + start_connection(addr_info, happy_eyeballs_delay=0.0001, interleave=0) + ) + await asyncio.sleep(0.1) + loop.call_soon(finish.set_result, None) + await task + + +@pytest.mark.asyncio +@patch_socket async def test_multiple_addr_success_second_one_happy_eyeballs( m_socket: ModuleType, ) -> None: @@ -1652,11 +1721,14 @@ @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.""" + + class MockSystemExit(BaseException): + """Mock SystemExit.""" + mock_socket = mock.MagicMock( family=socket.AF_INET, type=socket.SOCK_STREAM, @@ -1674,7 +1746,7 @@ sock: socket.socket, address: Tuple[str, int, int, int] ) -> None: create_calls.append(address) - raise SystemExit + raise MockSystemExit m_socket.socket = _socket # type: ignore ipv6_addr_info = ( @@ -1716,9 +1788,9 @@ ), ] loop = asyncio.get_running_loop() - with pytest.raises(SystemExit), mock.patch.object( + with pytest.raises(MockSystemExit), mock.patch.object( loop, "sock_connect", _sock_connect - ): + ), mock.patch.object(_staggered, "RE_RAISE_EXCEPTIONS", (MockSystemExit,)): await start_connection( addr_info, happy_eyeballs_delay=0.3, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aiohappyeyeballs-2.4.6/tests/test_staggered.py new/aiohappyeyeballs-2.4.8/tests/test_staggered.py --- old/aiohappyeyeballs-2.4.6/tests/test_staggered.py 1970-01-01 01:00:00.000000000 +0100 +++ new/aiohappyeyeballs-2.4.8/tests/test_staggered.py 1970-01-01 01:00:00.000000000 +0100 @@ -84,3 +84,4 @@ assert excs == [None, None, None, None] loop.run_until_complete(run()) + loop.close()