feat: ensure ServiceInfo.properties always returns bytes (#1333) · python-zeroconf/python-zeroconf@d29553a

@@ -191,7 +191,7 @@ def __init__(

191191

self.priority = priority

192192

self.server = server if server else None

193193

self.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

195195

if isinstance(properties, bytes):

196196

self._set_text(properties)

197197

else:

@@ -260,14 +260,8 @@ def addresses(self, value: List[bytes]) -> None:

260260

self._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."""

271265

if self._properties is None:

272266

self._unpack_text_into_properties()

273267

if TYPE_CHECKING:

@@ -356,21 +350,31 @@ def parsed_scoped_addresses(self, version: IPVersion = IPVersion.All) -> List[st

356350357351

def _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

360353

list_: List[bytes] = []

354+

properties_contain_str = False

361355

result = b''

362356

for key, value in properties.items():

363357

if isinstance(key, str):

364358

key = key.encode('utf-8')

359+

properties_contain_str = True

365360366361

record = key

367362

if value is not None:

368363

if not isinstance(value, bytes):

369364

value = str(value).encode('utf-8')

365+

properties_contain_str = True

370366

record += b'=' + value

371367

list_.append(record)

372368

for item in list_:

373369

result = 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

374378

self.text = result

375379376380

def _set_text(self, text: bytes) -> None:

@@ -392,7 +396,7 @@ def _unpack_text_into_properties(self) -> None:

392396

return

393397394398

index = 0

395-

properties: Dict[Union[str, bytes], Optional[Union[str, bytes]]] = {}

399+

properties: Dict[bytes, Optional[bytes]] = {}

396400

while index < end:

397401

length = text[index]

398402

index += 1