Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/zeroconf/_utils/ipaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

from __future__ import annotations

from functools import cache, lru_cache
from functools import lru_cache
from ipaddress import AddressValueError, IPv4Address, IPv6Address, NetmaskValueError
from typing import Any

Expand All @@ -34,7 +34,7 @@


class ZeroconfIPv4Address(IPv4Address):
__slots__ = ("__hash__", "_is_link_local", "_is_loopback", "_is_unspecified", "_str", "zc_integer")
__slots__ = ("_hash", "_is_link_local", "_is_loopback", "_is_unspecified", "_str", "zc_integer")

def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize a new IPv4 address."""
Expand All @@ -43,13 +43,17 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self._is_link_local = super().is_link_local
self._is_unspecified = super().is_unspecified
self._is_loopback = super().is_loopback
self.__hash__ = cache(lambda: IPv4Address.__hash__(self)) # type: ignore[method-assign]
self._hash = IPv4Address.__hash__(self)
self.zc_integer = int(self)

def __str__(self) -> str:
"""Return the string representation of the IPv4 address."""
return self._str

def __hash__(self) -> int:
"""Return the precomputed hash of the IPv4 address."""
return self._hash

@property
def is_link_local(self) -> bool:
"""Return True if this is a link-local address."""
Expand All @@ -67,7 +71,7 @@ def is_loopback(self) -> bool:


class ZeroconfIPv6Address(IPv6Address):
__slots__ = ("__hash__", "_is_link_local", "_is_loopback", "_is_unspecified", "_str", "zc_integer")
__slots__ = ("_hash", "_is_link_local", "_is_loopback", "_is_unspecified", "_str", "zc_integer")

def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize a new IPv6 address."""
Expand All @@ -76,13 +80,17 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self._is_link_local = super().is_link_local
self._is_unspecified = super().is_unspecified
self._is_loopback = super().is_loopback
self.__hash__ = cache(lambda: IPv6Address.__hash__(self)) # type: ignore[method-assign]
self._hash = IPv6Address.__hash__(self)
self.zc_integer = int(self)

def __str__(self) -> str:
"""Return the string representation of the IPv6 address."""
return self._str

def __hash__(self) -> int:
"""Return the precomputed hash of the IPv6 address."""
return self._hash

@property
def is_link_local(self) -> bool:
"""Return True if this is a link-local address."""
Expand Down
14 changes: 14 additions & 0 deletions tests/utils/test_ipaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ def test_cached_ip_addresses_wrapper():
assert ipv6.is_unspecified is True


def test_address_hash_matches_stdlib_and_dedups():
"""Cached address objects hash like their stdlib equals and dedup in sets."""
v4 = ipaddress.cached_ip_addresses("192.168.1.1")
assert v4 is not None
assert hash(v4) == hash(ipaddress.IPv4Address("192.168.1.1"))
assert hash(v4) == hash(v4)
assert len({v4, ipaddress.ZeroconfIPv4Address("192.168.1.1")}) == 1

v6 = ipaddress.cached_ip_addresses("fe80::1")
assert v6 is not None
assert hash(v6) == hash(ipaddress.IPv6Address("fe80::1"))
assert len({v6, ipaddress.ZeroconfIPv6Address("fe80::1")}) == 1


def test_get_ip_address_object_from_record():
"""Test the get_ip_address_object_from_record."""
# not link local
Expand Down
Loading