feat: ensure ServiceInfo.properties always returns bytes (#1333) · python-zeroconf/python-zeroconf@d29553a
@@ -191,7 +191,7 @@ def __init__(
191191self.priority = priority
192192self.server = server if server else None
193193self.server_key = server.lower() if server else None
194-self._properties: Optional[Dict[Union[str, bytes], Optional[Union[str, bytes]]]] = None
194+self._properties: Optional[Dict[bytes, Optional[bytes]]] = None
195195if isinstance(properties, bytes):
196196self._set_text(properties)
197197else:
@@ -260,14 +260,8 @@ def addresses(self, value: List[bytes]) -> None:
260260self._ipv6_addresses.append(addr)
261261262262@property
263-def properties(self) -> Dict[Union[str, bytes], Optional[Union[str, bytes]]]:
264-"""If properties were set in the constructor this property returns the original dictionary
265- of type `Dict[Union[bytes, str], Any]`.
266-267- If properties are coming from the network, after decoding a TXT record, the keys are always
268- bytes and the values are either bytes, if there was a value, even empty, or `None`, if there
269- was none. No further decoding is attempted. The type returned is `Dict[bytes, Optional[bytes]]`.
270- """
263+def properties(self) -> Dict[bytes, Optional[bytes]]:
264+"""Return properties as bytes."""
271265if self._properties is None:
272266self._unpack_text_into_properties()
273267if TYPE_CHECKING:
@@ -356,21 +350,31 @@ def parsed_scoped_addresses(self, version: IPVersion = IPVersion.All) -> List[st
356350357351def _set_properties(self, properties: Dict[Union[str, bytes], Optional[Union[str, bytes]]]) -> None:
358352"""Sets properties and text of this info from a dictionary"""
359-self._properties = properties
360353list_: List[bytes] = []
354+properties_contain_str = False
361355result = b''
362356for key, value in properties.items():
363357if isinstance(key, str):
364358key = key.encode('utf-8')
359+properties_contain_str = True
365360366361record = key
367362if value is not None:
368363if not isinstance(value, bytes):
369364value = str(value).encode('utf-8')
365+properties_contain_str = True
370366record += b'=' + value
371367list_.append(record)
372368for item in list_:
373369result = b''.join((result, bytes((len(item),)), item))
370+if not properties_contain_str:
371+# If there are no str keys or values, we can use the properties
372+# as-is, without decoding them, otherwise calling
373+# self.properties will lazy decode them, which is expensive.
374+if TYPE_CHECKING:
375+self._properties = cast("Dict[bytes, Optional[bytes]]", properties)
376+else:
377+self._properties = properties
374378self.text = result
375379376380def _set_text(self, text: bytes) -> None:
@@ -392,7 +396,7 @@ def _unpack_text_into_properties(self) -> None:
392396return
393397394398index = 0
395-properties: Dict[Union[str, bytes], Optional[Union[str, bytes]]] = {}
399+properties: Dict[bytes, Optional[bytes]] = {}
396400while index < end:
397401length = text[index]
398402index += 1