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

Reply via email to