qemu-devel.nongnu.org archive mirror
 help / color / mirror / Atom feed
From: John Snow <jsnow@redhat.com>
To: qemu-devel@nongnu.org
Cc: "Kevin Wolf" <kwolf@redhat.com>,
	"Michael Roth" <michael.roth@amd.com>,
	"John Snow" <jsnow@redhat.com>, "Hanna Reitz" <hreitz@redhat.com>,
	"Peter Maydell" <peter.maydell@linaro.org>,
	"Cleber Rosa" <crosa@redhat.com>,
	qemu-block@nongnu.org, "Markus Armbruster" <armbru@redhat.com>,
	"Daniel P. Berrangé" <berrange@redhat.com>
Subject: [PULL 09/19] python: backport 'make require() preserve async-ness'
Date: Tue, 16 Sep 2025 12:23:54 -0400	[thread overview]
Message-ID: <20250916162404.9195-10-jsnow@redhat.com> (raw)
In-Reply-To: <20250916162404.9195-1-jsnow@redhat.com>

This is not strictly needed functionality-wise, but doing this allows
sphinx to see which decorated methods are async. Without this, sphinx
misses the "async" classifier on generated docs, which ... for an async
library, isn't great.

It does make an already gnarly function even gnarlier, though.

So, what's going on here?

A synchronous function (like require() before this patch) can return a
coroutine that can be awaited on, for example:

  def some_func():
      return asyncio.task(asyncio.sleep(5))

  async def some_async_func():
      await some_func()

However, this function is not considered to be an "async" function in
the eyes of the abstract syntax tree. Specifically,
some_func.__code__.co_flags will not be set with CO_COROUTINE.

The interpreter uses this flag to know if it's legal to use "await" from
within the body of the function. Since this function is just wrapping
another function, it doesn't matter much for the decorator, but sphinx
uses the stdlib inspect.iscoroutinefunction() to determine when to add
the "async" prefix in generated output. This function uses the presence
of CO_COROUTINE.

So, in order to preserve the "async" flag for docs, the require()
decorator needs to differentiate based on whether it is decorating a
sync or async function and use a different wrapping mechanism
accordingly.

Phew.

Signed-off-by: John Snow <jsnow@redhat.com>
cherry picked from commit python-qemu-qmp@40aa9699d619849f528032aa456dd061a4afa957
Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
---
 python/qemu/qmp/protocol.py | 53 ++++++++++++++++++++++---------------
 1 file changed, 32 insertions(+), 21 deletions(-)

diff --git a/python/qemu/qmp/protocol.py b/python/qemu/qmp/protocol.py
index 3d5eb553aad..4d8a39f014b 100644
--- a/python/qemu/qmp/protocol.py
+++ b/python/qemu/qmp/protocol.py
@@ -18,6 +18,7 @@
 from contextlib import asynccontextmanager
 from enum import Enum
 from functools import wraps
+from inspect import iscoroutinefunction
 import logging
 import socket
 from ssl import SSLContext
@@ -130,6 +131,25 @@ def require(required_state: Runstate) -> Callable[[F], F]:
     :param required_state: The `Runstate` required to invoke this method.
     :raise StateError: When the required `Runstate` is not met.
     """
+    def _check(proto: 'AsyncProtocol[Any]') -> None:
+        name = type(proto).__name__
+        if proto.runstate == required_state:
+            return
+
+        if proto.runstate == Runstate.CONNECTING:
+            emsg = f"{name} is currently connecting."
+        elif proto.runstate == Runstate.DISCONNECTING:
+            emsg = (f"{name} is disconnecting."
+                    " Call disconnect() to return to IDLE state.")
+        elif proto.runstate == Runstate.RUNNING:
+            emsg = f"{name} is already connected and running."
+        elif proto.runstate == Runstate.IDLE:
+            emsg = f"{name} is disconnected and idle."
+        else:
+            assert False
+
+        raise StateError(emsg, proto.runstate, required_state)
+
     def _decorator(func: F) -> F:
         # _decorator is the decorator that is built by calling the
         # require() decorator factory; e.g.:
@@ -140,29 +160,20 @@ def _decorator(func: F) -> F:
         @wraps(func)
         def _wrapper(proto: 'AsyncProtocol[Any]',
                      *args: Any, **kwargs: Any) -> Any:
-            # _wrapper is the function that gets executed prior to the
-            # decorated method.
-
-            name = type(proto).__name__
-
-            if proto.runstate != required_state:
-                if proto.runstate == Runstate.CONNECTING:
-                    emsg = f"{name} is currently connecting."
-                elif proto.runstate == Runstate.DISCONNECTING:
-                    emsg = (f"{name} is disconnecting."
-                            " Call disconnect() to return to IDLE state.")
-                elif proto.runstate == Runstate.RUNNING:
-                    emsg = f"{name} is already connected and running."
-                elif proto.runstate == Runstate.IDLE:
-                    emsg = f"{name} is disconnected and idle."
-                else:
-                    assert False
-                raise StateError(emsg, proto.runstate, required_state)
-            # No StateError, so call the wrapped method.
+            _check(proto)
             return func(proto, *args, **kwargs)
 
-        # Return the decorated method;
-        # Transforming Func to Decorated[Func].
+        @wraps(func)
+        async def _async_wrapper(proto: 'AsyncProtocol[Any]',
+                                 *args: Any, **kwargs: Any) -> Any:
+            _check(proto)
+            return await func(proto, *args, **kwargs)
+
+        # Return the decorated method; F => Decorated[F]
+        # Use an async version when applicable, which
+        # preserves async signature generation in sphinx.
+        if iscoroutinefunction(func):
+            return cast(F, _async_wrapper)
         return cast(F, _wrapper)
 
     # Return the decorator instance from the decorator factory. Phew!
-- 
2.51.0



  parent reply	other threads:[~2025-09-16 16:25 UTC|newest]

Thread overview: 24+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-09-16 16:23 [PULL 00/19] Python patches John Snow
2025-09-16 16:23 ` [PULL 01/19] python: backport 'Change error classes to have better repr methods' John Snow
2025-09-16 16:23 ` [PULL 02/19] python: backport 'EventListener: add __repr__ method' John Snow
2025-09-16 16:23 ` [PULL 03/19] python: backport 'kick event queue on legacy event_pull()' John Snow
2025-09-16 16:23 ` [PULL 04/19] python: backport 'protocol: adjust logging name when changing client name' John Snow
2025-10-15  7:11   ` Thomas Huth
2025-10-22  0:37     ` John Snow
2025-09-16 16:23 ` [PULL 05/19] python: backport 'drop Python3.6 workarounds' John Snow
2025-09-16 16:23 ` [PULL 06/19] python: backport 'Use @asynciocontextmanager' John Snow
2025-09-16 16:23 ` [PULL 07/19] python: backport 'qmp-shell: add common_parser()' John Snow
2025-09-16 16:23 ` [PULL 08/19] python: backport 'feat: allow setting read buffer limit' John Snow
2025-09-16 16:23 ` John Snow [this message]
2025-09-16 16:23 ` [PULL 10/19] python: backport 'qmp-shell-wrap: handle missing binary gracefully' John Snow
2025-09-16 16:23 ` [PULL 11/19] python: backport 'qmp-tui: Do not crash if optional dependencies are not met' John Snow
2025-09-16 16:23 ` [PULL 12/19] python: backport 'Remove deprecated get_event_loop calls' John Snow
2025-09-16 16:23 ` [PULL 13/19] python: backport 'avoid creating additional event loops per thread' John Snow
2025-09-16 16:23 ` [PULL 14/19] python: synchronize qemu.qmp documentation John Snow
2025-09-16 16:24 ` [PULL 15/19] iotests: drop compat for old version context manager John Snow
2025-09-16 16:24 ` [PULL 16/19] python: ensure QEMUQtestProtocol closes its socket John Snow
2025-09-16 16:24 ` [PULL 17/19] iotests/147: ensure temporary sockets are closed before exiting John Snow
2025-09-16 16:24 ` [PULL 18/19] iotests/151: ensure subprocesses are cleaned up John Snow
2025-09-16 16:24 ` [PULL 19/19] iotests/check: always enable all python warnings John Snow
2025-09-16 18:20 ` [PULL 00/19] Python patches John Snow
2025-09-16 19:22 ` Richard Henderson

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250916162404.9195-10-jsnow@redhat.com \
    --to=jsnow@redhat.com \
    --cc=armbru@redhat.com \
    --cc=berrange@redhat.com \
    --cc=crosa@redhat.com \
    --cc=hreitz@redhat.com \
    --cc=kwolf@redhat.com \
    --cc=michael.roth@amd.com \
    --cc=peter.maydell@linaro.org \
    --cc=qemu-block@nongnu.org \
    --cc=qemu-devel@nongnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).