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