message.py (6355B)
1""" 2QMP Message Format 3 4This module provides the `Message` class, which represents a single QMP 5message sent to or from the server. 6""" 7 8import json 9from json import JSONDecodeError 10from typing import ( 11 Dict, 12 Iterator, 13 Mapping, 14 MutableMapping, 15 Optional, 16 Union, 17) 18 19from .error import ProtocolError 20 21 22class Message(MutableMapping[str, object]): 23 """ 24 Represents a single QMP protocol message. 25 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. 30 31 Once instantiated, it may be treated like any other MutableMapping:: 32 33 >>> msg = Message(b'{"hello": "world"}') 34 >>> assert msg['hello'] == 'world' 35 >>> msg['id'] = 'foobar' 36 >>> print(msg) 37 { 38 "hello": "world", 39 "id": "foobar" 40 } 41 42 It can be converted to `bytes`:: 43 44 >>> msg = Message({"hello": "world"}) 45 >>> print(bytes(msg)) 46 b'{"hello":"world","id":"foobar"}' 47 48 Or back into a garden-variety `dict`:: 49 50 >>> dict(msg) 51 {'hello': 'world'} 52 53 54 :param value: Initial value, if any. 55 :param eager: 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__()``. 59 """ 60 # pylint: disable=too-many-ancestors 61 62 def __init__(self, 63 value: Union[bytes, Mapping[str, object]] = b'{}', *, 64 eager: bool = True): 65 self._data: Optional[bytes] = None 66 self._obj: Optional[Dict[str, object]] = None 67 68 if isinstance(value, bytes): 69 self._data = value 70 if eager: 71 self._obj = self._deserialize(self._data) 72 else: 73 self._obj = dict(value) 74 if eager: 75 self._data = self._serialize(self._obj) 76 77 # Methods necessary to implement the MutableMapping interface, see: 78 # https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping 79 80 # We get pop, popitem, clear, update, setdefault, __contains__, 81 # keys, items, values, get, __eq__ and __ne__ for free. 82 83 def __getitem__(self, key: str) -> object: 84 return self._object[key] 85 86 def __setitem__(self, key: str, value: object) -> None: 87 self._object[key] = value 88 self._data = None 89 90 def __delitem__(self, key: str) -> None: 91 del self._object[key] 92 self._data = None 93 94 def __iter__(self) -> Iterator[str]: 95 return iter(self._object) 96 97 def __len__(self) -> int: 98 return len(self._object) 99 100 # Dunder methods not related to MutableMapping: 101 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})" 106 107 def __str__(self) -> str: 108 """Pretty-printed representation of this QMP message.""" 109 return json.dumps(self._object, indent=2) 110 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 {}) 115 return self._data 116 117 # Conversion Methods 118 119 @property 120 def _object(self) -> Dict[str, object]: 121 """ 122 A `dict` representing this QMP message. 123 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. 127 """ 128 if self._obj is None: 129 self._obj = self._deserialize(self._data or b'{}') 130 return self._obj 131 132 @classmethod 133 def _serialize(cls, value: object) -> bytes: 134 """ 135 Serialize a JSON object as `bytes`. 136 137 :raise ValueError: When the object cannot be serialized. 138 :raise TypeError: When the object cannot be serialized. 139 140 :return: `bytes` ready to be sent over the wire. 141 """ 142 return json.dumps(value, separators=(',', ':')).encode('utf-8') 143 144 @classmethod 145 def _deserialize(cls, data: bytes) -> Dict[str, object]: 146 """ 147 Deserialize JSON `bytes` into a native Python `dict`. 148 149 :raise DeserializationError: 150 If JSON deserialization fails for any reason. 151 :raise UnexpectedTypeError: 152 If the data does not represent a JSON object. 153 154 :return: A `dict` representing this QMP message. 155 """ 156 try: 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.", 164 obj 165 ) 166 return obj 167 168 169class DeserializationError(ProtocolError): 170 """ 171 A QMP message was not understood as JSON. 172 173 When this Exception is raised, ``__cause__`` will be set to the 174 `json.JSONDecodeError` Exception, which can be interrogated for 175 further details. 176 177 :param error_message: Human-readable string describing the error. 178 :param raw: The raw `bytes` that prompted the failure. 179 """ 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 184 185 def __str__(self) -> str: 186 return "\n".join([ 187 super().__str__(), 188 f" raw bytes were: {str(self.raw)}", 189 ]) 190 191 192class UnexpectedTypeError(ProtocolError): 193 """ 194 A QMP message was JSON, but not a JSON object. 195 196 :param error_message: Human-readable string describing the error. 197 :param value: The deserialized JSON value that wasn't an object. 198 """ 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 203 204 def __str__(self) -> str: 205 strval = json.dumps(self.value, indent=2) 206 return "\n".join([ 207 super().__str__(), 208 f" json value was: {strval}", 209 ])