4 This module provides the `Message` class, which represents a single QMP
5 message sent to or from the server.
9 from json import JSONDecodeError
19 from .error import ProtocolError
22 class Message(MutableMapping[str, object]):
24 Represents a single QMP protocol message.
26 QMP uses JSON objects as its basic communicative unit; so this
27 Python object is a :py:obj:`~collections.abc.MutableMapping`. It may
28 be instantiated from either another mapping (like a `dict`), or from
29 raw `bytes` that still need to be deserialized.
31 Once instantiated, it may be treated like any other MutableMapping::
33 >>> msg = Message(b'{"hello": "world"}')
34 >>> assert msg['hello'] == 'world'
35 >>> msg['id'] = 'foobar'
42 It can be converted to `bytes`::
44 >>> msg = Message({"hello": "world"})
46 b'{"hello":"world","id":"foobar"}'
48 Or back into a garden-variety `dict`::
54 :param value: Initial value, if any.
56 When `True`, attempt to serialize or deserialize the initial value
57 immediately, so that conversion exceptions are raised during
58 the call to ``__init__()``.
60 # pylint: disable=too-many-ancestors
63 value: Union[bytes, Mapping[str, object]] = b'{}', *,
65 self._data: Optional[bytes] = None
66 self._obj: Optional[Dict[str, object]] = None
68 if isinstance(value, bytes):
71 self._obj = self._deserialize(self._data)
73 self._obj = dict(value)
75 self._data = self._serialize(self._obj)
77 # Methods necessary to implement the MutableMapping interface, see:
78 # https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping
80 # We get pop, popitem, clear, update, setdefault, __contains__,
81 # keys, items, values, get, __eq__ and __ne__ for free.
83 def __getitem__(self, key: str) -> object:
84 return self._object[key]
86 def __setitem__(self, key: str, value: object) -> None:
87 self._object[key] = value
90 def __delitem__(self, key: str) -> None:
94 def __iter__(self) -> Iterator[str]:
95 return iter(self._object)
97 def __len__(self) -> int:
98 return len(self._object)
100 # Dunder methods not related to MutableMapping:
102 def __repr__(self) -> str:
103 if self._obj is not None:
104 return f"Message({self._object!r})"
105 return f"Message({bytes(self)!r})"
107 def __str__(self) -> str:
108 """Pretty-printed representation of this QMP message."""
109 return json.dumps(self._object, indent=2)
111 def __bytes__(self) -> bytes:
112 """bytes representing this QMP message."""
113 if self._data is None:
114 self._data = self._serialize(self._obj or {})
120 def _object(self) -> Dict[str, object]:
122 A `dict` representing this QMP message.
124 Generated on-demand, if required. This property is private
125 because it returns an object that could be used to invalidate
126 the internal state of the `Message` object.
128 if self._obj is None:
129 self._obj = self._deserialize(self._data or b'{}')
133 def _serialize(cls, value: object) -> bytes:
135 Serialize a JSON object as `bytes`.
137 :raise ValueError: When the object cannot be serialized.
138 :raise TypeError: When the object cannot be serialized.
140 :return: `bytes` ready to be sent over the wire.
142 return json.dumps(value, separators=(',', ':')).encode('utf-8')
145 def _deserialize(cls, data: bytes) -> Dict[str, object]:
147 Deserialize JSON `bytes` into a native Python `dict`.
149 :raise DeserializationError:
150 If JSON deserialization fails for any reason.
151 :raise UnexpectedTypeError:
152 If the data does not represent a JSON object.
154 :return: A `dict` representing this QMP message.
157 obj = json.loads(data)
158 except JSONDecodeError as err:
159 emsg = "Failed to deserialize QMP message."
160 raise DeserializationError(emsg, data) from err
161 if not isinstance(obj, dict):
162 raise UnexpectedTypeError(
163 "QMP message is not a JSON object.",
169 class DeserializationError(ProtocolError):
171 A QMP message was not understood as JSON.
173 When this Exception is raised, ``__cause__`` will be set to the
174 `json.JSONDecodeError` Exception, which can be interrogated for
177 :param error_message: Human-readable string describing the error.
178 :param raw: The raw `bytes` that prompted the failure.
180 def __init__(self, error_message: str, raw: bytes):
181 super().__init__(error_message)
182 #: The raw `bytes` that were not understood as JSON.
183 self.raw: bytes = raw
185 def __str__(self) -> str:
188 f" raw bytes were: {str(self.raw)}",
192 class UnexpectedTypeError(ProtocolError):
194 A QMP message was JSON, but not a JSON object.
196 :param error_message: Human-readable string describing the error.
197 :param value: The deserialized JSON value that wasn't an object.
199 def __init__(self, error_message: str, value: object):
200 super().__init__(error_message)
201 #: The JSON value that was expected to be an object.
202 self.value: object = value
204 def __str__(self) -> str:
205 strval = json.dumps(self.value, indent=2)
208 f" json value was: {strval}",