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 :|
next prev parent 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 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).