All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Niteesh G. S." <niteesh.gs@gmail.com>
To: John Snow <jsnow@redhat.com>
Cc: Eduardo Habkost <ehabkost@redhat.com>,
	Stefan Hajnoczi <stefanha@redhat.com>,
	qemu-devel@nongnu.org,
	Wainer dos Santos Moschetta <wainersm@redhat.com>,
	Markus Armbruster <armbru@redhat.com>,
	Willian Rampazzo <wrampazz@redhat.com>,
	Cleber Rosa <crosa@redhat.com>, Eric Blake <eblake@redhat.com>
Subject: Re: [PATCH 12/20] python/aqmp: add QMP Message format
Date: Wed, 7 Jul 2021 20:22:13 +0530	[thread overview]
Message-ID: <CAN6ztm9ZTSOnx69HuhhragQjR6DeGBuERXuSsPFAqoZNxPcHuw@mail.gmail.com> (raw)
In-Reply-To: <20210701041313.1696009-13-jsnow@redhat.com>

[-- Attachment #1: Type: text/plain, Size: 8376 bytes --]

On Thu, Jul 1, 2021 at 9:43 AM John Snow <jsnow@redhat.com> 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 <jsnow@redhat.com>
> ---
>  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?

> +
> +    @property
> +    def _object(self) -> Dict[str, object]:
> +        """
> +        A `dict` representing this QMP message.
> +
> +        Generated on-demand, if required. This property is private
> +        because it returns an object that could be used to invalidate
> +        the internal state of the `Message` object.
> +        """
> +        if self._obj is None:
> +            self._obj = self._deserialize(self._data or b'')
> +        return self._obj
> +
> +    @classmethod
> +    def _serialize(cls, value: object) -> bytes:
> +        """
> +        Serialize a JSON object as `bytes`.
> +
> +        :raise ValueError: When the object cannot be serialized.
> +        :raise TypeError: When the object cannot be serialized.
> +
> +        :return: `bytes` ready to be sent over the wire.
> +        """
> +        return json.dumps(value, separators=(',', ':')).encode('utf-8')
> +
> +    @classmethod
> +    def _deserialize(cls, data: bytes) -> Dict[str, object]:
> +        """
> +        Deserialize JSON `bytes` into a native Python `dict`.
> +
> +        :raise DeserializationError:
> +            If JSON deserialization fails for any reason.
> +        :raise UnexpectedTypeError:
> +            If the data does not represent a JSON object.
> +
> +        :return: A `dict` representing this QMP message.
> +        """
> +        try:
> +            obj = json.loads(data)
> +        except JSONDecodeError as err:
> +            emsg = "Failed to deserialize QMP message."
> +            raise DeserializationError(emsg, data) from err
> +        if not isinstance(obj, dict):
> +            raise UnexpectedTypeError(
> +                "QMP message is not a JSON object.",
> +                obj
> +            )
> +        return obj
> +
> +
> +class DeserializationError(ProtocolError):
> +    """
> +    A QMP message was not understood as JSON.
> +
> +    When this Exception is raised, ``__cause__`` will be set to the
> +    `json.JSONDecodeError` Exception, which can be interrogated for
> +    further details.
> +
> +    :param error_message: Human-readable string describing the error.
> +    :param raw: The raw `bytes` that prompted the failure.
> +    """
> +    def __init__(self, error_message: str, raw: bytes):
> +        super().__init__(error_message)
> +        #: The raw `bytes` that were not understood as JSON.
> +        self.raw: bytes = raw
> +
> +    def __str__(self) -> str:
> +        return "\n".join([
> +            super().__str__(),
> +            f"  raw bytes were: {str(self.raw)}",
> +        ])
> +
> +
> +class UnexpectedTypeError(ProtocolError):
> +    """
> +    A QMP message was JSON, but not a JSON object.
> +
> +    :param error_message: Human-readable string describing the error.
> +    :param value: The deserialized JSON value that wasn't an object.
> +    """
> +    def __init__(self, error_message: str, value: object):
> +        super().__init__(error_message)
> +        #: The JSON value that was expected to be an object.
> +        self.value: object = value
> +
> +    def __str__(self) -> str:
> +        strval = json.dumps(self.value, indent=2)
> +        return "\n".join([
> +            super().__str__(),
> +            f"  json value was: {strval}",
> +        ])
> --
> 2.31.1
>
>

[-- Attachment #2: Type: text/html, Size: 11155 bytes --]

  reply	other threads:[~2021-07-07 14:53 UTC|newest]

Thread overview: 25+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-07-01  4:12 [PATCH 00/20] python: introduce Asynchronous QMP package John Snow
2021-07-01  4:12 ` [PATCH 01/20] python/pylint: Add exception for TypeVar names ('T') John Snow
2021-07-01  4:12 ` [PATCH 02/20] python/pylint: disable too-many-function-args John Snow
2021-07-01  4:12 ` [PATCH 03/20] python/aqmp: add asynchronous QMP (AQMP) subpackage John Snow
2021-07-01  4:12 ` [PATCH 04/20] python/aqmp: add error classes John Snow
2021-07-01  4:12 ` [PATCH 05/20] python/aqmp: add asyncio compatibility wrappers John Snow
2021-07-01  4:12 ` [PATCH 06/20] python/aqmp: add generic async message-based protocol support John Snow
2021-07-01  4:13 ` [PATCH 07/20] python/aqmp: add runstate state machine to AsyncProtocol John Snow
2021-07-01  4:13 ` [PATCH 08/20] python/aqmp: add logging " John Snow
2021-07-01  4:13 ` [PATCH 09/20] python/aqmp: add AsyncProtocol.accept() method John Snow
2021-07-01  4:13 ` [PATCH 10/20] python/aqmp: add _cb_inbound and _cb_inbound logging hooks John Snow
2021-07-01  4:13 ` [PATCH 11/20] python/aqmp: add AsyncProtocol._readline() method John Snow
2021-07-01  4:13 ` [PATCH 12/20] python/aqmp: add QMP Message format John Snow
2021-07-07 14:52   ` Niteesh G. S. [this message]
2021-07-08 16:50     ` John Snow
2021-07-01  4:13 ` [PATCH 13/20] python/aqmp: add well-known QMP object models John Snow
2021-07-01  4:13 ` [PATCH 14/20] python/aqmp: add QMP event support John Snow
2021-07-01  4:13 ` [PATCH 15/20] python/aqmp: add QMP protocol support John Snow
2021-07-01  4:13 ` [PATCH 16/20] python/aqmp: Add message routing to QMP protocol John Snow
2021-07-01  4:13 ` [PATCH 17/20] python/aqmp: add execute() interfaces John Snow
2021-07-01  4:13 ` [PATCH 18/20] python/aqmp: add _raw() execution interface John Snow
2021-07-01  4:13 ` [PATCH 19/20] python/aqmp: add asyncio_run compatibility wrapper John Snow
2021-07-01  4:13 ` [PATCH 20/20] python/aqmp: add scary message John Snow
2021-07-05 13:19 ` [PATCH 00/20] python: introduce Asynchronous QMP package Stefan Hajnoczi
2021-07-08 13:24   ` 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=CAN6ztm9ZTSOnx69HuhhragQjR6DeGBuERXuSsPFAqoZNxPcHuw@mail.gmail.com \
    --to=niteesh.gs@gmail.com \
    --cc=armbru@redhat.com \
    --cc=crosa@redhat.com \
    --cc=eblake@redhat.com \
    --cc=ehabkost@redhat.com \
    --cc=jsnow@redhat.com \
    --cc=qemu-devel@nongnu.org \
    --cc=stefanha@redhat.com \
    --cc=wainersm@redhat.com \
    --cc=wrampazz@redhat.com \
    /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.