zeroconf 0.151.3__tar.gz → 0.151.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. {zeroconf-0.151.3 → zeroconf-0.151.6}/CHANGELOG.md +27 -0
  2. {zeroconf-0.151.3 → zeroconf-0.151.6}/PKG-INFO +2 -1
  3. {zeroconf-0.151.3 → zeroconf-0.151.6}/pyproject.toml +4 -4
  4. {zeroconf-0.151.3 → zeroconf-0.151.6}/setup.py +1 -1
  5. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/__init__.py +1 -1
  6. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_cache.py +2 -2
  7. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_handlers/query_handler.py +3 -2
  8. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_listener.py +1 -1
  9. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_services/info.pxd +3 -1
  10. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_services/info.py +8 -8
  11. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_utils/net.py +11 -3
  12. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/const.py +4 -2
  13. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_listener.py +20 -1
  14. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/utils/test_net.py +29 -0
  15. {zeroconf-0.151.3 → zeroconf-0.151.6}/COPYING +0 -0
  16. {zeroconf-0.151.3 → zeroconf-0.151.6}/README.rst +0 -0
  17. {zeroconf-0.151.3 → zeroconf-0.151.6}/build_ext.py +0 -0
  18. {zeroconf-0.151.3 → zeroconf-0.151.6}/docs/Makefile +0 -0
  19. {zeroconf-0.151.3 → zeroconf-0.151.6}/docs/_ext/zeroconfautodocfix.py +0 -0
  20. {zeroconf-0.151.3 → zeroconf-0.151.6}/docs/api.rst +0 -0
  21. {zeroconf-0.151.3 → zeroconf-0.151.6}/docs/conf.py +0 -0
  22. {zeroconf-0.151.3 → zeroconf-0.151.6}/docs/index.rst +0 -0
  23. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_cache.pxd +0 -0
  24. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_core.py +0 -0
  25. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_dns.pxd +0 -0
  26. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_dns.py +0 -0
  27. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_engine.py +0 -0
  28. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_exceptions.py +0 -0
  29. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_handlers/__init__.py +0 -0
  30. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_handlers/answers.pxd +0 -0
  31. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_handlers/answers.py +0 -0
  32. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_handlers/multicast_outgoing_queue.pxd +0 -0
  33. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_handlers/multicast_outgoing_queue.py +0 -0
  34. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_handlers/query_handler.pxd +0 -0
  35. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_handlers/record_manager.pxd +0 -0
  36. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_handlers/record_manager.py +0 -0
  37. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_history.pxd +0 -0
  38. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_history.py +0 -0
  39. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_listener.pxd +0 -0
  40. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_logger.py +0 -0
  41. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_protocol/__init__.py +0 -0
  42. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_protocol/incoming.pxd +0 -0
  43. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_protocol/incoming.py +0 -0
  44. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_protocol/outgoing.pxd +0 -0
  45. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_protocol/outgoing.py +0 -0
  46. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_record_update.pxd +0 -0
  47. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_record_update.py +0 -0
  48. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_services/__init__.pxd +0 -0
  49. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_services/__init__.py +0 -0
  50. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_services/browser.pxd +0 -0
  51. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_services/browser.py +0 -0
  52. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_services/registry.pxd +0 -0
  53. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_services/registry.py +0 -0
  54. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_services/types.py +0 -0
  55. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_transport.py +0 -0
  56. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_updates.pxd +0 -0
  57. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_updates.py +0 -0
  58. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_utils/__init__.py +0 -0
  59. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_utils/asyncio.py +0 -0
  60. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_utils/ipaddress.pxd +0 -0
  61. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_utils/ipaddress.py +0 -0
  62. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_utils/name.py +0 -0
  63. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_utils/time.pxd +0 -0
  64. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/_utils/time.py +0 -0
  65. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/asyncio.py +0 -0
  66. {zeroconf-0.151.3 → zeroconf-0.151.6}/src/zeroconf/py.typed +0 -0
  67. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/__init__.py +0 -0
  68. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/benchmarks/__init__.py +0 -0
  69. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/benchmarks/helpers.py +0 -0
  70. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/benchmarks/test_cache.py +0 -0
  71. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/benchmarks/test_cache_bound.py +0 -0
  72. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/benchmarks/test_cache_mark_expire.py +0 -0
  73. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/benchmarks/test_incoming.py +0 -0
  74. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/benchmarks/test_ipaddress.py +0 -0
  75. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/benchmarks/test_listener_dedup.py +0 -0
  76. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/benchmarks/test_mark_seen.py +0 -0
  77. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/benchmarks/test_outgoing.py +0 -0
  78. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/benchmarks/test_registry.py +0 -0
  79. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/benchmarks/test_send.py +0 -0
  80. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/benchmarks/test_txt_properties.py +0 -0
  81. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/conftest.py +0 -0
  82. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/services/__init__.py +0 -0
  83. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/services/test_browser.py +0 -0
  84. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/services/test_info.py +0 -0
  85. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/services/test_registry.py +0 -0
  86. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/services/test_types.py +0 -0
  87. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_asyncio.py +0 -0
  88. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_cache.py +0 -0
  89. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_circular_imports.py +0 -0
  90. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_core.py +0 -0
  91. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_dns.py +0 -0
  92. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_engine.py +0 -0
  93. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_exceptions.py +0 -0
  94. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_fuzz_incoming.py +0 -0
  95. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_handlers.py +0 -0
  96. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_history.py +0 -0
  97. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_init.py +0 -0
  98. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_interface_update.py +0 -0
  99. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_logger.py +0 -0
  100. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_protocol.py +0 -0
  101. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_services.py +0 -0
  102. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/test_updates.py +0 -0
  103. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/utils/__init__.py +0 -0
  104. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/utils/test_asyncio.py +0 -0
  105. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/utils/test_ipaddress.py +0 -0
  106. {zeroconf-0.151.3 → zeroconf-0.151.6}/tests/utils/test_name.py +0 -0
@@ -2,6 +2,33 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v0.151.6 (2026-10-31)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Shorten the duplicate packet window to 800 ms
10
+ ([#1930](https://github.com/python-zeroconf/python-zeroconf/pull/1930),
11
+ [`c245c2f`](https://github.com/python-zeroconf/python-zeroconf/commit/c245c2fab48ebedd145895b9fa5ee36f86715159))
12
+
13
+
14
+ ## v0.151.5 (2026-09-28)
15
+
16
+ ### Bug Fixes
17
+
18
+ - Build only the sdist in the release job
19
+ ([#1921](https://github.com/python-zeroconf/python-zeroconf/pull/1921),
20
+ [`1d939f4`](https://github.com/python-zeroconf/python-zeroconf/commit/1d939f471f045a2e4a5d44cfe35cf4f61e55c5ba))
21
+
22
+
23
+ ## v0.151.4 (2026-09-28)
24
+
25
+ ### Bug Fixes
26
+
27
+ - Skip adapter scan for interface lists with no IPv6 entries
28
+ ([#1919](https://github.com/python-zeroconf/python-zeroconf/pull/1919),
29
+ [`47ec4ba`](https://github.com/python-zeroconf/python-zeroconf/commit/47ec4bab70d548503b937ba357f160eebfa2af1f))
30
+
31
+
5
32
  ## v0.151.3 (2026-08-30)
6
33
 
7
34
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zeroconf
3
- Version: 0.151.3
3
+ Version: 0.151.6
4
4
  Summary: A pure python implementation of multicast DNS service discovery
5
5
  License-Expression: LGPL-2.1-or-later
6
6
  License-File: COPYING
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: 3.15
23
24
  Classifier: Programming Language :: Python :: Implementation :: CPython
24
25
  Classifier: Programming Language :: Python :: Implementation :: PyPy
25
26
  Classifier: Topic :: Software Development :: Libraries
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "zeroconf"
7
- version = "0.151.3"
7
+ version = "0.151.6"
8
8
  license = "LGPL-2.1-or-later"
9
9
  description = "A pure python implementation of multicast DNS service discovery"
10
10
  readme = "README.rst"
@@ -56,7 +56,7 @@ version_toml = ["pyproject.toml:project.version"]
56
56
  version_variables = [
57
57
  "src/zeroconf/__init__.py:__version__"
58
58
  ]
59
- build_command = "pip install poetry && poetry build"
59
+ build_command = "pip install poetry && poetry build --format sdist"
60
60
  tag_format = "{version}"
61
61
  allow_zero_version = true
62
62
 
@@ -91,8 +91,8 @@ cython = "^3.3.0"
91
91
  setuptools = ">=84.0.0,<85.0.0"
92
92
  pytest-timeout = "^2.1.0"
93
93
  pytest-codspeed = ">=5.0.3,<6.0"
94
- blockbuster = ">=1.5.27,<2.0.0"
95
- hypothesis = {version = "^6.165.10", markers = "implementation_name == 'cpython'"}
94
+ blockbuster = ">=1.5.29,<2.0.0"
95
+ hypothesis = {version = "^6.168.5", markers = "implementation_name == 'cpython'"}
96
96
 
97
97
  [tool.poetry.group.docs.dependencies]
98
98
  sphinx = "^7.4.7 || ^8.1.3"
@@ -19,7 +19,7 @@ install_requires = \
19
19
 
20
20
  setup_kwargs = {
21
21
  'name': 'zeroconf',
22
- 'version': '0.151.3',
22
+ 'version': '0.151.6',
23
23
  'description': 'A pure python implementation of multicast DNS service discovery',
24
24
  '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 project is a fork of pyzeroconf, originally by Paul Scott-Murphy\n(https://github.com/paulsm/pyzeroconf), modified by William McBrine\n(https://github.com/wmcbrine/pyzeroconf).\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 how to discover services:\n\n.. code-block:: python\n\n from zeroconf import ServiceBrowser, ServiceListener, Zeroconf\n\n\n class EventLogger(ServiceListener):\n def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n info = zc.get_service_info(type_, name)\n print(f"discovered {name}: {info}")\n\n def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n print(f"lost {name}")\n\n def update_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n print(f"refreshed {name}")\n\n\n with Zeroconf() as zc:\n ServiceBrowser(zc, "_http._tcp.local.", EventLogger())\n input("browsing, press enter to stop\\n")\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\nTo see which service types are present on the network, 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',
25
25
  'author': 'The python-zeroconf authors',
@@ -73,7 +73,7 @@ from ._utils.time import ( # noqa # import needed for backwards compat
73
73
 
74
74
  __author__ = "The python-zeroconf authors" # full history in COPYING and git
75
75
  __maintainer__ = "J. Nick Koston <nick@koston.org>"
76
- __version__ = "0.151.3"
76
+ __version__ = "0.151.6"
77
77
  __license__ = "LGPL"
78
78
 
79
79
 
@@ -77,7 +77,7 @@ class DNSCache:
77
77
  return new
78
78
 
79
79
  def async_all_by_details(self, name: _str, type_: _int, class_: _int) -> list[DNSRecord]:
80
- """Gets all matching entries by details.
80
+ """Return every cached record carrying this name, type and class.
81
81
 
82
82
  This function is not thread-safe and must be called from
83
83
  the event loop.
@@ -206,7 +206,7 @@ class DNSCache:
206
206
  return [entry for entry in list(records.values()) if type_ == entry.type and class_ == entry.class_]
207
207
 
208
208
  def get_by_details(self, name: str, type_: _int, class_: _int) -> DNSRecord | None:
209
- """Gets the first matching entry by details. Returns None if no entries match.
209
+ """Return the first cached record carrying this name, type and class, or None.
210
210
 
211
211
  Calling this function is not recommended as it will only
212
212
  return one record even if there are multiple entries.
@@ -247,8 +247,9 @@ class QueryHandler:
247
247
  continue
248
248
  if ucast_source:
249
249
  query_res.add_ucast_question_response(answer_set)
250
- # We always multicast as well even if its a unicast
251
- # source as long as we haven't done it recently (75% of ttl)
250
+ # The answer also goes out by multicast even for a unicast
251
+ # source; a copy multicast within the last second is folded
252
+ # into the aggregated response instead (RFC 6762 section 14)
252
253
  query_res.add_mcast_question_response(answer_set)
253
254
 
254
255
  return query_res.answers()
@@ -251,7 +251,7 @@ class AsyncListener:
251
251
  # _DUPLICATE_PACKET_SUPPRESSION_INTERVAL` so a cache miss never
252
252
  # triggers the branch even when `now` is small (time.monotonic
253
253
  # is allowed to start near zero, leaving `now - INTERVAL`
254
- # negative for the first ~1s after boot). Only non-QU payloads
254
+ # negative for the first interval after boot). Only non-QU payloads
255
255
  # are cached, so any hit here is safe to suppress without re-
256
256
  # checking has_qu_question.
257
257
  recent_time = self._recent_packets.get(data, -1e30)
@@ -90,7 +90,9 @@ cdef class ServiceInfo(RecordUpdateListener):
90
90
 
91
91
  @cython.locals(
92
92
  length="unsigned char",
93
- index="unsigned int",
93
+ pos="unsigned int",
94
+ start="unsigned int",
95
+ total="unsigned int",
94
96
  key_value=bytes,
95
97
  key=bytes,
96
98
  sep=bytes,
@@ -1044,26 +1044,26 @@ class ServiceInfo(RecordUpdateListener):
1044
1044
  def _unpack_text_into_properties(self) -> None:
1045
1045
  """Unpacks the text field into properties"""
1046
1046
  text = self.text
1047
- end = len(text)
1048
- if end == 0:
1047
+ total = len(text)
1048
+ if total == 0:
1049
1049
  # Properties should be set atomically
1050
1050
  # in case another thread is reading them
1051
1051
  self._properties = {}
1052
1052
  return
1053
1053
 
1054
- index = 0
1055
1054
  properties: dict[bytes, bytes | None] = {}
1056
- while index < end:
1057
- length = text[index]
1058
- index += 1
1059
- key_value = text[index : index + length]
1055
+ pos = 0
1056
+ while pos < total:
1057
+ length = text[pos]
1058
+ start = pos + 1
1059
+ key_value = text[start : start + length]
1060
+ pos = start + length
1060
1061
  key, sep, value = key_value.partition(b"=")
1061
1062
  if key not in properties:
1062
1063
  # RFC 6763 section 6.4 distinguishes a key with no '=' (a
1063
1064
  # boolean attribute: present, no value) from `key=` (present
1064
1065
  # with an empty value), so test the separator, not the value.
1065
1066
  properties[key] = value if sep else None
1066
- index += length
1067
1067
 
1068
1068
  self._properties = properties
1069
1069
 
@@ -126,13 +126,21 @@ def ip6_addresses_to_indexes(
126
126
  :param interfaces: List of IP addresses and indexes.
127
127
  :returns: List of indexes.
128
128
  """
129
- result = []
129
+ result: list[tuple[tuple[str, int, int], int]] = []
130
+ needs_scan = [
131
+ iface
132
+ for iface in interfaces
133
+ if isinstance(iface, int) or (isinstance(iface, str) and ipaddress.ip_address(iface).version == 6)
134
+ ]
135
+ if not needs_scan:
136
+ return result
137
+
130
138
  adapters = ifaddr.get_adapters()
131
139
 
132
- for iface in interfaces:
140
+ for iface in needs_scan:
133
141
  if isinstance(iface, int):
134
142
  result.append((interface_index_to_ip6_address(adapters, iface), iface)) # type: ignore[arg-type]
135
- elif isinstance(iface, str) and ipaddress.ip_address(iface).version == 6:
143
+ else:
136
144
  result.append(ip6_to_address_and_index(adapters, iface)) # type: ignore[arg-type]
137
145
 
138
146
  return result
@@ -15,14 +15,16 @@ _CHECK_TIME = 500 # ms
15
15
  _LISTENER_TIME = 200 # ms
16
16
  _REGISTER_TIME = 225 # ms
17
17
  _UNREGISTER_TIME = 125 # ms
18
- _DUPLICATE_PACKET_SUPPRESSION_INTERVAL = 1000 # ms
18
+ # Below the ~1s spacing of repeated announcements (RFC 6762 §8.3), which some
19
+ # responders jitter to a little under a second.
20
+ _DUPLICATE_PACKET_SUPPRESSION_INTERVAL = 800 # ms
19
21
  # Per-listener bounded recency window. 16 is large enough to defeat
20
22
  # the alternating-payload bypass (RFC 6762 §6.2, issue #1724 — even a
21
23
  # rotation of a dozen distinct payloads still dedups), and small
22
24
  # enough that the dict bookkeeping per miss stays cheap under a
23
25
  # hostile flood.
24
26
  _RECENT_PACKETS_MAX = 16
25
- _DUPLICATE_QUESTION_INTERVAL = 999 # ms # Must be 1ms less than _DUPLICATE_PACKET_SUPPRESSION_INTERVAL
27
+ _DUPLICATE_QUESTION_INTERVAL = 999 # ms
26
28
  _CACHE_CLEANUP_INTERVAL = 10 # s
27
29
  _LOADED_SYSTEM_TIMEOUT = 10 # s
28
30
  _STARTUP_TIMEOUT = 9 # s must be lower than _LOADED_SYSTEM_TIMEOUT
@@ -389,7 +389,7 @@ def test_recent_packets_miss_with_small_now_is_not_suppressed(zc: Zeroconf) -> N
389
389
  """A cache miss must not trigger suppression when `now` is below the suppression interval."""
390
390
  # time.monotonic() can start near zero on freshly booted systems, so
391
391
  # `now - _DUPLICATE_PACKET_SUPPRESSION_INTERVAL` is negative for the
392
- # first second of process lifetime. A 0.0 default on the recency
392
+ # first interval of process lifetime. A 0.0 default on the recency
393
393
  # dict would let any negative `now - INTERVAL` satisfy the compare
394
394
  # and suppress legitimate traffic.
395
395
  zc.registry.async_add(
@@ -425,3 +425,22 @@ def test_recent_packets_miss_with_small_now_is_not_suppressed(zc: Zeroconf) -> N
425
425
  with patch.object(listener, "handle_query_or_defer") as _handle_query_or_defer:
426
426
  listener._process_datagram_at_time(False, len(packet), 0.0, packet, addrs)
427
427
  _handle_query_or_defer.assert_called_once()
428
+
429
+
430
+ def test_repeated_announcement_under_one_second_is_processed(zc: Zeroconf) -> None:
431
+ """An announcement repeated a little under a second later is not dropped as a duplicate."""
432
+ listener = _listener.AsyncListener(zc)
433
+ listener.transport = MagicMock()
434
+ response = r.DNSOutgoing(const._FLAGS_QR_RESPONSE | const._FLAGS_AA)
435
+ response.add_answer_at_time(
436
+ r.DNSAddress("device.local.", const._TYPE_A, const._CLASS_IN, 120, b"\x7f\x00\x00\x01"), 0
437
+ )
438
+ packet = response.packets()[0]
439
+ addrs = ("1.2.3.4", 5353)
440
+
441
+ now = current_time_millis()
442
+ listener._process_datagram_at_time(False, len(packet), now, packet, addrs)
443
+ listener._process_datagram_at_time(False, len(packet), now + 5, packet, addrs)
444
+ assert listener.last_time == now
445
+ listener._process_datagram_at_time(False, len(packet), now + 926, packet, addrs)
446
+ assert listener.last_time == now + 926
@@ -118,6 +118,35 @@ def test_ip6_addresses_to_indexes():
118
118
  assert netutils.ip6_addresses_to_indexes(interfaces_2) == [(("2001:db8::", 1, 1), 1)]
119
119
 
120
120
 
121
+ @pytest.mark.parametrize("interfaces", [[], ["192.168.1.5"], ["192.168.1.5", "169.254.3.2"]])
122
+ def test_ip6_addresses_to_indexes_skips_scan_without_ipv6(interfaces: list[str]) -> None:
123
+ """Test the adapters are not scanned when there are no IPv6 entries."""
124
+ with patch("zeroconf._utils.net.ifaddr.get_adapters") as mock_get_adapters:
125
+ assert netutils.ip6_addresses_to_indexes(interfaces) == []
126
+ mock_get_adapters.assert_not_called()
127
+
128
+
129
+ def test_ip6_addresses_to_indexes_scans_once() -> None:
130
+ """Test the adapters are scanned once for a mixed list."""
131
+ with patch(
132
+ "zeroconf._utils.net.ifaddr.get_adapters",
133
+ return_value=_generate_mock_adapters(),
134
+ ) as mock_get_adapters:
135
+ assert netutils.ip6_addresses_to_indexes(["192.168.1.5", "2001:db8::", 1, "fd00:db8::"]) == [
136
+ (("2001:db8::", 1, 1), 1),
137
+ (("2001:db8::", 1, 1), 1),
138
+ (("fd00:db8::", 1, 1), 1),
139
+ ]
140
+ mock_get_adapters.assert_called_once()
141
+
142
+
143
+ def test_normalize_interface_choice_ipv4_list_skips_scan() -> None:
144
+ """Test an IPv4 only list is normalized without scanning the adapters."""
145
+ with patch("zeroconf._utils.net.ifaddr.get_adapters") as mock_get_adapters:
146
+ assert netutils.normalize_interface_choice(["192.168.1.5"]) == ["192.168.1.5"]
147
+ mock_get_adapters.assert_not_called()
148
+
149
+
121
150
  def test_normalize_interface_choice_errors():
122
151
  """Test we generate exception on invalid input."""
123
152
  with (
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes