zeroconf 0.149.17__tar.gz → 0.150.2__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.149.17 → zeroconf-0.150.2}/CHANGELOG.md +32 -0
  2. {zeroconf-0.149.17 → zeroconf-0.150.2}/PKG-INFO +1 -1
  3. {zeroconf-0.149.17 → zeroconf-0.150.2}/pyproject.toml +4 -3
  4. {zeroconf-0.149.17 → zeroconf-0.150.2}/setup.py +1 -1
  5. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/__init__.py +1 -1
  6. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_cache.py +2 -1
  7. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_core.py +94 -0
  8. zeroconf-0.150.2/src/zeroconf/_engine.py +471 -0
  9. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_handlers/query_handler.pxd +0 -1
  10. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_handlers/query_handler.py +9 -7
  11. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_handlers/record_manager.py +4 -2
  12. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_listener.pxd +2 -0
  13. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_listener.py +18 -4
  14. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_protocol/incoming.py +1 -1
  15. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_services/browser.pxd +2 -2
  16. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_services/browser.py +8 -5
  17. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_services/info.pxd +8 -1
  18. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_services/info.py +8 -5
  19. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_services/registry.py +3 -2
  20. zeroconf-0.150.2/src/zeroconf/_transport.py +129 -0
  21. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_utils/net.py +67 -11
  22. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/asyncio.py +16 -0
  23. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/services/test_info.py +58 -0
  24. zeroconf-0.150.2/tests/test_interface_update.py +974 -0
  25. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/utils/test_net.py +103 -0
  26. zeroconf-0.149.17/src/zeroconf/_engine.py +0 -195
  27. zeroconf-0.149.17/src/zeroconf/_transport.py +0 -68
  28. {zeroconf-0.149.17 → zeroconf-0.150.2}/COPYING +0 -0
  29. {zeroconf-0.149.17 → zeroconf-0.150.2}/README.rst +0 -0
  30. {zeroconf-0.149.17 → zeroconf-0.150.2}/build_ext.py +0 -0
  31. {zeroconf-0.149.17 → zeroconf-0.150.2}/docs/Makefile +0 -0
  32. {zeroconf-0.149.17 → zeroconf-0.150.2}/docs/_ext/zeroconfautodocfix.py +0 -0
  33. {zeroconf-0.149.17 → zeroconf-0.150.2}/docs/api.rst +0 -0
  34. {zeroconf-0.149.17 → zeroconf-0.150.2}/docs/conf.py +0 -0
  35. {zeroconf-0.149.17 → zeroconf-0.150.2}/docs/index.rst +0 -0
  36. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_cache.pxd +0 -0
  37. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_dns.pxd +0 -0
  38. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_dns.py +0 -0
  39. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_exceptions.py +0 -0
  40. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_handlers/__init__.py +0 -0
  41. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_handlers/answers.pxd +0 -0
  42. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_handlers/answers.py +0 -0
  43. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_handlers/multicast_outgoing_queue.pxd +0 -0
  44. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_handlers/multicast_outgoing_queue.py +0 -0
  45. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_handlers/record_manager.pxd +0 -0
  46. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_history.pxd +0 -0
  47. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_history.py +0 -0
  48. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_logger.py +0 -0
  49. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_protocol/__init__.py +0 -0
  50. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_protocol/incoming.pxd +0 -0
  51. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_protocol/outgoing.pxd +0 -0
  52. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_protocol/outgoing.py +0 -0
  53. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_record_update.pxd +0 -0
  54. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_record_update.py +0 -0
  55. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_services/__init__.pxd +0 -0
  56. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_services/__init__.py +0 -0
  57. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_services/registry.pxd +0 -0
  58. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_services/types.py +0 -0
  59. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_updates.pxd +0 -0
  60. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_updates.py +0 -0
  61. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_utils/__init__.py +0 -0
  62. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_utils/asyncio.py +0 -0
  63. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_utils/ipaddress.pxd +0 -0
  64. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_utils/ipaddress.py +0 -0
  65. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_utils/name.py +0 -0
  66. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_utils/time.pxd +0 -0
  67. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/_utils/time.py +0 -0
  68. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/const.py +0 -0
  69. {zeroconf-0.149.17 → zeroconf-0.150.2}/src/zeroconf/py.typed +0 -0
  70. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/__init__.py +0 -0
  71. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/benchmarks/__init__.py +0 -0
  72. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/benchmarks/helpers.py +0 -0
  73. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/benchmarks/test_cache.py +0 -0
  74. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/benchmarks/test_cache_bound.py +0 -0
  75. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/benchmarks/test_cache_mark_expire.py +0 -0
  76. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/benchmarks/test_incoming.py +0 -0
  77. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/benchmarks/test_ipaddress.py +0 -0
  78. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/benchmarks/test_listener_dedup.py +0 -0
  79. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/benchmarks/test_mark_seen.py +0 -0
  80. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/benchmarks/test_outgoing.py +0 -0
  81. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/benchmarks/test_send.py +0 -0
  82. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/benchmarks/test_txt_properties.py +0 -0
  83. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/conftest.py +0 -0
  84. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/services/__init__.py +0 -0
  85. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/services/test_browser.py +0 -0
  86. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/services/test_registry.py +0 -0
  87. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/services/test_types.py +0 -0
  88. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_asyncio.py +0 -0
  89. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_cache.py +0 -0
  90. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_circular_imports.py +0 -0
  91. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_core.py +0 -0
  92. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_dns.py +0 -0
  93. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_engine.py +0 -0
  94. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_exceptions.py +0 -0
  95. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_handlers.py +0 -0
  96. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_history.py +0 -0
  97. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_init.py +0 -0
  98. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_listener.py +0 -0
  99. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_logger.py +0 -0
  100. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_protocol.py +0 -0
  101. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_services.py +0 -0
  102. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/test_updates.py +0 -0
  103. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/utils/__init__.py +0 -0
  104. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/utils/test_asyncio.py +0 -0
  105. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/utils/test_ipaddress.py +0 -0
  106. {zeroconf-0.149.17 → zeroconf-0.150.2}/tests/utils/test_name.py +0 -0
@@ -2,6 +2,38 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v0.150.2 (2026-08-28)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Keep `key=` distinct from a valueless `key` in TXT records
10
+ ([#1813](https://github.com/python-zeroconf/python-zeroconf/pull/1813),
11
+ [`899eaaa`](https://github.com/python-zeroconf/python-zeroconf/commit/899eaaa8a11c3d63b8325f73c9968a270e1e72f2))
12
+
13
+
14
+ ## v0.150.1 (2026-08-28)
15
+
16
+ ### Bug Fixes
17
+
18
+ - Build with cython 3.3.0 ([#1820](https://github.com/python-zeroconf/python-zeroconf/pull/1820),
19
+ [`df947b2`](https://github.com/python-zeroconf/python-zeroconf/commit/df947b2f4df94950e272fe09dc9d48ad18411743))
20
+
21
+ ### Documentation
22
+
23
+ - Add an example for re-announcing on interface changes
24
+ ([#1803](https://github.com/python-zeroconf/python-zeroconf/pull/1803),
25
+ [`c690b78`](https://github.com/python-zeroconf/python-zeroconf/commit/c690b78cbd8d54df9847e8873cf5edce80e70b8d))
26
+
27
+
28
+ ## v0.150.0 (2026-06-22)
29
+
30
+ ### Features
31
+
32
+ - Add async_update_interfaces to rescan network interfaces at runtime
33
+ ([#1797](https://github.com/python-zeroconf/python-zeroconf/pull/1797),
34
+ [`471feb4`](https://github.com/python-zeroconf/python-zeroconf/commit/471feb490dc315578ca124411c00745028a1a736))
35
+
36
+
5
37
  ## v0.149.17 (2026-06-21)
6
38
 
7
39
  ### Performance Improvements
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zeroconf
3
- Version: 0.149.17
3
+ Version: 0.150.2
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
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "zeroconf"
7
- version = "0.149.17"
7
+ version = "0.150.2"
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"
@@ -84,10 +84,10 @@ python = "^3.10"
84
84
  ifaddr = ">=0.1.7"
85
85
 
86
86
  [tool.poetry.group.dev.dependencies]
87
- pytest = ">=9.1.0,<10.0"
87
+ pytest = ">=9.1.1,<10.0"
88
88
  pytest-cov = ">=4,<8"
89
89
  pytest-asyncio = ">=1.4.0,<1.5.0"
90
- cython = "^3.2.5"
90
+ cython = "^3.2.9"
91
91
  setuptools = ">=65.6.3,<83.0.0"
92
92
  pytest-timeout = "^2.1.0"
93
93
  pytest-codspeed = ">=5.0.3,<6.0"
@@ -108,6 +108,7 @@ ignore = [
108
108
  "PLR0912", # too many to fix right now
109
109
  "TID252", # skip
110
110
  "PLR0913", # too late to make changes here
111
+ "PLR0917", # too late to make changes here
111
112
  "PLR0911", # would be breaking change
112
113
  "TRY003", # too many to fix
113
114
  "SLF001", # design choice
@@ -19,7 +19,7 @@ install_requires = \
19
19
 
20
20
  setup_kwargs = {
21
21
  'name': 'zeroconf',
22
- 'version': '0.149.17',
22
+ 'version': '0.150.2',
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 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',
25
25
  'author': 'Paul Scott-Murphy',
@@ -88,7 +88,7 @@ from ._utils.time import ( # noqa # import needed for backwards compat
88
88
 
89
89
  __author__ = "Paul Scott-Murphy, William McBrine"
90
90
  __maintainer__ = "Jakub Stasiak <jakub@stasiak.at>"
91
- __version__ = "0.149.17"
91
+ __version__ = "0.150.2"
92
92
  __license__ = "LGPL"
93
93
 
94
94
 
@@ -42,6 +42,7 @@ from .const import _MAX_CACHE_RECORDS, _ONE_SECOND, _TYPE_PTR
42
42
  _UNIQUE_RECORD_TYPES = (DNSAddress, DNSHinfo, DNSPointer, DNSText, DNSService)
43
43
  _UniqueRecordsType = DNSAddress | DNSHinfo | DNSPointer | DNSText | DNSService
44
44
  _DNSRecordCacheType = dict[str, dict[DNSRecord, DNSRecord]]
45
+ _UniqueType = tuple[str, int, int]
45
46
  _DNSRecord = DNSRecord
46
47
  _str = str
47
48
  _float = float
@@ -324,7 +325,7 @@ class DNSCache:
324
325
 
325
326
  def async_mark_unique_records_older_than_1s_to_expire(
326
327
  self,
327
- unique_types: set[tuple[_str, _int, _int]],
328
+ unique_types: set[_UniqueType],
328
329
  answers: Iterable[DNSRecord],
329
330
  now: _float,
330
331
  ) -> None:
@@ -199,6 +199,12 @@ class Zeroconf(QuietLogger):
199
199
 
200
200
  self.unicast = unicast
201
201
  self._use_asyncio = use_asyncio
202
+ # Retained so async_update_interfaces can re-run create_sockets /
203
+ # normalize_interface_choice against the live interface set later.
204
+ # Copy a mutable list so later caller mutation can't change it.
205
+ self._interfaces = list(interfaces) if isinstance(interfaces, list) else interfaces
206
+ self._ip_version = ip_version
207
+ self._apple_p2p = apple_p2p
202
208
  listen_socket, respond_sockets = create_sockets(interfaces, unicast, ip_version, apple_p2p=apple_p2p)
203
209
  log.debug("Listen socket %s, respond sockets %s", listen_socket, respond_sockets)
204
210
 
@@ -216,6 +222,9 @@ class Zeroconf(QuietLogger):
216
222
  self.record_manager = RecordManager(self)
217
223
 
218
224
  self._notify_futures: set[asyncio.Future] = set()
225
+ # Serializes async_update_interfaces so overlapping calls (a bursty
226
+ # adapter-change source) don't diff against a stale sender snapshot.
227
+ self._interface_update_lock = asyncio.Lock()
219
228
  self.loop: asyncio.AbstractEventLoop | None = None
220
229
  self._loop_thread: threading.Thread | None = None
221
230
 
@@ -406,6 +415,91 @@ class Zeroconf(QuietLogger):
406
415
  self.registry.async_update(info)
407
416
  return asyncio.ensure_future(self._async_broadcast_service(info, _REGISTER_TIME, None))
408
417
 
418
+ def update_interfaces(
419
+ self,
420
+ interfaces: InterfacesType | None = None,
421
+ ip_version: IPVersion | None = None,
422
+ apple_p2p: bool | None = None,
423
+ ) -> None:
424
+ """Rescan network interfaces and reconcile the sockets in use.
425
+
426
+ While it is not expected during normal operation,
427
+ this function may raise EventLoopBlocked if the underlying
428
+ call to `async_update_interfaces` cannot be completed. Raises
429
+ RuntimeError if apple_p2p is set on a non-Apple platform.
430
+ """
431
+ assert self.loop is not None
432
+ # Unlike register/update, the re-announce is awaited inline (to log
433
+ # per-service failures), so the budget must cover the full announce
434
+ # window ((_REGISTER_BROADCASTS - 1) * _REGISTER_TIME) plus the reconcile
435
+ # and wait-for-start overhead; double the register budget for headroom.
436
+ run_coro_with_timeout(
437
+ self.async_update_interfaces(interfaces, ip_version, apple_p2p),
438
+ self.loop,
439
+ _REGISTER_TIME * _REGISTER_BROADCASTS * 2,
440
+ )
441
+
442
+ async def async_update_interfaces(
443
+ self,
444
+ interfaces: InterfacesType | None = None,
445
+ ip_version: IPVersion | None = None,
446
+ apple_p2p: bool | None = None,
447
+ ) -> None:
448
+ """Rescan network interfaces and reconcile the sockets in use.
449
+
450
+ Adds sockets for interfaces that appeared, drops sockets for
451
+ interfaces that disappeared, and re-announces existing
452
+ registrations when a new sender appeared. ``interfaces``,
453
+ ``ip_version`` and ``apple_p2p`` each default to the value passed at
454
+ construction; pass a new value to switch it. When the resulting
455
+ interface set is unchanged this is a no-op (no sockets touched,
456
+ nothing re-announced). The listen socket is rebuilt if the new set
457
+ needs a different address family; unicast mode is fixed at
458
+ construction. Concurrent calls are serialized. Bringing up interfaces
459
+ is best-effort: a requested interface that fails to bind, or fails to
460
+ re-join after a rebuild, is logged rather than raised, and likewise a
461
+ registration that fails to re-announce is logged so one failure cannot
462
+ block the others. Raises RuntimeError if apple_p2p is set on a non-Apple
463
+ platform (input validation, matching the constructor).
464
+ """
465
+ # Resolve against the retained config but only commit it after the
466
+ # engine reconcile succeeds, so a failed reconcile leaves the stored
467
+ # config unchanged rather than recording a set that never fully bound
468
+ # (a mid-reconcile failure may still have changed some sockets).
469
+ interfaces = self._interfaces if interfaces is None else interfaces
470
+ ip_version = self._ip_version if ip_version is None else ip_version
471
+ apple_p2p = self._apple_p2p if apple_p2p is None else apple_p2p
472
+ if apple_p2p and sys.platform != "darwin":
473
+ raise RuntimeError("Option `apple_p2p` is not supported on non-Apple platforms.")
474
+ await self.async_wait_for_start()
475
+ # Only the reconcile mutates the sender set, so hold the lock for that
476
+ # alone; the multi-second re-announce runs unlocked so a bursty
477
+ # adapter-change source isn't blocked behind it.
478
+ async with self._interface_update_lock:
479
+ added = await self.engine.async_update_interfaces(interfaces, ip_version, apple_p2p)
480
+ # Copy a mutable list so later caller mutation can't change the
481
+ # retained configuration.
482
+ self._interfaces = list(interfaces) if isinstance(interfaces, list) else interfaces
483
+ self._ip_version = ip_version
484
+ self._apple_p2p = apple_p2p
485
+ if not added:
486
+ return
487
+ # Re-announce every registration; one broadcast failing must not mask
488
+ # the rest, so collect exceptions and log them individually, naming the
489
+ # service so a partial failure is actionable.
490
+ infos = self.registry.async_get_service_infos()
491
+ results = await asyncio.gather(
492
+ *[self._async_broadcast_service(info, _REGISTER_TIME, None) for info in infos],
493
+ return_exceptions=True,
494
+ )
495
+ for info, result in zip(infos, results, strict=True):
496
+ if isinstance(result, Exception):
497
+ log.warning("Error re-announcing %s after interface update: %s", info.name, result)
498
+ elif isinstance(result, BaseException):
499
+ # gather(return_exceptions=True) also captures BaseExceptions
500
+ # such as CancelledError; don't swallow a cancellation/interrupt.
501
+ raise result
502
+
409
503
  async def async_get_service_info(
410
504
  self,
411
505
  type_: str,