Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-gevent for openSUSE:Factory checked in at 2026-08-22 21:34:36 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-gevent (Old) and /work/SRC/openSUSE:Factory/.python-gevent.new.1258 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-gevent" Sat Aug 22 21:34:36 2026 rev:63 rq:1372808 version:26.8.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-gevent/python-gevent.changes 2026-07-09 22:18:29.744321282 +0200 +++ /work/SRC/openSUSE:Factory/.python-gevent.new.1258/python-gevent.changes 2026-08-22 21:35:40.127181953 +0200 @@ -1,0 +2,55 @@ +Fri Aug 21 10:19:12 UTC 2026 - Dirk Müller <[email protected]> + +- update to 26.8.0: + * Binary wheels for 3.15 are now built with 3.15rc1. This + should be a stable ABI. + * Replace concurrent.futures.thread._global_shutdown_lock when + patching threads, importing that module if needed. + Executor.submit holds it across Thread.start(), so a worker + greenlet that forks runs the :func:`os.register_at_fork` + handlers it is registered with while another greenlet holds + it: a native lock deadlocked, a cooperative one parked the + greenlet inside os.fork() (which filelock 3.30 rejects). Like + the rest of patch_thread(existing_locks=True), this needs the + process to be single threaded when patching. See + :issue:`1865`. + * Fixed a semaphore acquired and released by a hubless native + thread failing to wake greenlets waiting on the semaphore's + owning hub. See :issue:`2013`. + * Stop the greenlets that communicate() spawned before + gevent.subprocess.Popen.__exit__ closes the child's pipes. + Leaving the with block while one of them was still parked in + a pipe, because an exception was propagating or because the + greenlet running the block was killed, raised RuntimeError: + reentrant call out of __exit__. That replaced the exception + that was really unwinding, and skipped the wait() that reaps + the child. A pipe some other greenlet is reading is now left + alone rather than raising, which is what communicate() + already did. See :issue:`2194`. + * Fix repr() of a destroyed hub raising :exc:`AttributeError`. + Hub.destroy() deleted the _resolver and _threadpool + attributes that Hub.__repr__ reads; it now sets them to None. + This also fixes gevent.util.format_run_info(), which renders + any destroyed hub that is still reachable. See :issue:`2185`. + * Fix a hang at interpreter exit, on Python 3.13 and above, + when a non-daemon thread is waiting on a + threading._register_atexit hook. The patched + threading._shutdown joined those threads before running the + hooks, the reverse of the native order. This hung any program + holding a live + :class:`concurrent.futures.ThreadPoolExecutor`, whose non- + daemon workers stop only when its _python_exit hook runs. See + :issue:`2188`. + * Make gevent.os.close not initialize a hub if one wasn't + already present. In that case, it can just directly close the + file descriptor. + * In a future version (early 2027), gevent.monkey.patch_all + will ONLY accept keyword arguments. Currently, you could be + calling it with positional arguments, although that has never + been the intent or documented way to call it. + * Binary wheels for Python 3.15 are now built with 3.15b4. They + may not be compatible with older or newer versions of Python. + Likewise, binary wheels built by previous gevent releases may + not be compatible with 3.15b4 or newer. + +------------------------------------------------------------------- Old: ---- gevent-26.5.0.tar.gz New: ---- gevent-26.8.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-gevent.spec ++++++ --- /var/tmp/diff_new_pack.wOapbi/_old 2026-08-22 21:35:41.214220714 +0200 +++ /var/tmp/diff_new_pack.wOapbi/_new 2026-08-22 21:35:41.215220750 +0200 @@ -26,7 +26,7 @@ %bcond_with colortest %{?sle15_python_module_pythons} Name: python-gevent -Version: 26.5.0 +Version: 26.8.0 Release: 0 Summary: Python network library that uses greenlet and libevent License: MIT ++++++ gevent-26.5.0.tar.gz -> gevent-26.8.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/.github/workflows/ci.yml new/gevent-26.8.0/.github/workflows/ci.yml --- old/gevent-26.5.0/.github/workflows/ci.yml 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/.github/workflows/ci.yml 2026-08-10 18:54:02.000000000 +0200 @@ -68,8 +68,7 @@ fail-fast: false matrix: # 3.10+ needs more work: dnspython for example doesn't work - # with it. That means for the bulk of our testing we need to - # stick to 3.9. + # with it. # We'd like to always pin to specific minor versions of Python, # so we can make sure the test suite we include is in synce with @@ -135,9 +134,9 @@ steps: - name: checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} cache: 'pip' @@ -145,13 +144,14 @@ - name: Install ccache (ubuntu) if: startsWith(runner.os, 'Linux') run: | - sudo apt-get install -y ccache sed gcc + sudo apt-get install -y ccache sed gcc libffi-dev echo CCACHE_DIR=$HOME/.ccache >>$GITHUB_ENV mkdir -p $HOME/.ccache - name: Install ccache (macos) if: startsWith(runner.os, 'macOS') run: | brew install ccache + brew install libffi echo CFLAGS=$CFLAGS -Wno-parentheses-equality >>$GITHUB_ENV echo CCACHE_DIR=$HOME/.ccache >>$GITHUB_ENV mkdir -p $HOME/.ccache @@ -171,7 +171,7 @@ # the cache. ### - name: Cache ~/.ccache - uses: actions/cache@v5 + uses: actions/cache@v6 # This is repeated in an explicit save always step below # because normally it won't save anything if there's a cache hit! # Which is silly, because things in the cache might have (will have) @@ -183,7 +183,7 @@ - name: Cache config.cache # Store the configure caches. Having a cache can speed up c-ares # configure from 2-3 minutes to 20 seconds. - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: deps/*/config.cache # XXX: This should probably include a hash of each configure @@ -213,8 +213,8 @@ run: | pip install -U pip pip install -U -q setuptools wheel twine - pip install -q -U 'cffi;platform_python_implementation=="CPython"' - pip install -q -U 'cython>=3.2.4' + pip install -q -U 'cffi;platform_python_implementation=="CPython"' + pip install -q -U 'cython>=3.2.4' # Use a debug version of greenlet to help catch any errors earlier. CFLAGS="$CFLAGS -Og -g -UNDEBUG" pip install -v --no-binary :all: 'greenlet>=3.2.0;platform_python_implementation=="CPython" ' @@ -266,7 +266,7 @@ ls -l dist twine check dist/*whl - name: Cache ~/.ccache - uses: actions/cache/save@v5 + uses: actions/cache/save@v6 if: always() with: path: ~/.ccache/** @@ -406,7 +406,7 @@ test_win: runs-on: ${{ matrix.os }} - name: test(${{ matrix.python-version }}, ${{ matrix.os }}) + name: test (${{ matrix.python-version }}, ${{ matrix.os }}) env: # Reuse APPVEYOR flag to share the same known_failures logic # originally written for AppVeyor Windows builds. @@ -418,17 +418,21 @@ - windows-11-arm - windows-latest python-version: + - "3.10" - "3.11" - "3.12" - "3.13" - "3.14.5" - "3.15-dev" + exclude: + - os: windows-11-arm + python-version: "3.10" steps: - name: checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} cache: 'pip' @@ -455,7 +459,7 @@ twine check dist/*whl - name: Upload gevent wheel - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: gevent-${{ matrix.os }}-${{ matrix.python-version }}.whl path: dist/*whl @@ -499,9 +503,9 @@ os: [ubuntu-latest] steps: - name: checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} cache: 'pip' @@ -515,7 +519,7 @@ mkdir -p $HOME/.ccache - name: Cache ~/.ccache - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ~/.ccache/** key: ${{ runner.os }}-ccache_embed-${{ matrix.python-version }} @@ -523,7 +527,7 @@ - name: Cache config.cache # Store the configure caches. Having a cache can speed up c-ares # configure from 2-3 minutes to 20 seconds. - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: deps/*/config.cache # XXX: This should probably include a hash of each configure @@ -608,13 +612,13 @@ name: ${{ matrix.image }} steps: - name: checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Cache ~/.ccache - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ~/.ccache/** key: ${{ runner.os }}-ccache_${{ matrix.config[2] }}-${{ matrix.config[0] }} @@ -630,13 +634,13 @@ run: scripts/releases/make-manylinux - name: Publish package to PyPI - uses: pypa/[email protected] + uses: pypa/[email protected] if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') with: user: __token__ password: ${{ secrets.TWINE_PASSWORD }} - skip_existing: true - packages_dir: wheelhouse/ + skip-existing: true + packages-dir: wheelhouse/ - name: Upload gevent wheels uses: actions/upload-artifact@v7 with: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/CHANGES.rst new/gevent-26.8.0/CHANGES.rst --- old/gevent-26.5.0/CHANGES.rst 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/CHANGES.rst 2026-08-10 18:54:02.000000000 +0200 @@ -6,6 +6,89 @@ .. towncrier release notes start +26.8.0 (2026-08-10) +=================== + + +Features +-------- + +- Binary wheels for 3.15 are now built with 3.15rc1. This should be a + stable ABI. + + + +Bugfixes +-------- + +- Replace ``concurrent.futures.thread._global_shutdown_lock`` when patching + threads, importing that module if needed. ``Executor.submit`` holds it across + ``Thread.start()``, so a worker greenlet that forks runs the + :func:`os.register_at_fork` handlers it is registered with while another + greenlet holds it: a native lock deadlocked, a cooperative one parked the + greenlet inside ``os.fork()`` (which ``filelock`` 3.30 rejects). + + Like the rest of ``patch_thread(existing_locks=True)``, this needs the process + to be single threaded when patching. + See :issue:`1865`. +- Fixed a semaphore acquired and released by a hubless native thread failing to + wake greenlets waiting on the semaphore's owning hub. + See :issue:`2013`. +- Stop the greenlets that ``communicate()`` spawned before + ``gevent.subprocess.Popen.__exit__`` closes the child's pipes. Leaving the + ``with`` block while one of them was still parked in a pipe, because an + exception was propagating or because the greenlet running the block was + killed, raised ``RuntimeError: reentrant call`` out of ``__exit__``. That + replaced the exception that was really unwinding, and skipped the ``wait()`` + that reaps the child. A pipe some other greenlet is reading is now left alone + rather than raising, which is what ``communicate()`` already did. + See :issue:`2194`. + + +26.7.0 (2026-07-22) +=================== + + +Bugfixes +-------- + +- Fix ``repr()`` of a destroyed hub raising :exc:`AttributeError`. + ``Hub.destroy()`` deleted the ``_resolver`` and ``_threadpool`` attributes + that ``Hub.__repr__`` reads; it now sets them to ``None``. This also fixes + ``gevent.util.format_run_info()``, which renders any destroyed hub that is + still reachable. + See :issue:`2185`. +- Fix a hang at interpreter exit, on Python 3.13 and above, when a non-daemon + thread is waiting on a ``threading._register_atexit`` hook. The patched + ``threading._shutdown`` joined those threads before running the hooks, the + reverse of the native order. This hung any program holding a live + :class:`concurrent.futures.ThreadPoolExecutor`, whose non-daemon workers stop + only when its ``_python_exit`` hook runs. + See :issue:`2188`. +- Make ``gevent.os.close`` not initialize a hub if one wasn't already present. + In that case, it can just directly close the file descriptor. + + + +Deprecations and Removals +------------------------- + +- In a future version (early 2027), ``gevent.monkey.patch_all`` will ONLY accept + keyword arguments. Currently, you could be calling it with positional + arguments, although that has never been the intent or documented way + to call it. + + + +Misc +---- + +- Binary wheels for Python 3.15 are now built with 3.15b4. They may + not be compatible with older or newer versions of Python. Likewise, + binary wheels built by previous gevent releases may not be + compatible with 3.15b4 or newer. + + 26.5.0 (2026-05-20) =================== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/pyproject.toml new/gevent-26.8.0/pyproject.toml --- old/gevent-26.5.0/pyproject.toml 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/pyproject.toml 2026-08-10 18:54:02.000000000 +0200 @@ -7,7 +7,7 @@ "Cython >= 3.2.4", # See version requirements in setup.py - "cffi >= 1.17.1 ; platform_python_implementation == 'CPython'", + "cffi >= 2.1.1 ; platform_python_implementation == 'CPython'", # Python 3.7 requires at least 0.4.14, which is ABI incompatible with earlier # releases. Python 3.9 and 3.10 require 0.4.16; # 0.4.17 is ABI incompatible with earlier releases, but compatible with 1.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/setup.py new/gevent-26.8.0/setup.py --- old/gevent-26.5.0/setup.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/setup.py 2026-08-10 18:54:02.000000000 +0200 @@ -224,7 +224,7 @@ # The exception is on Windows, where we want the libuv backend we distribute # to be the default, and that requires cffi; but don't try to install it # on PyPy or it messes up the build -CFFI_DEP = "cffi >= 1.17.1 ; platform_python_implementation == 'CPython'" +CFFI_DEP = "cffi >= 2.1.1 ; platform_python_implementation == 'CPython'" CFFI_REQUIRES = [ CFFI_DEP + " and sys_platform == 'win32'" ] @@ -435,7 +435,6 @@ zip_safe=False, classifiers=[ "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -450,7 +449,7 @@ "Intended Audience :: Developers", "Development Status :: 4 - Beta" ], - python_requires=">=3.9", + python_requires=">=3.10", entry_points={ 'gevent.plugins.monkey.will_patch_all': [ "signal_os_incompat = gevent.monkey:_subscribe_signal_os", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/__init__.py new/gevent-26.8.0/src/gevent/__init__.py --- old/gevent-26.5.0/src/gevent/__init__.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/__init__.py 2026-08-10 18:54:02.000000000 +0200 @@ -27,7 +27,7 @@ #: Use ``pkg_resources.parse_version(__version__)`` or #: ``packaging.version.Version(__version__)`` to get a machine-usable #: value. -__version__ = '26.5.0' +__version__ = '26.8.0' __all__ = [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/_abstract_linkable.py new/gevent-26.8.0/src/gevent/_abstract_linkable.py --- old/gevent-26.5.0/src/gevent/_abstract_linkable.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/_abstract_linkable.py 2026-08-10 18:54:02.000000000 +0200 @@ -37,14 +37,25 @@ _get_thread_ident = __import__(thread_mod_name).get_ident _allocate_thread_lock = __import__(thread_mod_name).allocate_lock +# Reserve _notifier before a foreign thread wakes the owner loop. It mirrors +# the callback attributes used by the wait and unlink paths. class _FakeNotifier(object): __slots__ = ( + 'args', 'pending', ) def __init__(self): + self.args = ([],) + self.pending = True + + def stop(self): + self.args = None self.pending = False + def __bool__(self): + return self.args is not None + def get_roots_and_hubs(): from gevent.hub import Hub # delay import return { @@ -220,11 +231,38 @@ try: hub = self._capture_hub(False) # Must create, we need it. except InvalidThreadUseError: - # The current hub doesn't match self.hub. That's OK, - # we still want to start the notifier in the thread running - # self.hub (because the links probably contains greenlet.switch - # calls valid only in that hub) - pass + # Links are normally greenlet.switch methods and must be + # notified by their owning hub. First publish a notifier that + # coalesces foreign-thread wakeups, then wake that hub. The + # callback returned by run_callback_threadsafe cannot itself + # be used as _notifier: the hub may run it before this thread + # has received and stored it. + hub = self.hub + loop = hub.loop + if loop is None: + # The owner was destroyed while this thread was using the + # linkable. Its callbacks cannot safely run anywhere else. + raise + # The check on self._notifier above and this assignment are + # not atomic: another thread could install its own notifier + # in between, and one of the two would be lost. That's safe + # per the class comment above (2a/2b) when this is compiled + # with Cython (the GIL is held for the whole method) or the + # subclass holds a native lock (only Semaphore does). In + # pure-Python mode without such a lock (Event, AsyncResult), + # this is a real, currently-unclosed race. + notifier = _FakeNotifier() + self._notifier = notifier + try: + loop.run_callback_threadsafe( + self._notify_links_from_threadsafe, + notifier + ) + except: + if self._notifier is notifier: + self._notifier = None + raise + return if hub is not None: self._notifier = hub.loop.run_callback(self._notify_links, []) else: @@ -236,6 +274,25 @@ finally: self._notifier = None + def _notify_links_from_threadsafe(self, notifier): + # notifier.args[0] starts out empty, but it is not necessarily + # still empty by the time this runs: if a greenlet calls wait() + # (see _wait() below) while we're already ready() and this + # notifier is still pending, instead of registering a normal + # rawlink it appends its resume callback directly to + # notifier.args[0] (see __wait_to_be_notified()), so that it gets + # woken up along with everyone else once this callback finally + # runs. + # + # The callback may be stale after cancellation, fork, or a newer + # notification. Only the notifier that requested this wakeup may run. + if self._notifier is notifier: + if notifier: + notifier.pending = False + self._notify_links(notifier.args[0]) + else: + self._notifier = None + def _notify_link_list(self, links): # The core of the _notify_links method to notify # links in order. Lets the ``links`` list be mutated, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/_gevent_c_abstract_linkable.pxd new/gevent-26.8.0/src/gevent/_gevent_c_abstract_linkable.pxd --- old/gevent-26.5.0/src/gevent/_gevent_c_abstract_linkable.pxd 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/_gevent_c_abstract_linkable.pxd 2026-08-10 18:54:02.000000000 +0200 @@ -36,7 +36,8 @@ cdef dict get_roots_and_hubs() cdef class _FakeNotifier(object): - cdef bint pending + cdef public object args + cdef public bint pending cdef class AbstractLinkable(object): # We declare the __weakref__ here in the base (even though @@ -57,6 +58,7 @@ cpdef unlink(self, callback) cdef _check_and_notify(self) + cdef _notify_links_from_threadsafe(self, notifier) cdef SwitchOutGreenletWithLoop _capture_hub(self, bint create) cdef __wait_to_be_notified(self, bint rawlink) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/hub.py new/gevent-26.8.0/src/gevent/hub.py --- old/gevent-26.5.0/src/gevent/hub.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/hub.py 2026-08-10 18:54:02.000000000 +0200 @@ -789,10 +789,10 @@ self.periodic_monitoring_thread = None if self._resolver is not None: self._resolver.close() - del self._resolver + self._resolver = None if self._threadpool is not None: self._threadpool.kill() - del self._threadpool + self._threadpool = None # Let the frame be cleaned up by causing the run() function to # exit. This is the only way to guarantee that the hub itself diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/monkey/__init__.py new/gevent-26.8.0/src/gevent/monkey/__init__.py --- old/gevent-26.5.0/src/gevent/monkey/__init__.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/monkey/__init__.py 2026-08-10 18:54:02.000000000 +0200 @@ -352,6 +352,9 @@ best-effort attempt and, on certain implementations, may not detect all locks. It is important to monkey-patch extremely early in the startup process. Setting this to False is not recommended, especially on Python 2. + This also replaces the lock :mod:`concurrent.futures.thread` registers + with :func:`os.register_at_fork`, which deadlocks or switches greenlets + mid-fork. .. caution:: Monkey-patching :mod:`thread` and using @@ -367,6 +370,9 @@ Add *logging* and *existing_locks* params. .. versionchanged:: 1.3a2 ``Event`` defaults to True. + .. versionchanged:: 26.8.0 + *existing_locks* imports :mod:`concurrent.futures.thread` and replaces + its ``_global_shutdown_lock``. """ if sys.version_info[:2] < (3, 13): from ._patch_thread_lt313 import Patcher @@ -667,9 +673,17 @@ Add the ``contextvars`` argument. .. versionchanged:: 1.5 Better handling of patching more than once. + .. versionchanged:: 26.7.0 + A future version (released in early 2027) will make all arguments keyword-only. Users calling + this API positionally will need to migrate to keywords. """ # pylint:disable=too-many-locals,too-many-branches + # See test__threading.py for implications of the order in which + # we patch modules. We could rearrange the arguments, but they're not + # keyword only; somebody could be calling ``patch_all(True, True, False, True)`` + # so that would be a breaking change, requiring notification + # Check to see if they're changing the patched list _warnings, first_time, modules_to_patch = _check_repatching(**locals()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/monkey/_patch_thread_common.py new/gevent-26.8.0/src/gevent/monkey/_patch_thread_common.py --- old/gevent-26.5.0/src/gevent/monkey/_patch_thread_common.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/monkey/_patch_thread_common.py 2026-08-10 18:54:02.000000000 +0200 @@ -15,9 +15,29 @@ from ._util import _queue_warning +def _patch_global_shutdown_lock(threading): + # ``concurrent.futures.thread`` registers its ``_global_shutdown_lock`` with + # ``os.register_at_fork(before=acquire, after_in_parent=release)``, and + # ``Executor.submit`` holds it across ``Thread.start()``. So a worker + # greenlet that forks runs those handlers while another greenlet holds the + # lock: a native one blocks the only OS thread forever, one of ours parks + # the greenlet inside ``os.fork()``. See :issue:`1865`. + # + # We can't unregister, so we hand the handlers a lock nobody else holds; + # uncontended, it neither blocks nor switches. Its users re-read the global. + # We import the module since applications import it after patching. + import concurrent.futures.thread as cf_thread + if not hasattr(cf_thread, '_global_shutdown_lock'): # pragma: no cover + # Private; it may go away. + return + + cf_thread._global_shutdown_lock = threading._allocate_lock() + + def _patch_existing_locks(threading): if len(list(threading.enumerate())) != 1: return + _patch_global_shutdown_lock(threading) # This is used to protect internal data structures for enumerate. # It's acquired when threads are started and when they're stopped. # Stopping a thread checks a Condition, which on Python 2 wants to test diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/monkey/_patch_thread_gte313.py new/gevent-26.8.0/src/gevent/monkey/_patch_thread_gte313.py --- old/gevent-26.5.0/src/gevent/monkey/_patch_thread_gte313.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/monkey/_patch_thread_gte313.py 2026-08-10 18:54:02.000000000 +0200 @@ -82,6 +82,15 @@ # acquire should be our own (hopefully), and the call to # _stop that orig_shutdown makes will discard it. + # Native _shutdown runs these before joining, and a non-daemon + # thread may be waiting on one: concurrent.futures' _python_exit + # is the only thing that stops a ThreadPoolExecutor's workers. + # orig_shutdown runs the loop again, hence the clear. + threading_mod._SHUTTING_DOWN = True + for atexit_call in reversed(threading_mod._threading_atexits): + atexit_call() + del threading_mod._threading_atexits[:] + # XXX: What if more get spawned? for t in list(threading_mod.enumerate()): if t.daemon or t is main_thread: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/os.py new/gevent-26.8.0/src/gevent/os.py --- old/gevent-26.5.0/src/gevent/os.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/os.py 2026-08-10 18:54:02.000000000 +0200 @@ -47,6 +47,7 @@ from stat import S_ISREG from gevent.hub import _get_hub_noargs as get_hub +from gevent.hub import _get_hub from gevent.hub import reinit from gevent.event import Event from gevent._config import config @@ -133,7 +134,12 @@ if S_ISREG(stats.st_mode) and _NO_DEFER_REG_FILE: return _close(fd) - hub = get_hub() + # Don't init the hub if not already in use. If not + # in use, we can just close regularly, there's no + # chance the FD was being used for IO. + hub = _get_hub() + if hub is None: + return _close(fd) loop = hub.loop if fd in _closing_fd_to_event: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/resolver/_addresses.py new/gevent-26.8.0/src/gevent/resolver/_addresses.py --- old/gevent-26.5.0/src/gevent/resolver/_addresses.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/resolver/_addresses.py 2026-08-10 18:54:02.000000000 +0200 @@ -60,8 +60,14 @@ try: ints = [int(part) for part in parts] return struct.pack('BBBB', *ints) - except: - raise AddressSyntaxError(text) + except Exception as ex: + # Used to catch `BaseException`. + # We expect struct.error or ValueError, + # but historically we've caught everything, so + # we're mostly leaving that for BWC (e.g, what if it raises + # MemoryError), just no longer + # catching BaseException. + raise AddressSyntaxError(text) from ex def _ipv6_inet_aton(text, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/subprocess.py new/gevent-26.8.0/src/gevent/subprocess.py --- old/gevent-26.5.0/src/gevent/subprocess.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/subprocess.py 2026-08-10 18:54:02.000000000 +0200 @@ -75,7 +75,7 @@ from gevent._util import _NONE from gevent._util import copy_globals -from gevent.greenlet import Greenlet, joinall +from gevent.greenlet import Greenlet, joinall, killall spawn = Greenlet.spawn import subprocess as __subprocess__ # We need our sockets (at least those involved in launching children) @@ -1001,10 +1001,35 @@ return self def __exit__(self, t, v, tb): - if self.stdout: - self.stdout.close() - if self.stderr: - self.stderr.close() + # gevent: If we're leaving the block early, because an exception is + # propagating or because the greenlet running it was killed, the + # greenlets ``communicate`` spawned can still be parked in the pipes. + # Closing one out from under a parked greenlet raises ``RuntimeError: + # reentrant call``: it shares our thread, so the buffered object's + # lock is held by the thread doing the close. Stop them first, and + # each one closes its own pipe as it unwinds. ``communicate`` avoids + # the same hazard by waiting for them to finish. + if self._communicating_greenlets is not None: + still_running = [ + glet for glet in self._communicating_greenlets + if not glet.dead + ] + if still_running: + killall(still_running) + + # Some other greenlet, reading ``popen.stdout`` directly, can be + # parked in there too, and we have no handle on that one. Its + # ``RuntimeError`` must not replace the exception already unwinding + # this block, nor skip the ``self.wait()`` below that reaps the child. + # The pipe closes once that reader finishes and drops its last + # reference to it. + for pipe in (self.stdout, self.stderr): + if pipe: + try: + pipe.close() + except RuntimeError: + pass + try: # Flushing a BufferedWriter may raise an error if self.stdin: self.stdin.close() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/testing/patched_tests_setup.py new/gevent-26.8.0/src/gevent/testing/patched_tests_setup.py --- old/gevent-26.5.0/src/gevent/testing/patched_tests_setup.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/testing/patched_tests_setup.py 2026-08-10 18:54:02.000000000 +0200 @@ -1158,12 +1158,18 @@ 'test_httplib.BasicTest.test_dir_with_added_behavior_on_status', 'test_httplib.TunnelTests.test_tunnel_connect_single_send_connection_setup', 'test_ssl.TestSSLDebug.test_msg_callback_deadlock_bpo43577', - # This one fails with the updated certs - 'test_ssl.ContextTests.test_load_verify_cadata', # This one times out on 3.7.1 on Appveyor 'test_ftplib.TestTLS_FTPClassMixin.test_retrbinary_rest', ] +disabled_tests += [ + # These are flaky and depend on the exact version of the tests, + # the exact version of CA data, the exact version of the stdlib + # ssl module, etc. They sometimes break only on musl. + 'test_ssl.ContextTests.test_load_verify_cadata', + 'test_ssl.SimpleBackgroundTests.test_connect_cadata', +] + if RESOLVER_DNSPYTHON: disabled_tests += [ # This does two things DNS python doesn't. First, it sends it diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/tests/known_failures.py new/gevent-26.8.0/src/gevent/tests/known_failures.py --- old/gevent-26.5.0/src/gevent/tests/known_failures.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/tests/known_failures.py 2026-08-10 18:54:02.000000000 +0200 @@ -99,6 +99,7 @@ and sys.version_info[2] < 5 ) + class _Definition(object): __slots__ = ( '__name__', @@ -312,9 +313,9 @@ ).ignored( """ This fails to run a single test. It looks like just importing the module - can hang. All I see is the output from patch_all() + can hang. All I see is the output from patch_all(). """, - when=APPVEYOR & PYPY3 + when=(APPVEYOR & PYPY3) ) test__monkey_sigchld_2 = Ignored( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/tests/test__core.py new/gevent-26.8.0/src/gevent/tests/test__core.py --- old/gevent-26.5.0/src/gevent/tests/test__core.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/tests/test__core.py 2026-08-10 18:54:02.000000000 +0200 @@ -115,11 +115,13 @@ class TestLibevCext(LibevTestMixin, unittest.TestCase): kind = available_loops['libev-cext'] [email protected](not_available('libev-cffi'), "Needs libev-cffi") [email protected](not_available('libev-cffi') or sys.version_info == (3, 15, 0, 'beta', 4), + "Needs libev-cffi") class TestLibevCffi(LibevTestMixin, unittest.TestCase): kind = available_loops['libev-cffi'] [email protected](not_available('libuv-cffi'), "Needs libuv-cffi") [email protected](not_available('libuv-cffi') or sys.version_info == (3, 15, 0, 'beta', 4), + "Needs libuv-cffi") class TestLibuvCffi(WatcherTestMixin, unittest.TestCase): kind = available_loops['libuv-cffi'] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/tests/test__destroy.py new/gevent-26.8.0/src/gevent/tests/test__destroy.py --- old/gevent-26.5.0/src/gevent/tests/test__destroy.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/tests/test__destroy.py 2026-08-10 18:54:02.000000000 +0200 @@ -3,6 +3,8 @@ import gevent import unittest +from gevent.hub import Hub + class TestDestroyHub(unittest.TestCase): def test_destroy_hub(self): @@ -48,5 +50,25 @@ hub.destroy() + +class TestDestroyedHubRepr(unittest.TestCase): + + def test_repr_after_destroy(self): + # Uses Hub directly: the test suite installs QuietHub, whose + # class-level _resolver/_threadpool defaults hide this bug. + hub = Hub(default=False) + # destroy() only clears the attributes that were created. + self.assertIsNotNone(hub.threadpool) + self.assertIsNotNone(hub.resolver) + + hub.destroy(destroy_loop=True) + + # destroy() sets these to None instead of deleting them, so + # __repr__, which reads both, still works. + self.assertIsNone(hub._threadpool) + self.assertIsNone(hub._resolver) + self.assertIn('destroyed', repr(hub)) + + if __name__ == '__main__': unittest.main() # pragma: testrunner-no-combine diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/tests/test__event.py new/gevent-26.8.0/src/gevent/tests/test__event.py --- old/gevent-26.5.0/src/gevent/tests/test__event.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/tests/test__event.py 2026-08-10 18:54:02.000000000 +0200 @@ -279,6 +279,81 @@ # Do it again to make sure it works multiple times. self.test_cross_thread_use_set_in_bg() + @greentest.ignores_leakcheck + def test_cross_thread_callback_can_run_before_scheduling_returns(self): + hub = gevent.get_hub() + real_loop = hub.loop + + from threading import Thread as NativeThread + from threading import Event as NativeEvent + + result = self._makeOne() + result.wait(0) # Capture the owner hub. + result.rawlink(lambda _: None) + + scheduling = NativeEvent() + allow_enqueue = NativeEvent() + enqueued = NativeEvent() + link_started = NativeEvent() + producer_done = NativeEvent() + + class LoopProxy(object): + # We can't always wrap methods directly on ``real_loop``: + # depending on the backend, it may be a Cython-compiled + # extension type with no instance ``__dict__`` (so we + # can't add attributes to it) and its class is immutable + # (so we can't patch the class either). Instead, we swap + # out ``hub.loop`` itself for a plain Python object that + # forwards everything to the real loop except the one + # method we need to intercept. + def __getattr__(self, name): + return getattr(real_loop, name) + + def run_callback_threadsafe(self, callback, *args): + scheduling.set() + if not allow_enqueue.wait(DELAY * 15): + raise AssertionError("The owner did not allow the callback") + scheduled = real_loop.run_callback_threadsafe(callback, *args) + enqueued.set() + if not link_started.wait(DELAY * 15): + raise AssertionError("The owner did not start notification") + return scheduled + + def set_result(): + try: + self._setOne(result) + except BaseException as ex: # pylint:disable=broad-exception-caught + thread.error = ex + finally: + producer_done.set() + + def link(_): + link_started.set() + self.assertTrue(producer_done.wait(DELAY * 15)) + + thread = NativeThread(target=set_result) + thread.error = None + hub.loop = LoopProxy() + try: + thread.start() + self.assertTrue(scheduling.wait(DELAY * 15)) + result.rawlink(link) + allow_enqueue.set() + self.assertTrue(enqueued.wait(DELAY * 15)) + with gevent.Timeout(DELAY * 15): + while not producer_done.is_set(): + gevent.sleep(0) + thread.join(DELAY * 15) + self.assertFalse(thread.is_alive()) + if thread.error is not None: + raise thread.error + finally: + allow_enqueue.set() + link_started.set() + producer_done.set() + thread.join(DELAY * 15) + hub.loop = real_loop + class TestEventCrossThread(TestAsyncResultCrossThread): def _makeOne(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/tests/test__issue2013.py new/gevent-26.8.0/src/gevent/tests/test__issue2013.py --- old/gevent-26.5.0/src/gevent/tests/test__issue2013.py 1970-01-01 01:00:00.000000000 +0100 +++ new/gevent-26.8.0/src/gevent/tests/test__issue2013.py 2026-08-10 18:54:02.000000000 +0200 @@ -0,0 +1,106 @@ +import _thread +import time + + +# This module monkey-patches at import time and depends on a fresh native-thread +# state. It must run in its own subprocess. +# pragma: testrunner-no-combine + + +# Capture the native APIs before monkey-patching. The producer must not be a +# gevent-managed thread and must never create a hub of its own. +native_start_new_thread = _thread.start_new_thread +native_get_ident = _thread.get_ident +native_sleep = time.sleep + +from gevent import monkey +monkey.patch_all() + +import gevent +from gevent._hub_local import get_hub_if_exists +from gevent.lock import BoundedSemaphore +import gevent.testing as greentest + + +class TestHublessThreadSemaphore(greentest.TestCase): + + __timeout__ = 10 + + def _wait_for(self, condition): + with gevent.Timeout(2): + while not condition(): + gevent.sleep(0.001) + + def _run_waiters(self, semaphore, count, release): + owner_ident = native_get_ident() + notified_on = [] + resumed_on = [] + + semaphore.rawlink( + lambda _: notified_on.append(native_get_ident()) + ) + + def waiter(): + semaphore.acquire() + resumed_on.append(native_get_ident()) + semaphore.release() + + waiters = [gevent.spawn(waiter) for _ in range(count)] + self._wait_for(lambda: semaphore.linkcount() == count + 1) + release() + gevent.joinall(waiters, timeout=2) + + self.assertTrue(all(waiter.successful() for waiter in waiters)) + self.assertEqual(notified_on, [owner_ident]) + self.assertEqual(resumed_on, [owner_ident] * count) + + def _run_native_round(self, owner_ident): + semaphore = BoundedSemaphore(1) + acquired = [] + release_requested = [] + released = [] + producer_ident = [] + producer_hub = [] + + def producer(): + producer_ident.append(native_get_ident()) + producer_hub.append(get_hub_if_exists()) + semaphore.acquire() + acquired.append(True) + while not release_requested: + native_sleep(0.001) + semaphore.release() + released.append(True) + + native_start_new_thread(producer, ()) + self._wait_for(lambda: acquired) + self._run_waiters( + semaphore, + 4, + lambda: release_requested.append(True), + ) + self._wait_for(lambda: released) + + self.assertNotEqual(producer_ident, [owner_ident]) + self.assertEqual(producer_hub, [None]) + + def test_hubless_native_thread_release_wakes_waiters(self): + owner_ident = native_get_ident() + for _ in range(20): + self._run_native_round(owner_ident) + + def test_owner_thread_release_wakes_waiters(self): + semaphore = BoundedSemaphore(1) + semaphore.acquire() + self._run_waiters(semaphore, 2, semaphore.release) + + def test_gevent_producer_release_wakes_waiters(self): + semaphore = BoundedSemaphore(1) + semaphore.acquire() + producer = gevent.Greenlet(semaphore.release) + self._run_waiters(semaphore, 2, producer.start) + producer.get(timeout=2) + + +if __name__ == '__main__': + greentest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/tests/test__monkey_fork_atomic.py new/gevent-26.8.0/src/gevent/tests/test__monkey_fork_atomic.py --- old/gevent-26.5.0/src/gevent/tests/test__monkey_fork_atomic.py 1970-01-01 01:00:00.000000000 +0100 +++ new/gevent-26.8.0/src/gevent/tests/test__monkey_fork_atomic.py 2026-08-10 18:54:02.000000000 +0200 @@ -0,0 +1,48 @@ +""" +Nothing may switch greenlets inside ``os.fork()``: :func:`os.register_at_fork` +handlers assume that window is atomic, and ``filelock`` 3.30 enforces it from +an audit hook. Importing :mod:`concurrent.futures.thread` after patching used +to break that. See :issue:`1865`. +""" +from gevent import monkey; monkey.patch_all() # pragma: testrunner-no-monkey-combine + +import os +import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor + +from gevent import testing as greentest + +# Stands in for filelock. Registered after concurrent.futures, so this runs +# first: ``before`` handlers run in reverse. +_forking = [] + +if hasattr(os, 'register_at_fork'): + os.register_at_fork( + before=lambda: _forking.append(1), + after_in_parent=_forking.pop, + after_in_child=_forking.clear) + + def _audit(event, _args): + if event == 'os.fork' and _forking: + raise RuntimeError("fork began inside another fork") + + sys.addaudithook(_audit) + + +class Test(greentest.TestCase): + + @greentest.skipOnWindows("Uses fork") + def test_forks_do_not_overlap(self): + with ThreadPoolExecutor(max_workers=4) as pool: + statuses = list(pool.map(self._spawn, range(4))) + self.assertEqual(statuses, [[0] * 4] * 4) + + @staticmethod + def _spawn(_i): + return [subprocess.Popen([sys.executable, '-c', 'pass']).wait() + for _ in range(4)] + + +if __name__ == '__main__': + greentest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/tests/test__monkey_futures_thread_fork.py new/gevent-26.8.0/src/gevent/tests/test__monkey_futures_thread_fork.py --- old/gevent-26.5.0/src/gevent/tests/test__monkey_futures_thread_fork.py 1970-01-01 01:00:00.000000000 +0100 +++ new/gevent-26.8.0/src/gevent/tests/test__monkey_futures_thread_fork.py 2026-08-10 18:54:02.000000000 +0200 @@ -0,0 +1,41 @@ +""" +:mod:`concurrent.futures.thread` imported before patching leaves a native lock +registered with :func:`os.register_at_fork`; a greenlet that forks while +another holds it hangs the process. See :issue:`1865`. + +The runner catches that hang at its timeout. +""" +import concurrent.futures.thread # MUST come before patch_all(); that's the bug + +from gevent import monkey; monkey.patch_all() # pragma: testrunner-no-monkey-combine + +import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor + +from gevent import testing as greentest + + +class Test(greentest.TestCase): + + def test_global_shutdown_lock_is_patched(self): + from gevent.thread import LockType + self.assertIsInstance( + concurrent.futures.thread._global_shutdown_lock, + LockType) + + @greentest.skipOnWindows("Uses fork") + def test_concurrent_fork_while_lock_held(self): + # Two workers are needed; a single spawn on the main greenlet never + # overlaps with ``submit`` holding the lock. + with ThreadPoolExecutor(max_workers=2) as pool: + statuses = list(pool.map(self._spawn, range(2))) + self.assertEqual(statuses, [0, 0]) + + @staticmethod + def _spawn(_i): + return subprocess.Popen([sys.executable, '-c', 'pass']).wait() + + +if __name__ == '__main__': + greentest.main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/tests/test__subprocess.py new/gevent-26.8.0/src/gevent/tests/test__subprocess.py --- old/gevent-26.5.0/src/gevent/tests/test__subprocess.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/tests/test__subprocess.py 2026-08-10 18:54:02.000000000 +0200 @@ -311,6 +311,59 @@ # https://github.com/gevent/gevent/pull/939 self.__test_no_output({}, bytes) + def __popen_never_exits(self): + # A child that neither writes nor exits, so a greenlet reading it + # stays parked in the pipe until we do something about it. + return subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(3600)'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + @greentest.skipOnWindows("Windows uses FileObjectThread, with different close semantics") + def test_exit_stops_communicating_greenlets_before_closing(self): + # Leaving the block while the greenlets communicate() spawned are + # still parked in the pipes must not raise ``RuntimeError: reentrant + # call``. They share our thread, so they hold the buffered object's + # lock while we close it. + with self.__popen_never_exits() as popen: + communicator = gevent.spawn(popen.communicate) + gevent.sleep(0.1) # let the readers park inside the pipes + readers = list(popen._communicating_greenlets) + self.assertEqual(len(readers), 2) + for reader in readers: + self.assertFalse(reader.dead) + + communicator.kill() # abandon the read, as a cancelled task would + for reader in readers: + self.assertFalse(reader.dead) + popen.kill() # so that __exit__'s wait() has a child to reap + + # __exit__ ran, without raising. + for reader in readers: + self.assertTrue(reader.dead) + self.assertTrue(popen.stdout.closed) + self.assertTrue(popen.stderr.closed) + self.assertIsNotNone(popen.poll()) + + @greentest.skipOnWindows("Windows uses FileObjectThread, with different close semantics") + def test_exit_ignores_reentrant_call_from_foreign_greenlet(self): + # The same hazard, but the greenlet parked in the pipe is not one of + # ours, so __exit__ has no handle on it. The RuntimeError still must + # not escape, and the child still has to be reaped. + popen = self.__popen_never_exits() + reader = gevent.spawn(popen.stdout.read) + gevent.sleep(0.1) + self.assertFalse(reader.dead) + self.assertIsNone(popen._communicating_greenlets) + + popen.kill() + try: + popen.__exit__(None, None, None) + finally: + reader.kill() + + self.assertTrue(popen.stdout.closed) + self.assertIsNotNone(popen.poll()) + @greentest.skipOnWindows("Testing POSIX fd closing") class TestFDs(unittest.TestCase): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/tests/test__threading.py new/gevent-26.8.0/src/gevent/tests/test__threading.py --- old/gevent-26.5.0/src/gevent/tests/test__threading.py 2026-05-20 22:11:13.000000000 +0200 +++ new/gevent-26.8.0/src/gevent/tests/test__threading.py 2026-08-10 18:54:02.000000000 +0200 @@ -3,12 +3,22 @@ """ from gevent import monkey; monkey.patch_all() # pragma: testrunner-no-monkey-combine import gevent.hub -import sys -# check that the locks initialized by 'threading' did not init the hub -# XXX: Python 3.15b1 on ubuntu-latest on github actions fails this assert. -# I can't reproduce it in the manylinux_2_28 image locally. -assert gevent.hub._get_hub() is None or sys.version_info == ( - 3, 15, 0, 'beta', 1), 'monkey.patch_all() should not init hub' + +# check that the locks initialized by 'threading' did not init the hub. + +# Beginning in Python 3.15, on Linux kernels new enough, importing +# ``subprocess`` attempts to check whether pidfd is available. This leads +# it to call ``os.close(pidfd)``. If we've already monkey-patched ``os`` +# at that point, the hub gets created. The order of module patching +# is somewhat undefined: it relies on ``locals()`` and dictionary iteration +# order, so effectively it's the order of arguments to ``patch_all``, which +# we cannot currently change. A release early 2027 can do that, so +# we might be able to fix this problem then by moving os after subprocess, +# though I'm hesitant to change patch order because we know it works, even if +# accidentally. In the meantime, we changed ``gevent.os.close`` to not init +# the hub. +assert (gevent.hub._get_hub() is None +), 'monkey.patch_all() should not init hub' import gevent import gevent.testing as greentest diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/gevent-26.5.0/src/gevent/tests/test__threading_shutdown_atexit.py new/gevent-26.8.0/src/gevent/tests/test__threading_shutdown_atexit.py --- old/gevent-26.5.0/src/gevent/tests/test__threading_shutdown_atexit.py 1970-01-01 01:00:00.000000000 +0100 +++ new/gevent-26.8.0/src/gevent/tests/test__threading_shutdown_atexit.py 2026-08-10 18:54:02.000000000 +0200 @@ -0,0 +1,38 @@ +""" +The patched ``threading._shutdown`` must run the ``_register_atexit`` hooks +before it joins non-daemon threads, the way the native one does: a thread may +be waiting on a hook. + +Nothing here asserts that; a regression hangs the interpreter at exit, which +the test runner catches at its timeout. +""" +from gevent import monkey; monkey.patch_all() # pragma: testrunner-no-monkey-combine + +import threading +from concurrent.futures import ThreadPoolExecutor + +from gevent import testing as greentest + +executor = None + + +class Test(greentest.TestCase): + + @greentest.ignores_leakcheck # the thread and the hook only go away at exit + def test_thread_waiting_on_hook(self): + done = threading.Event() + threading._register_atexit(done.set) + threading.Thread(target=done.wait).start() + + def test_thread_pool_executor(self): + # How the bug reached real code: concurrent.futures registers + # _python_exit (bpo-39812), the only thing that stops a pool's + # non-daemon workers. The executor has to stay alive; once collected, + # its weakref callback stops the workers and the hang never happens. + global executor + executor = ThreadPoolExecutor(max_workers=2) + self.assertEqual(list(executor.map(int, ('1', '2'))), [1, 2]) + + +if __name__ == '__main__': + greentest.main()
