From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-10.8 required=3.0 tests=BAYES_00,DKIM_INVALID, DKIM_SIGNED,HEADER_FROM_DIFFERENT_DOMAINS,HTML_MESSAGE,INCLUDES_CR_TRAILER, INCLUDES_PATCH,MAILING_LIST_MULTI,SPF_HELO_NONE,SPF_PASS,URIBL_BLOCKED autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id DB303C07E96 for ; Thu, 8 Jul 2021 17:13:42 +0000 (UTC) Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail.kernel.org (Postfix) with ESMTPS id 4C930616EB for ; Thu, 8 Jul 2021 17:13:42 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.3.2 mail.kernel.org 4C930616EB Authentication-Results: mail.kernel.org; dmarc=fail (p=none dis=none) header.from=redhat.com Authentication-Results: mail.kernel.org; spf=pass smtp.mailfrom=qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org Received: from localhost ([::1]:53602 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1m1Xan-0001Gf-EU for qemu-devel@archiver.kernel.org; Thu, 08 Jul 2021 13:13:41 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:44832) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1m1XF3-0007rK-EY for qemu-devel@nongnu.org; Thu, 08 Jul 2021 12:51:13 -0400 Received: from us-smtp-delivery-124.mimecast.com ([216.205.24.124]:50382) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1m1XEw-0003CC-VN for qemu-devel@nongnu.org; Thu, 08 Jul 2021 12:51:12 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1625763064; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: in-reply-to:in-reply-to:references:references; bh=x0cIoiaswRb89JWxnqhUDOFii8xJALGJKWZDMiziT/8=; b=IFHJhgMV22asriz+RiMrKYVVqxJi0O6qOkdORh2HnRKIzqH5qhnGyBfpufP3Y8n3G9YPl/ n0zB4EXOyfCMAe63yFw2kW97tScWHzNyy+er8CJRPr1tf2Y9gkfoky/waTMMcuHEtmtxc1 dm3aTgYy+9ndVtU3p5ZKT8RrDwC3szQ= Received: from mail-oi1-f198.google.com (mail-oi1-f198.google.com [209.85.167.198]) (Using TLS) by relay.mimecast.com with ESMTP id us-mta-453-OblyUiZfNiyNOM4nSsAX1g-1; Thu, 08 Jul 2021 12:51:02 -0400 X-MC-Unique: OblyUiZfNiyNOM4nSsAX1g-1 Received: by mail-oi1-f198.google.com with SMTP id 22-20020aca0d160000b029023df2dc5611so4803138oin.22 for ; Thu, 08 Jul 2021 09:51:02 -0700 (PDT) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:mime-version:references:in-reply-to:from:date :message-id:subject:to:cc; bh=x0cIoiaswRb89JWxnqhUDOFii8xJALGJKWZDMiziT/8=; b=q9fqsa0b+6C2Stbme3hPQbP9Wv4lZ6klnAaibfVtfmU/GMtOPyt3C2y+8ze9aE5GxO jeqijf5kH9EruXMe+t/f5XQoornAYSDcnBYQhu+8FhKNE8V2JLTI9BKHo0vTx8WMUzU4 mhVUAck4bPM2JvwNmYN5zc7HSX4oRcJXVTI5rsIReZPlk7GKz4U7VyVTk7G1MV2SkTdY 9WEdbEX4RkC+KI2KEaq7Z5pxvRxN8Qt3KN8VYeUxXqRu3Es37xZkIXICTXWpOhO3C3Gs 0e7GTKJDyhfVUQd/rCmQ5TpzMHlP/Gt9WsqEuo/NxhntU2luMmMDE3K4+Wbz1mdaA099 /awg== X-Gm-Message-State: AOAM531wO5OutOSi3o7AQAl7eLbeUD4/ld1q5tCQzwNllD1ReBtTku3d r/KGOgHgdMeAQAL5oaKM30FnyJmkzuE+kFaZNwAOEG4wpc7iTs3O7+WARlIDzzkg+Phlcbi3Dle eNIf9+1LjPeFTUK59GrYdZs0LPhBNYAg= X-Received: by 2002:a9d:344:: with SMTP id 62mr15139248otv.323.1625763061720; Thu, 08 Jul 2021 09:51:01 -0700 (PDT) X-Google-Smtp-Source: ABdhPJxlezWMMHm0RWGV6rFUjklyrZN7Ky/iIwtwA6RNeMUVHNC7NN5E0U+eUwEloRPsfabBZfX65ku4oZ1tkCkL5WA= X-Received: by 2002:a9d:344:: with SMTP id 62mr15139239otv.323.1625763061516; Thu, 08 Jul 2021 09:51:01 -0700 (PDT) MIME-Version: 1.0 References: <20210701041313.1696009-1-jsnow@redhat.com> <20210701041313.1696009-13-jsnow@redhat.com> In-Reply-To: From: John Snow Date: Thu, 8 Jul 2021 12:50:50 -0400 Message-ID: Subject: Re: [PATCH 12/20] python/aqmp: add QMP Message format To: "Niteesh G. S." Authentication-Results: relay.mimecast.com; auth=pass smtp.auth=CUSA124A263 smtp.mailfrom=jsnow@redhat.com X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com Content-Type: multipart/alternative; boundary="000000000000c55fca05c69f752d" Received-SPF: pass client-ip=216.205.24.124; envelope-from=jsnow@redhat.com; helo=us-smtp-delivery-124.mimecast.com X-Spam_score_int: -31 X-Spam_score: -3.2 X-Spam_bar: --- X-Spam_report: (-3.2 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.45, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, HTML_MESSAGE=0.001, RCVD_IN_DNSWL_LOW=-0.7, RCVD_IN_MSPIKE_H4=0.001, RCVD_IN_MSPIKE_WL=0.001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.23 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Eduardo Habkost , Stefan Hajnoczi , qemu-devel , Wainer dos Santos Moschetta , Markus Armbruster , Willian Rampazzo , Cleber Rosa , Eric Blake Errors-To: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org Sender: "Qemu-devel" --000000000000c55fca05c69f752d Content-Type: text/plain; charset="UTF-8" On Wed, Jul 7, 2021 at 10:52 AM Niteesh G. S. wrote: > > > On Thu, Jul 1, 2021 at 9:43 AM John Snow wrote: > >> The Message class is here primarily to serve as a solid type to use for >> mypy static typing for unambiguous annotation and documentation. >> >> We can also stuff JSON serialization and deserialization into this class >> itself so it can be re-used even outside this infrastructure. >> >> Signed-off-by: John Snow >> --- >> python/qemu/aqmp/__init__.py | 4 +- >> python/qemu/aqmp/message.py | 207 +++++++++++++++++++++++++++++++++++ >> 2 files changed, 210 insertions(+), 1 deletion(-) >> create mode 100644 python/qemu/aqmp/message.py >> >> diff --git a/python/qemu/aqmp/__init__.py b/python/qemu/aqmp/__init__.py >> index 5c44fabeea..c1ec68a023 100644 >> --- a/python/qemu/aqmp/__init__.py >> +++ b/python/qemu/aqmp/__init__.py >> @@ -22,12 +22,14 @@ >> # the COPYING file in the top-level directory. >> >> from .error import AQMPError, MultiException >> +from .message import Message >> from .protocol import ConnectError, Runstate >> >> >> # The order of these fields impact the Sphinx documentation order. >> __all__ = ( >> - # Classes >> + # Classes, most to least important >> + 'Message', >> 'Runstate', >> >> # Exceptions, most generic to most explicit >> diff --git a/python/qemu/aqmp/message.py b/python/qemu/aqmp/message.py >> new file mode 100644 >> index 0000000000..3a4b283032 >> --- /dev/null >> +++ b/python/qemu/aqmp/message.py >> @@ -0,0 +1,207 @@ >> +""" >> +QMP Message Format >> + >> +This module provides the `Message` class, which represents a single QMP >> +message sent to or from the server. >> +""" >> + >> +import json >> +from json import JSONDecodeError >> +from typing import ( >> + Dict, >> + Iterator, >> + Mapping, >> + MutableMapping, >> + Optional, >> + Union, >> +) >> + >> +from .error import ProtocolError >> + >> + >> +class Message(MutableMapping[str, object]): >> + """ >> + Represents a single QMP protocol message. >> + >> + QMP uses JSON objects as its basic communicative unit; so this >> + Python object is a :py:obj:`~collections.abc.MutableMapping`. It may >> + be instantiated from either another mapping (like a `dict`), or from >> + raw `bytes` that still need to be deserialized. >> + >> + Once instantiated, it may be treated like any other MutableMapping:: >> + >> + >>> msg = Message(b'{"hello": "world"}') >> + >>> assert msg['hello'] == 'world' >> + >>> msg['id'] = 'foobar' >> + >>> print(msg) >> + { >> + "hello": "world", >> + "id": "foobar" >> + } >> + >> + It can be converted to `bytes`:: >> + >> + >>> msg = Message({"hello": "world"}) >> + >>> print(bytes(msg)) >> + b'{"hello":"world","id":"foobar"}' >> + >> + Or back into a garden-variety `dict`:: >> + >> + >>> dict(msg) >> + {'hello': 'world'} >> + >> + >> + :param value: Initial value, if any. >> + :param eager: >> + When `True`, attempt to serialize or deserialize the initial >> value >> + immediately, so that conversion exceptions are raised during >> + the call to ``__init__()``. >> + """ >> + # pylint: disable=too-many-ancestors >> + >> + def __init__(self, >> + value: Union[bytes, Mapping[str, object]] = b'', *, >> + eager: bool = True): >> + self._data: Optional[bytes] = None >> + self._obj: Optional[Dict[str, object]] = None >> + >> + if isinstance(value, bytes): >> + self._data = value >> + if eager: >> + self._obj = self._deserialize(self._data) >> + else: >> + self._obj = dict(value) >> + if eager: >> + self._data = self._serialize(self._obj) >> + >> + # Methods necessary to implement the MutableMapping interface, see: >> + # >> https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping >> + >> + # We get pop, popitem, clear, update, setdefault, __contains__, >> + # keys, items, values, get, __eq__ and __ne__ for free. >> + >> + def __getitem__(self, key: str) -> object: >> + return self._object[key] >> + >> + def __setitem__(self, key: str, value: object) -> None: >> + self._object[key] = value >> + self._data = None >> + >> + def __delitem__(self, key: str) -> None: >> + del self._object[key] >> + self._data = None >> + >> + def __iter__(self) -> Iterator[str]: >> + return iter(self._object) >> + >> + def __len__(self) -> int: >> + return len(self._object) >> + >> + # Dunder methods not related to MutableMapping: >> + >> + def __repr__(self) -> str: >> + return f"Message({self._object!r})" >> + >> + def __str__(self) -> str: >> + """Pretty-printed representation of this QMP message.""" >> + return json.dumps(self._object, indent=2) >> + >> + def __bytes__(self) -> bytes: >> + """bytes representing this QMP message.""" >> + if self._data is None: >> + self._data = self._serialize(self._obj or {}) >> + return self._data >> + >> + # >> > Is this something intentional? > Err, oops, kind of. I sometimes use little comment blocks to delineate sections of methods. Above, I have a "MutableMapping" section, and then a "Dunder method" section, and this marks the end of the dunder method section, but I neglected to give it its own title. I suppose I could name it "Conversion Methods" or similar. Thanks, --js --000000000000c55fca05c69f752d Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable


=
On Wed, Jul 7, 2021 at 10:52 AM Nitee= sh G. S. <niteesh.gs@gmail.com> wrote:


The Message class is here primarily t= o serve as a solid type to use for
mypy static typing for unambiguous annotation and documentation.

We can also stuff JSON serialization and deserialization into this class itself so it can be re-used even outside this infrastructure.

Signed-off-by: John Snow <jsnow@redhat.com>
---
=C2=A0python/qemu/aqmp/__init__.py |=C2=A0 =C2=A04 +-
=C2=A0python/qemu/aqmp/message.py=C2=A0 | 207 +++++++++++++++++++++++++++++= ++++++
=C2=A02 files changed, 210 insertions(+), 1 deletion(-)
=C2=A0create mode 100644 python/qemu/aqmp/message.py

diff --git a/python/qemu/aqmp/__init__.py b/python/qemu/aqmp/__init__.py index 5c44fabeea..c1ec68a023 100644
--- a/python/qemu/aqmp/__init__.py
+++ b/python/qemu/aqmp/__init__.py
@@ -22,12 +22,14 @@
=C2=A0# the COPYING file in the top-level directory.

=C2=A0from .error import AQMPError, MultiException
+from .message import Message
=C2=A0from .protocol import ConnectError, Runstate


=C2=A0# The order of these fields impact the Sphinx documentation order. =C2=A0__all__ =3D (
-=C2=A0 =C2=A0 # Classes
+=C2=A0 =C2=A0 # Classes, most to least important
+=C2=A0 =C2=A0 'Message',
=C2=A0 =C2=A0 =C2=A0'Runstate',

=C2=A0 =C2=A0 =C2=A0# Exceptions, most generic to most explicit
diff --git a/python/qemu/aqmp/message.py b/python/qemu/aqmp/message.py
new file mode 100644
index 0000000000..3a4b283032
--- /dev/null
+++ b/python/qemu/aqmp/message.py
@@ -0,0 +1,207 @@
+"""
+QMP Message Format
+
+This module provides the `Message` class, which represents a single QMP +message sent to or from the server.
+"""
+
+import json
+from json import JSONDecodeError
+from typing import (
+=C2=A0 =C2=A0 Dict,
+=C2=A0 =C2=A0 Iterator,
+=C2=A0 =C2=A0 Mapping,
+=C2=A0 =C2=A0 MutableMapping,
+=C2=A0 =C2=A0 Optional,
+=C2=A0 =C2=A0 Union,
+)
+
+from .error import ProtocolError
+
+
+class Message(MutableMapping[str, object]):
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 Represents a single QMP protocol message.
+
+=C2=A0 =C2=A0 QMP uses JSON objects as its basic communicative unit; so th= is
+=C2=A0 =C2=A0 Python object is a :py:obj:`~collections.abc.MutableMapping`= . It may
+=C2=A0 =C2=A0 be instantiated from either another mapping (like a `dict`),= or from
+=C2=A0 =C2=A0 raw `bytes` that still need to be deserialized.
+
+=C2=A0 =C2=A0 Once instantiated, it may be treated like any other MutableM= apping::
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 >>> msg =3D Message(b'{"hell= o": "world"}')
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 >>> assert msg['hello'] =3D= =3D 'world'
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 >>> msg['id'] =3D 'foobar= '
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 >>> print(msg)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 {
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "hello": "world", +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 "id": "foobar"
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 }
+
+=C2=A0 =C2=A0 It can be converted to `bytes`::
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 >>> msg =3D Message({"hello"= ;: "world"})
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 >>> print(bytes(msg))
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 b'{"hello":"world",&qu= ot;id":"foobar"}'
+
+=C2=A0 =C2=A0 Or back into a garden-variety `dict`::
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0>>> dict(msg)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0{'hello': 'world'}
+
+
+=C2=A0 =C2=A0 :param value: Initial value, if any.
+=C2=A0 =C2=A0 :param eager:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 When `True`, attempt to serialize or deseriali= ze the initial value
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 immediately, so that conversion exceptions are= raised during
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 the call to ``__init__()``.
+=C2=A0 =C2=A0 """
+=C2=A0 =C2=A0 # pylint: disable=3Dtoo-many-ancestors
+
+=C2=A0 =C2=A0 def __init__(self,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0value: Union= [bytes, Mapping[str, object]] =3D b'', *,
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0eager: bool = =3D True):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._data: Optional[bytes] =3D None
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._obj: Optional[Dict[str, object]] =3D Non= e
+
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if isinstance(value, bytes):
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._data =3D value
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if eager:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._obj =3D self= ._deserialize(self._data)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 else:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._obj =3D dict(value)
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if eager:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._data =3D sel= f._serialize(self._obj)
+
+=C2=A0 =C2=A0 # Methods necessary to implement the MutableMapping interfac= e, see:
+=C2=A0 =C2=A0 # https://docs.python.org/3/library/collections.abc.html#collections.abc.Mu= tableMapping
+
+=C2=A0 =C2=A0 # We get pop, popitem, clear, update, setdefault, __contains= __,
+=C2=A0 =C2=A0 # keys, items, values, get, __eq__ and __ne__ for free.
+
+=C2=A0 =C2=A0 def __getitem__(self, key: str) -> object:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self._object[key]
+
+=C2=A0 =C2=A0 def __setitem__(self, key: str, value: object) -> None: +=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._object[key] =3D value
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._data =3D None
+
+=C2=A0 =C2=A0 def __delitem__(self, key: str) -> None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 del self._object[key]
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 self._data =3D None
+
+=C2=A0 =C2=A0 def __iter__(self) -> Iterator[str]:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return iter(self._object)
+
+=C2=A0 =C2=A0 def __len__(self) -> int:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return len(self._object)
+
+=C2=A0 =C2=A0 # Dunder methods not related to MutableMapping:
+
+=C2=A0 =C2=A0 def __repr__(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return f"Message({self._object!r})"<= br> +
+=C2=A0 =C2=A0 def __str__(self) -> str:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """Pretty-printed representatio= n of this QMP message."""
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return json.dumps(self._object, indent=3D2) +
+=C2=A0 =C2=A0 def __bytes__(self) -> bytes:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 """bytes representing this QMP = message."""
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 if self._data is None:
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self._data =3D self._serialize(s= elf._obj or {})
+=C2=A0 =C2=A0 =C2=A0 =C2=A0 return self._data
+
+=C2=A0 =C2=A0 #
Is this something intentional?=C2=A0

Err, oops, kind of. I sometimes use= little comment blocks to delineate sections of methods. Above, I have a &q= uot;MutableMapping" section, and then a "Dunder method" sect= ion, and this marks the end of the dunder method section, but I neglected t= o give it its own title. I suppose I could name it "Conversion Methods= " or similar.

Thanks,
--js

--000000000000c55fca05c69f752d--