fix: performance regression with ServiceInfo IPv6Addresses (#1330) · python-zeroconf/python-zeroconf@e2f9f81

@@ -23,8 +23,7 @@

2323

import asyncio

2424

import random

2525

import sys

26-

from functools import lru_cache

27-

from ipaddress import IPv4Address, IPv6Address, _BaseAddress, ip_address

26+

from ipaddress import IPv4Address, IPv6Address, _BaseAddress

2827

from typing import TYPE_CHECKING, Dict, List, Optional, Set, Union, cast

29283029

from .._dns import (

@@ -47,6 +46,12 @@

4746

run_coro_with_timeout,

4847

wait_for_future_set_or_timeout,

4948

)

49+

from .._utils.ipaddress import (

50+

cached_ip_addresses,

51+

get_ip_address_object_from_record,

52+

ip_bytes_and_scope_to_address,

53+

str_without_scope_id,

54+

)

5055

from .._utils.name import service_type_name

5156

from .._utils.net import IPVersion, _encode_address

5257

from .._utils.time import current_time_millis

@@ -67,6 +72,8 @@

6772

_TYPE_TXT,

6873

)

697475+

IPADDRESS_SUPPORTS_SCOPE_ID = sys.version_info >= (3, 9, 0)

76+7077

_IPVersion_All_value = IPVersion.All.value

7178

_IPVersion_V4Only_value = IPVersion.V4Only.value

7279

# https://datatracker.ietf.org/doc/html/rfc6762#section-5.2

@@ -86,7 +93,6 @@

8693

DNS_QUESTION_TYPE_QU = DNSQuestionType.QU

8794

DNS_QUESTION_TYPE_QM = DNSQuestionType.QM

889589-

IPADDRESS_SUPPORTS_SCOPE_ID = sys.version_info >= (3, 9, 0)

90969197

if TYPE_CHECKING:

9298

from .._core import Zeroconf

@@ -102,41 +108,6 @@ def instance_name_from_service_info(info: "ServiceInfo", strict: bool = True) ->

102108

return info.name[: -len(service_name) - 1]

103109104110105-

@lru_cache(maxsize=512)

106-

def _cached_ip_addresses(address: Union[str, bytes, int]) -> Optional[Union[IPv4Address, IPv6Address]]:

107-

"""Cache IP addresses."""

108-

try:

109-

return ip_address(address)

110-

except ValueError:

111-

return None

112-113-114-

_cached_ip_addresses_wrapper = _cached_ip_addresses

115-116-117-

def _get_ip_address_object_from_record(record: DNSAddress) -> Optional[Union[IPv4Address, IPv6Address]]:

118-

"""Get the IP address object from the record."""

119-

if IPADDRESS_SUPPORTS_SCOPE_ID and record.type == _TYPE_AAAA and record.scope_id is not None:

120-

return _ip_bytes_and_scope_to_address(record.address, record.scope_id)

121-

return _cached_ip_addresses_wrapper(record.address)

122-123-124-

def _ip_bytes_and_scope_to_address(address: bytes_, scope: int_) -> Optional[Union[IPv4Address, IPv6Address]]:

125-

"""Convert the bytes and scope to an IP address object."""

126-

base_address = _cached_ip_addresses_wrapper(address)

127-

if base_address is not None and base_address.is_link_local:

128-

return _cached_ip_addresses_wrapper(f"{base_address}%{scope}")

129-

return base_address

130-131-132-

def _str_without_scope_id(addr: Union[IPv4Address, IPv6Address]) -> str:

133-

"""Return the string representation of the address without the scope id."""

134-

if IPADDRESS_SUPPORTS_SCOPE_ID and addr.version == 6:

135-

address_str = str(addr)

136-

return address_str.partition('%')[0]

137-

return str(addr)

138-139-140111

class ServiceInfo(RecordUpdateListener):

141112

"""Service information.

142113

@@ -271,9 +242,9 @@ def addresses(self, value: List[bytes]) -> None:

271242272243

for address in value:

273244

if IPADDRESS_SUPPORTS_SCOPE_ID and len(address) == 16 and self.interface_index is not None:

274-

addr = _ip_bytes_and_scope_to_address(address, self.interface_index)

245+

addr = ip_bytes_and_scope_to_address(address, self.interface_index)

275246

else:

276-

addr = _cached_ip_addresses_wrapper(address)

247+

addr = cached_ip_addresses(address)

277248

if addr is None:

278249

raise TypeError(

279250

"Addresses must either be IPv4 or IPv6 strings, bytes, or integers;"

@@ -369,7 +340,7 @@ def parsed_addresses(self, version: IPVersion = IPVersion.All) -> List[str]:

369340

This means the first address will always be the most recently added

370341

address of the given IP version.

371342

"""

372-

return [_str_without_scope_id(addr) for addr in self._ip_addresses_by_version_value(version.value)]

343+

return [str_without_scope_id(addr) for addr in self._ip_addresses_by_version_value(version.value)]

373344374345

def parsed_scoped_addresses(self, version: IPVersion = IPVersion.All) -> List[str]:

375346

"""Equivalent to parsed_addresses, with the exception that IPv6 Link-Local

@@ -446,7 +417,7 @@ def _get_ip_addresses_from_cache_lifo(

446417

for record in self._get_address_records_from_cache_by_type(zc, type):

447418

if record.is_expired(now):

448419

continue

449-

ip_addr = _get_ip_address_object_from_record(record)

420+

ip_addr = get_ip_address_object_from_record(record)

450421

if ip_addr is not None and ip_addr not in address_list:

451422

address_list.append(ip_addr)

452423

address_list.reverse() # Reverse to get LIFO order

@@ -496,7 +467,7 @@ def _process_record_threadsafe(self, zc: 'Zeroconf', record: DNSRecord, now: flo

496467

dns_address_record = record

497468

if TYPE_CHECKING:

498469

assert isinstance(dns_address_record, DNSAddress)

499-

ip_addr = _get_ip_address_object_from_record(dns_address_record)

470+

ip_addr = get_ip_address_object_from_record(dns_address_record)

500471

if ip_addr is None:

501472

log.warning(

502473

"Encountered invalid address while processing %s: %s",