Skip to content
Open
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ['setuptools>=77.0', 'Cython>=3.0.8', "poetry-core>=2.1.0"]
requires = ['setuptools>=77.0', 'Cython==3.2.5', "poetry-core>=2.1.0"]
build-backend = "poetry.core.masonry.api"

[project]
Expand Down
33 changes: 33 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# flake8: noqa
# ruff: noqa
from setuptools import setup

package_dir = {"": "src"}

packages = ["zeroconf", "zeroconf._handlers", "zeroconf._protocol", "zeroconf._services", "zeroconf._utils"]

package_data = {"": ["*"]}

install_requires = ["ifaddr>=0.1.7"]

setup_kwargs = {
"name": "zeroconf",
"version": "0.149.16",
"description": "A pure python implementation of multicast DNS service discovery",
"long_description": 'python-zeroconf\n===============\n\n.. image:: https://github.com/python-zeroconf/python-zeroconf/workflows/CI/badge.svg\n :target: https://github.com/python-zeroconf/python-zeroconf?query=workflow%3ACI+branch%3Amaster\n\n.. image:: https://img.shields.io/pypi/v/zeroconf.svg\n :target: https://pypi.python.org/pypi/zeroconf\n\n.. image:: https://codecov.io/gh/python-zeroconf/python-zeroconf/branch/master/graph/badge.svg\n :target: https://codecov.io/gh/python-zeroconf/python-zeroconf\n\n.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\n :target: https://codspeed.io/python-zeroconf/python-zeroconf\n :alt: Codspeed.io status for python-zeroconf\n\n.. image:: https://readthedocs.org/projects/python-zeroconf/badge/?version=latest\n :target: https://python-zeroconf.readthedocs.io/en/latest/?badge=latest\n :alt: Documentation Status\n\n`Documentation <https://python-zeroconf.readthedocs.io/en/latest/>`_.\n\nThis is fork of pyzeroconf, Multicast DNS Service Discovery for Python,\noriginally by Paul Scott-Murphy (https://github.com/paulsm/pyzeroconf),\nmodified by William McBrine (https://github.com/wmcbrine/pyzeroconf).\n\nThe original William McBrine\'s fork note::\n\n This fork is used in all of my TiVo-related projects: HME for Python\n (and therefore HME/VLC), Network Remote, Remote Proxy, and pyTivo.\n Before this, I was tracking the changes for zeroconf.py in three\n separate repos. I figured I should have an authoritative source.\n\n Although I make changes based on my experience with TiVos, I expect that\n they\'re generally applicable. This version also includes patches found\n on the now-defunct (?) Launchpad repo of pyzeroconf, and elsewhere\n around the net -- not always well-documented, sorry.\n\nCompatible with:\n\n* Bonjour\n* Avahi\n\nCompared to some other Zeroconf/Bonjour/Avahi Python packages, python-zeroconf:\n\n* isn\'t tied to Bonjour or Avahi\n* doesn\'t use D-Bus\n* doesn\'t force you to use particular event loop or Twisted (asyncio is used under the hood but not required)\n* is pip-installable\n* has PyPI distribution\n* has an optional cython extension for performance (pure python is supported as well)\n\nPython compatibility\n--------------------\n\n* CPython 3.10+\n* PyPy 3.10+\n\nVersioning\n----------\n\nThis project uses semantic versioning.\n\nStatus\n------\n\nThis project is actively maintained.\n\nTraffic Reduction\n-----------------\n\nBefore version 0.32, most traffic reduction techniques described in https://datatracker.ietf.org/doc/html/rfc6762#section-7\nwhere not implemented which could lead to excessive network traffic. It is highly recommended that version 0.32 or later\nis used if this is a concern.\n\nIPv6 support\n------------\n\nIPv6 support is relatively new and currently limited, specifically:\n\n* `InterfaceChoice.All` is an alias for `InterfaceChoice.Default` on non-POSIX\n systems.\n* Dual-stack IPv6 sockets are used, which may not be supported everywhere (some\n BSD variants do not have them).\n* Listening on localhost (`::1`) does not work. Help with understanding why is\n appreciated.\n\nHow to get python-zeroconf?\n===========================\n\n* PyPI page https://pypi.org/project/zeroconf/\n* GitHub project https://github.com/python-zeroconf/python-zeroconf\n\nThe easiest way to install python-zeroconf is using pip::\n\n pip install zeroconf\n\n\n\nHow do I use it?\n================\n\nHere\'s an example of browsing for a service:\n\n.. code-block:: python\n\n from zeroconf import ServiceBrowser, ServiceListener, Zeroconf\n\n\n class MyListener(ServiceListener):\n\n def update_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n print(f"Service {name} updated")\n\n def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n print(f"Service {name} removed")\n\n def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n info = zc.get_service_info(type_, name)\n print(f"Service {name} added, service info: {info}")\n\n\n zeroconf = Zeroconf()\n listener = MyListener()\n browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener)\n try:\n input("Press enter to exit...\\n\\n")\n finally:\n zeroconf.close()\n\n.. note::\n\n Discovery and service registration use *all* available network interfaces by default.\n If you want to customize that you need to specify ``interfaces`` argument when\n constructing ``Zeroconf`` object (see the code for details).\n\nIf you don\'t know the name of the service you need to browse for, try:\n\n.. code-block:: python\n\n from zeroconf import ZeroconfServiceTypes\n print(\'\\n\'.join(ZeroconfServiceTypes.find()))\n\nSee examples directory for more.\n\nChangelog\n=========\n\n`Changelog <CHANGELOG.md>`_\n\nLicense\n=======\n\nGNU Lesser General Public License v2.1 or later (LGPL-2.1-or-later).\n\nThe full text of LGPL 2.1 is included in the `COPYING <COPYING>`_ file.\nYou may, at your option, use this library under the terms of any later\nversion of the LGPL published by the Free Software Foundation. The\ncanonical SPDX identifier for this project is ``LGPL-2.1-or-later``, as\ndeclared in ``pyproject.toml``.\n',
"author": "Paul Scott-Murphy",
"author_email": "None",
"maintainer": "None",
"maintainer_email": "None",
"url": "https://github.com/python-zeroconf/python-zeroconf",
"package_dir": package_dir,
"packages": packages,
"package_data": package_data,
"install_requires": install_requires,
"python_requires": ">=3.10",
}
from build_ext import *

build(setup_kwargs)

setup(**setup_kwargs)
2 changes: 1 addition & 1 deletion src/zeroconf/_cache.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ cdef class DNSCache:
@cython.locals(record=DNSRecord, now=double)
cpdef current_entry_with_name_and_alias(self, str name, str alias)

cpdef void _async_set_created_ttl(
cpdef DNSRecord _async_set_created_ttl(
self,
DNSRecord record,
double now,
Expand Down
45 changes: 40 additions & 5 deletions src/zeroconf/_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,44 @@ def async_mark_unique_records_older_than_1s_to_expire(
# Expire in 1s
self._async_set_created_ttl(record, now, 1)

def _async_set_created_ttl(self, record: DNSRecord, now: _float, ttl: _int) -> None:
def _async_set_created_ttl(self, record: DNSRecord, now: _float, ttl: _int) -> DNSRecord:
"""Set the created time and ttl of a record."""
# It would be better if we made a copy instead of mutating the record
# in place, but records currently don't have a copy method.
record._set_created_ttl(now, ttl)
self._async_add(record)
# The class_ attribute is stripped of the unique bit, so we must restore it
# 0x8000 is _CLASS_UNIQUE
original_class = record.class_ | (0x8000 if record.unique else 0)

new_record: DNSRecord
if isinstance(record, DNSAddress):
new_record = DNSAddress(
record.name, record.type, original_class, ttl, record.address, record.scope_id, now
)
elif isinstance(record, DNSHinfo):
new_record = DNSHinfo(record.name, record.type, original_class, ttl, record.cpu, record.os, now)
elif isinstance(record, DNSPointer):
new_record = DNSPointer(record.name, record.type, original_class, ttl, record.alias, now)
elif isinstance(record, DNSText):
new_record = DNSText(record.name, record.type, original_class, ttl, record.text, now)
elif isinstance(record, DNSService):
new_record = DNSService(
record.name,
record.type,
original_class,
ttl,
record.priority,
record.weight,
record.port,
record.server,
now,
)
elif isinstance(record, DNSNsec):
new_record = DNSNsec(
record.name, record.type, original_class, ttl, record.next_name, record.rdtypes, now
)
else: # pragma: no cover
new_record = type(record)(record.name, record.type, original_class, ttl, now)

store = self.cache.get(record.key)
if store is not None and record in store:
self.async_remove_records([record])
self._async_add(new_record)
return new_record
7 changes: 2 additions & 5 deletions src/zeroconf/_handlers/record_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,5 @@ def async_remove_listener(self, listener: RecordUpdateListener) -> None:
This function is not threadsafe and must be called in the eventloop.
"""
try:
self.listeners.remove(listener)
self.zc.async_notify_all()
except ValueError as e:
log.exception("Failed to remove listener: %r", e)
self.listeners.discard(listener)
self.zc.async_notify_all()
1 change: 0 additions & 1 deletion src/zeroconf/_logger.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Multicast DNS Service Discovery for Python, v0.14-wmcbrine
)
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
This module provides a framework for the use of DNS Service Discovery
Expand Down
2 changes: 1 addition & 1 deletion src/zeroconf/_services/registry.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ cdef class ServiceRegistry:
cdef public bint has_entries

@cython.locals(
record_list=cython.list,
record_list=cython.dict,
)
cdef cython.list _async_get_by_index(self, cython.dict records, str key)

Expand Down
14 changes: 7 additions & 7 deletions src/zeroconf/_services/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ def __init__(
) -> None:
"""Create the ServiceRegistry class."""
self._services: dict[str, ServiceInfo] = {}
self.types: dict[str, list] = {}
self.servers: dict[str, list] = {}
self.types: dict[str, dict[str, None]] = {}
self.servers: dict[str, dict[str, None]] = {}
self.has_entries: bool = False

def async_add(self, info: ServiceInfo) -> None:
Expand Down Expand Up @@ -79,7 +79,7 @@ def async_get_infos_server(self, server: str) -> list[ServiceInfo]:
"""Return all ServiceInfo matching server."""
return self._async_get_by_index(self.servers, server)

def _async_get_by_index(self, records: dict[str, list], key: _str) -> list[ServiceInfo]:
def _async_get_by_index(self, records: dict[str, dict[str, None]], key: _str) -> list[ServiceInfo]:
"""Return all ServiceInfo matching the index."""
record_list = records.get(key)
if record_list is None:
Expand All @@ -94,8 +94,8 @@ def _add(self, info: ServiceInfo) -> None:

info.async_clear_cache()
self._services[info.key] = info
self.types.setdefault(info.type.lower(), []).append(info.key)
self.servers.setdefault(info.server_key, []).append(info.key)
self.types.setdefault(info.type.lower(), {})[info.key] = None
self.servers.setdefault(info.server_key, {})[info.key] = None
self.has_entries = True

def _remove(self, infos: list[ServiceInfo]) -> None:
Expand All @@ -105,8 +105,8 @@ def _remove(self, infos: list[ServiceInfo]) -> None:
if old_service_info is None:
continue
assert old_service_info.server_key is not None
self.types[old_service_info.type.lower()].remove(info.key)
self.servers[old_service_info.server_key].remove(info.key)
self.types[old_service_info.type.lower()].pop(info.key, None)
self.servers[old_service_info.server_key].pop(info.key, None)
del self._services[info.key]

self.has_entries = bool(self._services)
2 changes: 1 addition & 1 deletion tests/services/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1562,7 +1562,7 @@ def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-de
# Force the ttl to be 1 second
now = current_time_millis()
for cache_record in list(zc.cache.cache.values()):
for record in cache_record.values():
for record in list(cache_record.values()):
zc.cache._async_set_created_ttl(record, now, 1)

# Wait for the add callback to fire from the original inject_response.
Expand Down
5 changes: 3 additions & 2 deletions tests/services/test_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
import unittest
from ipaddress import ip_address
from threading import Event
from typing import cast
from unittest.mock import patch

import pytest

import zeroconf as r
from zeroconf import DNSAddress, RecordUpdate, const
from zeroconf import DNSAddress, DNSText, RecordUpdate, const
from zeroconf._protocol.outgoing import DNSOutgoing
from zeroconf._services import info
from zeroconf._services.info import ServiceInfo, _has_more_scope_info
Expand Down Expand Up @@ -245,7 +246,7 @@ def test_service_info_rejects_expired_records(self):
ttl,
b"\x04ff=0\x04ci=3\x04sf=0\x0bsh=6fLM5A==",
)
zc.cache._async_set_created_ttl(expired_record, 1000, 1)
expired_record = cast(DNSText, zc.cache._async_set_created_ttl(expired_record, 1000, 1))
info.async_update_records(zc, now, [RecordUpdate(expired_record, None)])
assert info.properties[b"ci"] == b"2"
zc.close()
Expand Down
20 changes: 20 additions & 0 deletions tests/test_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,3 +719,23 @@ def actual() -> int:
cache.async_add_records([rec])
assert cache._total_records == actual()
assert cache._total_records == const._MAX_CACHE_RECORDS


def test_cache_async_set_created_ttl_dnsnsec():
from zeroconf._cache import DNSCache # noqa: PLC0415
from zeroconf._dns import DNSHinfo, DNSNsec # noqa: PLC0415
from zeroconf.const import _CLASS_IN # noqa: PLC0415

record = DNSNsec("test.local.", 47, _CLASS_IN, 100, "next.local.", [1, 2, 3])
cache = DNSCache()
cache.async_add_records([record])
new_record = cache._async_set_created_ttl(record, 10.0, 50)
assert isinstance(new_record, DNSNsec)
assert new_record.ttl == 50

# DNSHinfo coverage
hinfo_record = DNSHinfo("test-hinfo.local.", 13, _CLASS_IN, 100, "cpu", "os")
cache.async_add_records([hinfo_record])
new_hinfo = cache._async_set_created_ttl(hinfo_record, 10.0, 50)
assert isinstance(new_hinfo, DNSHinfo)
assert new_hinfo.ttl == 50
19 changes: 14 additions & 5 deletions tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import pytest

import zeroconf as r
from zeroconf import ServiceInfo, Zeroconf, const, current_time_millis
from zeroconf import DNSAddress, DNSPointer, ServiceInfo, Zeroconf, const, current_time_millis
from zeroconf._handlers.multicast_outgoing_queue import (
MulticastOutgoingQueue,
construct_outgoing_multicast_answers,
Expand Down Expand Up @@ -1149,7 +1149,12 @@ async def test_qu_response_only_sends_additionals_if_sends_answer():

# Add the A record to the cache with 50% ttl remaining
a_record = info.dns_addresses()[0]
zc.cache._async_set_created_ttl(a_record, current_time_millis() - (a_record.ttl * 1000 / 2), a_record.ttl)
a_record = cast(
DNSAddress,
zc.cache._async_set_created_ttl(
a_record, current_time_millis() - (a_record.ttl * 1000 / 2), a_record.ttl
),
)
assert not a_record.is_recent(current_time_millis())
info._dns_address_cache = None # we are mutating the record so clear the cache

Expand Down Expand Up @@ -1199,8 +1204,11 @@ async def test_qu_response_only_sends_additionals_if_sends_answer():

# Remove the 100% PTR record and add a 50% PTR record
zc.cache.async_remove_records([ptr_record])
zc.cache._async_set_created_ttl(
ptr_record, current_time_millis() - (ptr_record.ttl * 1000 / 2), ptr_record.ttl
ptr_record = cast(
DNSPointer,
zc.cache._async_set_created_ttl(
ptr_record, current_time_millis() - (ptr_record.ttl * 1000 / 2), ptr_record.ttl
),
)
assert not ptr_record.is_recent(current_time_millis())
# With QU should respond to only multicast since the has less
Expand Down Expand Up @@ -1318,7 +1326,8 @@ async def test_cache_flush_bit():
out.add_answer_at_time(answer, 0)
for packet in out.packets():
zc.record_manager.async_updates_from_response(r.DNSIncoming(packet))
assert original_a_record.ttl == 1
unique_a = zc.cache.async_get_unique(a_record)
assert unique_a is not None and unique_a.ttl == 1
for record in new_records:
assert zc.cache.async_get_unique(record) is not None

Expand Down
Loading