All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Daniel P. Berrangé" <berrange@redhat.com>
To: John Snow <jsnow@redhat.com>
Cc: qemu-devel@nongnu.org, qemu-block@nongnu.org,
	Cleber Rosa <crosa@redhat.com>, Hanna Reitz <hreitz@redhat.com>,
	Kevin Wolf <kwolf@redhat.com>
Subject: Re: [PATCH v2 05/18] python: backport 'drop Python3.6 workarounds'
Date: Mon, 8 Sep 2025 17:28:49 +0100	[thread overview]
Message-ID: <aL8EQRPwkvVCqKC-@redhat.com> (raw)
In-Reply-To: <20250903051125.3020805-6-jsnow@redhat.com>

On Wed, Sep 03, 2025 at 01:11:11AM -0400, John Snow wrote:
> Now that the minimum version is 3.7, drop some of the 3.6-specific hacks
> we've been carrying. A single remaining compatibility hack concerning
> 3.6's lack of @asynccontextmanager is addressed in the following commit.
> 
> Signed-off-by: John Snow <jsnow@redhat.com>
> cherry picked from commit 3e8e34e594cfc6b707e6f67959166acde4b421b8
> ---
>  python/qemu/qmp/protocol.py |  48 +++++++---------
>  python/qemu/qmp/qmp_tui.py  |   8 +--
>  python/qemu/qmp/util.py     | 107 ++----------------------------------
>  python/tests/protocol.py    |   8 +--
>  4 files changed, 33 insertions(+), 138 deletions(-)

This change appears to have also (mistakenly ?) squashed in

commit 97f7ffa3be17a50544b52767d14b6fd478c07b9e
Author: John Snow <jsnow@redhat.com>
Date:   Tue Jun 6 13:45:44 2023 -0400

    Use @asynciocontextmanager
    
    This removes a non-idiomatic use of a "coroutine callback" in favor of
    something a bit more standardized.
    
    Signed-off-by: John Snow <jsnow@redhat.com>


If the two backport commits are split apart then

  Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>


> 
> diff --git a/python/qemu/qmp/protocol.py b/python/qemu/qmp/protocol.py
> index ec4762c567b..958aeca08ac 100644
> --- a/python/qemu/qmp/protocol.py
> +++ b/python/qemu/qmp/protocol.py
> @@ -15,6 +15,7 @@
>  
>  import asyncio
>  from asyncio import StreamReader, StreamWriter
> +from contextlib import asynccontextmanager
>  from enum import Enum
>  from functools import wraps
>  import logging
> @@ -22,6 +23,7 @@
>  from ssl import SSLContext
>  from typing import (
>      Any,
> +    AsyncGenerator,
>      Awaitable,
>      Callable,
>      Generic,
> @@ -36,13 +38,10 @@
>  from .error import QMPError
>  from .util import (
>      bottom_half,
> -    create_task,
>      exception_summary,
>      flush,
> -    is_closing,
>      pretty_traceback,
>      upper_half,
> -    wait_closed,
>  )
>  
>  
> @@ -340,9 +339,8 @@ async def start_server(self, address: SocketAddrT,
>              This exception will wrap a more concrete one. In most cases,
>              the wrapped exception will be `OSError`.
>          """
> -        await self._session_guard(
> -            self._do_start_server(address, ssl),
> -            'Failed to establish connection')
> +        async with self._session_guard('Failed to establish connection'):
> +            await self._do_start_server(address, ssl)
>          assert self.runstate == Runstate.CONNECTING
>  
>      @upper_half
> @@ -365,12 +363,10 @@ async def accept(self) -> None:
>          """
>          if self._accepted is None:
>              raise QMPError("Cannot call accept() before start_server().")
> -        await self._session_guard(
> -            self._do_accept(),
> -            'Failed to establish connection')
> -        await self._session_guard(
> -            self._establish_session(),
> -            'Failed to establish session')
> +        async with self._session_guard('Failed to establish connection'):
> +            await self._do_accept()
> +        async with self._session_guard('Failed to establish session'):
> +            await self._establish_session()
>          assert self.runstate == Runstate.RUNNING
>  
>      @upper_half
> @@ -395,12 +391,10 @@ async def connect(self, address: Union[SocketAddrT, socket.socket],
>              protocol-level failure occurs while establishing a new
>              session, the wrapped error may also be an `QMPError`.
>          """
> -        await self._session_guard(
> -            self._do_connect(address, ssl),
> -            'Failed to establish connection')
> -        await self._session_guard(
> -            self._establish_session(),
> -            'Failed to establish session')
> +        async with self._session_guard('Failed to establish connection'):
> +            await self._do_connect(address, ssl)
> +        async with self._session_guard('Failed to establish session'):
> +            await self._establish_session()
>          assert self.runstate == Runstate.RUNNING
>  
>      @upper_half
> @@ -421,7 +415,8 @@ async def disconnect(self) -> None:
>      # Section: Session machinery
>      # --------------------------
>  
> -    async def _session_guard(self, coro: Awaitable[None], emsg: str) -> None:
> +    @asynccontextmanager
> +    async def _session_guard(self, emsg: str) -> AsyncGenerator[None, None]:
>          """
>          Async guard function used to roll back to `IDLE` on any error.
>  
> @@ -438,10 +433,9 @@ async def _session_guard(self, coro: Awaitable[None], emsg: str) -> None:
>          :raise ConnectError:
>              When any other error is encountered in the guarded block.
>          """
> -        # Note: After Python 3.6 support is removed, this should be an
> -        # @asynccontextmanager instead of accepting a callback.
>          try:
> -            await coro
> +            # Caller's code runs here.
> +            yield
>          except BaseException as err:
>              self.logger.error("%s: %s", emsg, exception_summary(err))
>              self.logger.debug("%s:\n%s\n", emsg, pretty_traceback())
> @@ -682,8 +676,8 @@ async def _establish_session(self) -> None:
>          reader_coro = self._bh_loop_forever(self._bh_recv_message, 'Reader')
>          writer_coro = self._bh_loop_forever(self._bh_send_message, 'Writer')
>  
> -        self._reader_task = create_task(reader_coro)
> -        self._writer_task = create_task(writer_coro)
> +        self._reader_task = asyncio.create_task(reader_coro)
> +        self._writer_task = asyncio.create_task(writer_coro)
>  
>          self._bh_tasks = asyncio.gather(
>              self._reader_task,
> @@ -708,7 +702,7 @@ def _schedule_disconnect(self) -> None:
>          if not self._dc_task:
>              self._set_state(Runstate.DISCONNECTING)
>              self.logger.debug("Scheduling disconnect.")
> -            self._dc_task = create_task(self._bh_disconnect())
> +            self._dc_task = asyncio.create_task(self._bh_disconnect())
>  
>      @upper_half
>      async def _wait_disconnect(self) -> None:
> @@ -844,13 +838,13 @@ async def _bh_close_stream(self, error_pathway: bool = False) -> None:
>          if not self._writer:
>              return
>  
> -        if not is_closing(self._writer):
> +        if not self._writer.is_closing():
>              self.logger.debug("Closing StreamWriter.")
>              self._writer.close()
>  
>          self.logger.debug("Waiting for StreamWriter to close ...")
>          try:
> -            await wait_closed(self._writer)
> +            await self._writer.wait_closed()
>          except Exception:  # pylint: disable=broad-except
>              # It's hard to tell if the Stream is already closed or
>              # not. Even if one of the tasks has failed, it may have
> diff --git a/python/qemu/qmp/qmp_tui.py b/python/qemu/qmp/qmp_tui.py
> index 2d9ebbd20bc..562be008d5e 100644
> --- a/python/qemu/qmp/qmp_tui.py
> +++ b/python/qemu/qmp/qmp_tui.py
> @@ -40,7 +40,7 @@
>  from .message import DeserializationError, Message, UnexpectedTypeError
>  from .protocol import ConnectError, Runstate
>  from .qmp_client import ExecInterruptedError, QMPClient
> -from .util import create_task, pretty_traceback
> +from .util import pretty_traceback
>  
>  
>  # The name of the signal that is used to update the history list
> @@ -225,7 +225,7 @@ def cb_send_to_server(self, raw_msg: str) -> None:
>          """
>          try:
>              msg = Message(bytes(raw_msg, encoding='utf-8'))
> -            create_task(self._send_to_server(msg))
> +            asyncio.create_task(self._send_to_server(msg))
>          except (DeserializationError, UnexpectedTypeError) as err:
>              raw_msg = format_json(raw_msg)
>              logging.info('Invalid message: %s', err.error_message)
> @@ -246,7 +246,7 @@ def kill_app(self) -> None:
>          Initiates killing of app. A bridge between asynchronous and synchronous
>          code.
>          """
> -        create_task(self._kill_app())
> +        asyncio.create_task(self._kill_app())
>  
>      async def _kill_app(self) -> None:
>          """
> @@ -393,7 +393,7 @@ def run(self, debug: bool = False) -> None:
>                                     handle_mouse=True,
>                                     event_loop=event_loop)
>  
> -        create_task(self.manage_connection(), self.aloop)
> +        self.aloop.create_task(self.manage_connection())
>          try:
>              main_loop.run()
>          except Exception as err:
> diff --git a/python/qemu/qmp/util.py b/python/qemu/qmp/util.py
> index ca6225e9cda..0b3e781373d 100644
> --- a/python/qemu/qmp/util.py
> +++ b/python/qemu/qmp/util.py
> @@ -1,25 +1,15 @@
>  """
>  Miscellaneous Utilities
>  
> -This module provides asyncio utilities and compatibility wrappers for
> -Python 3.6 to provide some features that otherwise become available in
> -Python 3.7+.
> -
> -Various logging and debugging utilities are also provided, such as
> -`exception_summary()` and `pretty_traceback()`, used primarily for
> -adding information into the logging stream.
> +This module provides asyncio and various logging and debugging
> +utilities, such as `exception_summary()` and `pretty_traceback()`, used
> +primarily for adding information into the logging stream.
>  """
>  
>  import asyncio
>  import sys
>  import traceback
> -from typing import (
> -    Any,
> -    Coroutine,
> -    Optional,
> -    TypeVar,
> -    cast,
> -)
> +from typing import TypeVar, cast
>  
>  
>  T = TypeVar('T')
> @@ -79,95 +69,6 @@ def bottom_half(func: T) -> T:
>      return func
>  
>  
> -# -------------------------------
> -# Section: Compatibility Wrappers
> -# -------------------------------
> -
> -
> -def create_task(coro: Coroutine[Any, Any, T],
> -                loop: Optional[asyncio.AbstractEventLoop] = None
> -                ) -> 'asyncio.Future[T]':
> -    """
> -    Python 3.6-compatible `asyncio.create_task` wrapper.
> -
> -    :param coro: The coroutine to execute in a task.
> -    :param loop: Optionally, the loop to create the task in.
> -
> -    :return: An `asyncio.Future` object.
> -    """
> -    if sys.version_info >= (3, 7):
> -        if loop is not None:
> -            return loop.create_task(coro)
> -        return asyncio.create_task(coro)  # pylint: disable=no-member
> -
> -    # Python 3.6:
> -    return asyncio.ensure_future(coro, loop=loop)
> -
> -
> -def is_closing(writer: asyncio.StreamWriter) -> bool:
> -    """
> -    Python 3.6-compatible `asyncio.StreamWriter.is_closing` wrapper.
> -
> -    :param writer: The `asyncio.StreamWriter` object.
> -    :return: `True` if the writer is closing, or closed.
> -    """
> -    if sys.version_info >= (3, 7):
> -        return writer.is_closing()
> -
> -    # Python 3.6:
> -    transport = writer.transport
> -    assert isinstance(transport, asyncio.WriteTransport)
> -    return transport.is_closing()
> -
> -
> -async def wait_closed(writer: asyncio.StreamWriter) -> None:
> -    """
> -    Python 3.6-compatible `asyncio.StreamWriter.wait_closed` wrapper.
> -
> -    :param writer: The `asyncio.StreamWriter` to wait on.
> -    """
> -    if sys.version_info >= (3, 7):
> -        await writer.wait_closed()
> -        return
> -
> -    # Python 3.6
> -    transport = writer.transport
> -    assert isinstance(transport, asyncio.WriteTransport)
> -
> -    while not transport.is_closing():
> -        await asyncio.sleep(0)
> -
> -    # This is an ugly workaround, but it's the best I can come up with.
> -    sock = transport.get_extra_info('socket')
> -
> -    if sock is None:
> -        # Our transport doesn't have a socket? ...
> -        # Nothing we can reasonably do.
> -        return
> -
> -    while sock.fileno() != -1:
> -        await asyncio.sleep(0)
> -
> -
> -def asyncio_run(coro: Coroutine[Any, Any, T], *, debug: bool = False) -> T:
> -    """
> -    Python 3.6-compatible `asyncio.run` wrapper.
> -
> -    :param coro: A coroutine to execute now.
> -    :return: The return value from the coroutine.
> -    """
> -    if sys.version_info >= (3, 7):
> -        return asyncio.run(coro, debug=debug)
> -
> -    # Python 3.6
> -    loop = asyncio.get_event_loop()
> -    loop.set_debug(debug)
> -    ret = loop.run_until_complete(coro)
> -    loop.close()
> -
> -    return ret
> -
> -
>  # ----------------------------
>  # Section: Logging & Debugging
>  # ----------------------------
> diff --git a/python/tests/protocol.py b/python/tests/protocol.py
> index 56c4d441f9c..c254c77b176 100644
> --- a/python/tests/protocol.py
> +++ b/python/tests/protocol.py
> @@ -8,7 +8,6 @@
>  
>  from qemu.qmp import ConnectError, Runstate
>  from qemu.qmp.protocol import AsyncProtocol, StateError
> -from qemu.qmp.util import asyncio_run, create_task
>  
>  
>  class NullProtocol(AsyncProtocol[None]):
> @@ -124,7 +123,7 @@ async def _runner():
>              if allow_cancellation:
>                  return
>              raise
> -    return create_task(_runner())
> +    return asyncio.create_task(_runner())
>  
>  
>  @contextmanager
> @@ -271,7 +270,7 @@ async def _watcher():
>                      msg=f"Expected state '{state.name}'",
>                  )
>  
> -        self.runstate_watcher = create_task(_watcher())
> +        self.runstate_watcher = asyncio.create_task(_watcher())
>          # Kick the loop and force the task to block on the event.
>          await asyncio.sleep(0)
>  
> @@ -589,7 +588,8 @@ async def _asyncTearDown(self):
>      async def testSmoke(self):
>          with TemporaryDirectory(suffix='.qmp') as tmpdir:
>              sock = os.path.join(tmpdir, type(self.proto).__name__ + ".sock")
> -            server_task = create_task(self.server.start_server_and_accept(sock))
> +            server_task = asyncio.create_task(
> +                self.server.start_server_and_accept(sock))
>  
>              # give the server a chance to start listening [...]
>              await asyncio.sleep(0)
> -- 
> 2.50.1
> 

With regards,
Daniel
-- 
|: https://berrange.com      -o-    https://www.flickr.com/photos/dberrange :|
|: https://libvirt.org         -o-            https://fstop138.berrange.com :|
|: https://entangle-photo.org    -o-    https://www.instagram.com/dberrange :|



  reply	other threads:[~2025-09-08 16:31 UTC|newest]

Thread overview: 35+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-09-03  5:11 [PATCH v2 00/18] python: 3.14 compatibility and python-qemu-qmp synchronization John Snow
2025-09-03  5:11 ` [PATCH v2 01/18] python: backport 'Change error classes to have better repr methods' John Snow
2025-09-08 16:23   ` Daniel P. Berrangé
2025-09-08 17:16     ` John Snow
2025-09-03  5:11 ` [PATCH v2 02/18] python: backport 'EventListener: add __repr__ method' John Snow
2025-09-08 16:24   ` Daniel P. Berrangé
2025-09-03  5:11 ` [PATCH v2 03/18] python: backport 'kick event queue on legacy event_pull()' John Snow
2025-09-08 16:24   ` Daniel P. Berrangé
2025-09-03  5:11 ` [PATCH v2 04/18] python: backport 'protocol: adjust logging name when changing client name' John Snow
2025-09-08 16:25   ` Daniel P. Berrangé
2025-09-03  5:11 ` [PATCH v2 05/18] python: backport 'drop Python3.6 workarounds' John Snow
2025-09-08 16:28   ` Daniel P. Berrangé [this message]
2025-09-03  5:11 ` [PATCH v2 06/18] python: backport 'qmp-shell: add common_parser()' John Snow
2025-09-08 16:47   ` Daniel P. Berrangé
2025-09-08 17:39     ` John Snow
2025-09-08 18:16       ` Daniel P. Berrangé
2025-09-03  5:11 ` [PATCH v2 07/18] python: backport 'feat: allow setting read buffer limit' John Snow
2025-09-08 16:49   ` Daniel P. Berrangé
2025-09-03  5:11 ` [PATCH v2 08/18] python: backport 'make require() preserve async-ness' John Snow
2025-09-08 16:50   ` Daniel P. Berrangé
2025-09-03  5:11 ` [PATCH v2 09/18] python: backport 'qmp-shell-wrap: handle missing binary gracefully' John Snow
2025-09-08 16:50   ` Daniel P. Berrangé
2025-09-03  5:11 ` [PATCH v2 10/18] python: backport 'qmp-tui: Do not crash if optional dependencies are not met' John Snow
2025-09-08 16:51   ` Daniel P. Berrangé
2025-09-03  5:11 ` [PATCH v2 11/18] python: backport 'Remove deprecated get_event_loop calls' John Snow
2025-09-08 16:52   ` Daniel P. Berrangé
2025-09-03  5:11 ` [PATCH v2 12/18] python: backport 'avoid creating additional event loops per thread' John Snow
2025-09-08 16:52   ` Daniel P. Berrangé
2025-09-03  5:11 ` [PATCH v2 13/18] python: synchronize qemu.qmp documentation John Snow
2025-09-08 16:53   ` Daniel P. Berrangé
2025-09-03  5:11 ` [PATCH v2 14/18] iotests: drop compat for old version context manager John Snow
2025-09-03  5:11 ` [PATCH v2 15/18] python: ensure QEMUQtestProtocol closes its socket John Snow
2025-09-03  5:11 ` [PATCH v2 16/18] iotests/147: ensure temporary sockets are closed before exiting John Snow
2025-09-03  5:11 ` [PATCH v2 17/18] iotests/151: ensure subprocesses are cleaned up John Snow
2025-09-03  5:11 ` [PATCH v2 18/18] iotests/check: always enable all python warnings John Snow

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=aL8EQRPwkvVCqKC-@redhat.com \
    --to=berrange@redhat.com \
    --cc=crosa@redhat.com \
    --cc=hreitz@redhat.com \
    --cc=jsnow@redhat.com \
    --cc=kwolf@redhat.com \
    --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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.