zeroconf 0.149.2__tar.gz → 0.149.4__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 (98) hide show
  1. {zeroconf-0.149.2 → zeroconf-0.149.4}/CHANGELOG.md +40 -0
  2. {zeroconf-0.149.2 → zeroconf-0.149.4}/PKG-INFO +1 -1
  3. {zeroconf-0.149.2 → zeroconf-0.149.4}/pyproject.toml +1 -1
  4. {zeroconf-0.149.2 → zeroconf-0.149.4}/setup.py +1 -1
  5. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/__init__.py +1 -1
  6. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_engine.py +29 -4
  7. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/__init__.py +17 -0
  8. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/services/test_browser.py +34 -13
  9. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/services/test_info.py +35 -49
  10. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/services/test_types.py +8 -8
  11. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_asyncio.py +34 -16
  12. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_core.py +16 -11
  13. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_engine.py +51 -3
  14. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_handlers.py +17 -6
  15. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_init.py +3 -0
  16. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_services.py +5 -3
  17. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_updates.py +1 -1
  18. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/utils/test_asyncio.py +1 -0
  19. {zeroconf-0.149.2 → zeroconf-0.149.4}/COPYING +0 -0
  20. {zeroconf-0.149.2 → zeroconf-0.149.4}/README.rst +0 -0
  21. {zeroconf-0.149.2 → zeroconf-0.149.4}/build_ext.py +0 -0
  22. {zeroconf-0.149.2 → zeroconf-0.149.4}/docs/Makefile +0 -0
  23. {zeroconf-0.149.2 → zeroconf-0.149.4}/docs/_ext/zeroconfautodocfix.py +0 -0
  24. {zeroconf-0.149.2 → zeroconf-0.149.4}/docs/api.rst +0 -0
  25. {zeroconf-0.149.2 → zeroconf-0.149.4}/docs/conf.py +0 -0
  26. {zeroconf-0.149.2 → zeroconf-0.149.4}/docs/index.rst +0 -0
  27. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_cache.pxd +0 -0
  28. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_cache.py +0 -0
  29. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_core.py +0 -0
  30. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_dns.pxd +0 -0
  31. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_dns.py +0 -0
  32. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_exceptions.py +0 -0
  33. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/__init__.py +0 -0
  34. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/answers.pxd +0 -0
  35. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/answers.py +0 -0
  36. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/multicast_outgoing_queue.pxd +0 -0
  37. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/multicast_outgoing_queue.py +0 -0
  38. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/query_handler.pxd +0 -0
  39. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/query_handler.py +0 -0
  40. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/record_manager.pxd +0 -0
  41. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_handlers/record_manager.py +0 -0
  42. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_history.pxd +0 -0
  43. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_history.py +0 -0
  44. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_listener.pxd +0 -0
  45. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_listener.py +0 -0
  46. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_logger.py +0 -0
  47. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_protocol/__init__.py +0 -0
  48. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_protocol/incoming.pxd +0 -0
  49. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_protocol/incoming.py +0 -0
  50. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_protocol/outgoing.pxd +0 -0
  51. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_protocol/outgoing.py +0 -0
  52. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_record_update.pxd +0 -0
  53. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_record_update.py +0 -0
  54. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/__init__.pxd +0 -0
  55. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/__init__.py +0 -0
  56. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/browser.pxd +0 -0
  57. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/browser.py +0 -0
  58. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/info.pxd +0 -0
  59. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/info.py +0 -0
  60. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/registry.pxd +0 -0
  61. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/registry.py +0 -0
  62. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_services/types.py +0 -0
  63. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_transport.py +0 -0
  64. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_updates.pxd +0 -0
  65. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_updates.py +0 -0
  66. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/__init__.py +0 -0
  67. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/asyncio.py +0 -0
  68. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/ipaddress.pxd +0 -0
  69. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/ipaddress.py +0 -0
  70. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/name.py +0 -0
  71. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/net.py +0 -0
  72. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/time.pxd +0 -0
  73. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/_utils/time.py +0 -0
  74. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/asyncio.py +0 -0
  75. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/const.py +0 -0
  76. {zeroconf-0.149.2 → zeroconf-0.149.4}/src/zeroconf/py.typed +0 -0
  77. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/__init__.py +0 -0
  78. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/helpers.py +0 -0
  79. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/test_cache.py +0 -0
  80. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/test_incoming.py +0 -0
  81. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/test_outgoing.py +0 -0
  82. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/test_send.py +0 -0
  83. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/benchmarks/test_txt_properties.py +0 -0
  84. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/conftest.py +0 -0
  85. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/services/__init__.py +0 -0
  86. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/services/test_registry.py +0 -0
  87. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_cache.py +0 -0
  88. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_circular_imports.py +0 -0
  89. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_dns.py +0 -0
  90. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_exceptions.py +0 -0
  91. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_history.py +0 -0
  92. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_listener.py +0 -0
  93. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_logger.py +0 -0
  94. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/test_protocol.py +0 -0
  95. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/utils/__init__.py +0 -0
  96. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/utils/test_ipaddress.py +0 -0
  97. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/utils/test_name.py +0 -0
  98. {zeroconf-0.149.2 → zeroconf-0.149.4}/tests/utils/test_net.py +0 -0
@@ -2,6 +2,46 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v0.149.4 (2026-05-17)
6
+
7
+ ### Bug Fixes
8
+
9
+ - **core**: Release sockets when close runs before engine setup completes
10
+ ([#1706](https://github.com/python-zeroconf/python-zeroconf/pull/1706),
11
+ [`0deb56b`](https://github.com/python-zeroconf/python-zeroconf/commit/0deb56b78fe6cd701a43ce34dccd4b69d6dd6d36))
12
+
13
+ ### Testing
14
+
15
+ - Drop pending multicast responses before TOCTOU assertion
16
+ ([#1701](https://github.com/python-zeroconf/python-zeroconf/pull/1701),
17
+ [`d2058d9`](https://github.com/python-zeroconf/python-zeroconf/commit/d2058d95882c70fb2eb786d373a630314e656c15))
18
+
19
+ - Speed up slow loopback tests (closes #1697)
20
+ ([#1699](https://github.com/python-zeroconf/python-zeroconf/pull/1699),
21
+ [`dd341a3`](https://github.com/python-zeroconf/python-zeroconf/commit/dd341a378e904cbf9b9a09e5c2ac8ff7c9944cd0))
22
+
23
+ - Speed up slow loopback tests (closes #1700)
24
+ ([#1703](https://github.com/python-zeroconf/python-zeroconf/pull/1703),
25
+ [`d03ea36`](https://github.com/python-zeroconf/python-zeroconf/commit/d03ea364dea5866e0301ff11f6b78714ecf991e8))
26
+
27
+ - Speed up test_async_wait_unblocks_on_update
28
+ ([#1702](https://github.com/python-zeroconf/python-zeroconf/pull/1702),
29
+ [`653c385`](https://github.com/python-zeroconf/python-zeroconf/commit/653c38559c468672cf907d808d432dec0fb06968))
30
+
31
+ - Widen scheduling buffer in flaky get_info suppression test
32
+ ([#1698](https://github.com/python-zeroconf/python-zeroconf/pull/1698),
33
+ [`9b4db62`](https://github.com/python-zeroconf/python-zeroconf/commit/9b4db625e121c2d590ed8fe2f4d187d5e3bb9a73))
34
+
35
+
36
+ ## v0.149.3 (2026-05-17)
37
+
38
+ ### Bug Fixes
39
+
40
+ - **ci**: Drop x86_64 mac wheels and clean up obsolete CIBW_SKIP entries
41
+ ([#1694](https://github.com/python-zeroconf/python-zeroconf/pull/1694),
42
+ [`104c5d6`](https://github.com/python-zeroconf/python-zeroconf/commit/104c5d6674896612aa83a89fd17b90de5f38a508))
43
+
44
+
5
45
  ## v0.149.2 (2026-05-17)
6
46
 
7
47
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zeroconf
3
- Version: 0.149.2
3
+ Version: 0.149.4
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.2"
7
+ version = "0.149.4"
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"
@@ -19,7 +19,7 @@ install_requires = \
19
19
 
20
20
  setup_kwargs = {
21
21
  'name': 'zeroconf',
22
- 'version': '0.149.2',
22
+ 'version': '0.149.4',
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\nLGPL, see COPYING file for details.\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.2"
91
+ __version__ = "0.149.4"
92
92
  __license__ = "LGPL"
93
93
 
94
94
 
@@ -114,10 +114,17 @@ class AsyncEngine:
114
114
  lambda: AsyncListener(self.zc), # type: ignore[arg-type, return-value]
115
115
  sock=s,
116
116
  )
117
+ # Register the wrapped transport before releasing the engine's
118
+ # handle so a concurrent shutdown always sees ``s`` in exactly
119
+ # one place; do not add an ``await`` between these two steps.
117
120
  self.protocols.append(cast(AsyncListener, protocol))
118
121
  self.readers.append(make_wrapped_transport(cast(asyncio.DatagramTransport, transport)))
119
122
  if s in sender_sockets:
120
123
  self.senders.append(make_wrapped_transport(cast(asyncio.DatagramTransport, transport)))
124
+ if s is self._listen_socket:
125
+ self._listen_socket = None
126
+ if s in self._respond_sockets:
127
+ self._respond_sockets.remove(s)
121
128
 
122
129
  def _async_cache_cleanup(self) -> None:
123
130
  """Periodic cache cleanup."""
@@ -139,19 +146,37 @@ class AsyncEngine:
139
146
  async def _async_close(self) -> None:
140
147
  """Cancel and wait for the cleanup task to finish."""
141
148
  assert self._setup_task is not None
142
- await self._setup_task
149
+ # Swallow CancelledError only if the setup task itself was
150
+ # cancelled (close-before-start); outer-task cancellation must
151
+ # propagate.
152
+ try:
153
+ await self._setup_task
154
+ except asyncio.CancelledError:
155
+ if not self._setup_task.cancelled():
156
+ raise
143
157
  self._async_shutdown()
144
158
  await asyncio.sleep(0) # flush out any call soons
145
- assert self._cleanup_timer is not None
146
- self._cleanup_timer.cancel()
159
+ if self._cleanup_timer is not None:
160
+ self._cleanup_timer.cancel()
147
161
 
148
162
  def _async_shutdown(self) -> None:
149
- """Shutdown transports and sockets."""
163
+ """Shutdown transports and sockets; safe to call repeatedly."""
150
164
  assert self.running_future is not None
151
165
  assert self.loop is not None
152
166
  self.running_future = self.loop.create_future()
167
+ # Cancel pending setup so it can't wrap fresh transports after
168
+ # shutdown has started.
169
+ if self._setup_task is not None and not self._setup_task.done():
170
+ self._setup_task.cancel()
153
171
  for wrapped_transport in itertools.chain(self.senders, self.readers):
154
172
  wrapped_transport.transport.close()
173
+ # Anything still here was never adopted by a transport.
174
+ if self._listen_socket is not None:
175
+ self._listen_socket.close()
176
+ self._listen_socket = None
177
+ for s in self._respond_sockets:
178
+ s.close()
179
+ self._respond_sockets = []
155
180
 
156
181
  def close(self) -> None:
157
182
  """Close from sync context.
@@ -91,6 +91,23 @@ def _clear_cache(zc: Zeroconf) -> None:
91
91
  zc.question_history.clear()
92
92
 
93
93
 
94
+ def _backdate_cache(zc: Zeroconf, ms: int = 1100) -> None:
95
+ """Backdate every cached record's `created` time by `ms` milliseconds.
96
+
97
+ rfc6762#section-10.2 keys off "received more than one second ago", so
98
+ backdating is equivalent to sleeping `ms` in real time without the
99
+ wall-clock wait.
100
+
101
+ Iterate `store.values()`, not the dict directly — when a record is
102
+ re-added with an equal hash, the key stays the original object while
103
+ the value is replaced with the latest; mutating the key would update
104
+ stale objects no one reads.
105
+ """
106
+ for store in zc.cache.cache.values():
107
+ for record in store.values():
108
+ record.created -= ms
109
+
110
+
94
111
  def time_changed_millis(millis: float | None = None) -> None:
95
112
  """Call all scheduled events for a time."""
96
113
  loop = asyncio.get_running_loop()
@@ -331,7 +331,9 @@ class TestServiceBrowser(unittest.TestCase):
331
331
  service_updated_event.clear()
332
332
  service_text = b"path=/~matt2/"
333
333
  _inject_response(zeroconf, mock_record_update_incoming_msg(r.ServiceStateChange.Updated))
334
- service_updated_event.wait(wait_time)
334
+ # Negative assertion: a duplicate update must NOT fire the listener. The wait
335
+ # always times out, so keep the budget short rather than reusing wait_time.
336
+ service_updated_event.wait(0.3)
335
337
  assert service_added_count == 1
336
338
  assert service_updated_count == 2
337
339
  assert service_removed_count == 0
@@ -554,7 +556,7 @@ def test_first_query_delay():
554
556
 
555
557
 
556
558
  @pytest.mark.asyncio
557
- async def test_asking_default_is_asking_qm_questions_after_the_first_qu():
559
+ async def test_asking_default_is_asking_qm_questions_after_the_first_qu(quick_timing: None) -> None:
558
560
  """Verify the service browser's first questions are QU and refresh queries are QM."""
559
561
  service_added = asyncio.Event()
560
562
  service_removed = asyncio.Event()
@@ -656,7 +658,7 @@ async def test_asking_default_is_asking_qm_questions_after_the_first_qu():
656
658
 
657
659
 
658
660
  @pytest.mark.asyncio
659
- async def test_ttl_refresh_cancelled_rescue_query():
661
+ async def test_ttl_refresh_cancelled_rescue_query(quick_timing: None) -> None:
660
662
  """Verify seeing a name again cancels the rescue query."""
661
663
  service_added = asyncio.Event()
662
664
  service_removed = asyncio.Event()
@@ -844,7 +846,7 @@ async def test_asking_qu_questions():
844
846
  await aiozc.async_close()
845
847
 
846
848
 
847
- def test_legacy_record_update_listener():
849
+ def test_legacy_record_update_listener(quick_timing: None) -> None:
848
850
  """Test a RecordUpdateListener that does not implement update_records."""
849
851
 
850
852
  # instantiate a zeroconf instance
@@ -1031,7 +1033,7 @@ def test_service_browser_listeners_update_service():
1031
1033
 
1032
1034
 
1033
1035
  def test_service_browser_listeners_no_update_service():
1034
- """Test that the ServiceBrowser ServiceListener that does not implement update_service."""
1036
+ """A listener that ignores update events records only add/remove callbacks."""
1035
1037
 
1036
1038
  # instantiate a zeroconf instance
1037
1039
  zc = Zeroconf(interfaces=["127.0.0.1"])
@@ -1049,6 +1051,9 @@ def test_service_browser_listeners_no_update_service():
1049
1051
  if name == registration_name:
1050
1052
  callbacks.append(("remove", type_, name))
1051
1053
 
1054
+ def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def]
1055
+ pass
1056
+
1052
1057
  listener = MyServiceListener()
1053
1058
 
1054
1059
  browser = r.ServiceBrowser(zc, type_, None, listener)
@@ -1497,10 +1502,15 @@ def test_service_browser_expire_callbacks():
1497
1502
  # Force the ttl to be 1 second
1498
1503
  now = current_time_millis()
1499
1504
  for cache_record in list(zc.cache.cache.values()):
1500
- for record in cache_record:
1505
+ for record in cache_record.values():
1501
1506
  zc.cache._async_set_created_ttl(record, now, 1)
1502
1507
 
1503
- time.sleep(0.3)
1508
+ # Wait for the add callback to fire from the original inject_response.
1509
+ for _ in range(30):
1510
+ time.sleep(0.01)
1511
+ if len(callbacks) == 1:
1512
+ break
1513
+
1504
1514
  info.port = 400
1505
1515
  info._dns_service_cache = None # we are mutating the record so clear the cache
1506
1516
 
@@ -1509,8 +1519,8 @@ def test_service_browser_expire_callbacks():
1509
1519
  mock_incoming_msg([info.dns_service()]),
1510
1520
  )
1511
1521
 
1512
- for _ in range(10):
1513
- time.sleep(0.05)
1522
+ for _ in range(30):
1523
+ time.sleep(0.01)
1514
1524
  if len(callbacks) == 2:
1515
1525
  break
1516
1526
 
@@ -1519,8 +1529,19 @@ def test_service_browser_expire_callbacks():
1519
1529
  ("update", type_, registration_name),
1520
1530
  ]
1521
1531
 
1522
- for _ in range(25):
1523
- time.sleep(0.05)
1532
+ # Re-add every cached record with `created` in the past so the
1533
+ # next reaper tick (0.01s) expires them and fires the remove
1534
+ # callback, instead of waiting the full TTL in real time.
1535
+ # Going through `_async_set_created_ttl` updates the expiration
1536
+ # heap; mutating `record.created` directly would leave the heap
1537
+ # entry pointing at the original `when` so the reaper never wakes.
1538
+ past = current_time_millis() - 2000
1539
+ for cache_record in list(zc.cache.cache.values()):
1540
+ for record in list(cache_record.values()):
1541
+ zc.cache._async_set_created_ttl(record, past, 1)
1542
+
1543
+ for _ in range(30):
1544
+ time.sleep(0.01)
1524
1545
  if len(callbacks) == 3:
1525
1546
  break
1526
1547
 
@@ -1565,7 +1586,7 @@ def test_scheduled_ptr_query_dunder_methods():
1565
1586
 
1566
1587
 
1567
1588
  @pytest.mark.asyncio
1568
- async def test_close_zeroconf_without_browser_before_start_up_queries():
1589
+ async def test_close_zeroconf_without_browser_before_start_up_queries(quick_timing: None) -> None:
1569
1590
  """Test that we stop sending startup queries if zeroconf is closed out from under the browser."""
1570
1591
  service_added = asyncio.Event()
1571
1592
  type_ = "_http._tcp.local."
@@ -1632,7 +1653,7 @@ async def test_close_zeroconf_without_browser_before_start_up_queries():
1632
1653
 
1633
1654
 
1634
1655
  @pytest.mark.asyncio
1635
- async def test_close_zeroconf_without_browser_after_start_up_queries():
1656
+ async def test_close_zeroconf_without_browser_after_start_up_queries(quick_timing: None) -> None:
1636
1657
  """Test that we stop sending rescue queries if zeroconf is closed out from under the browser."""
1637
1658
  service_added = asyncio.Event()
1638
1659
 
@@ -436,12 +436,29 @@ class TestServiceInfo(unittest.TestCase):
436
436
  service_info_event.set()
437
437
 
438
438
  try:
439
+ # Seed TXT/A/AAAA with a far-future `than` before the
440
+ # helper thread starts. The first (QU) query bypasses
441
+ # suppression so phase 1 still observes 4 questions; the
442
+ # second (QM) query fires ~220-320ms after the first, too
443
+ # tight a window to seed reliably from the test thread on
444
+ # slow runners. async_expire only removes entries where
445
+ # now - than > _DUPLICATE_QUESTION_INTERVAL, so future-
446
+ # dated entries persist for the duration of the test.
447
+ seed_history_questions = (
448
+ r.DNSQuestion(service_name, const._TYPE_A, const._CLASS_IN),
449
+ r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN),
450
+ r.DNSQuestion(service_name, const._TYPE_TXT, const._CLASS_IN),
451
+ )
452
+ far_future = r.current_time_millis() + 60_000
453
+ for question in seed_history_questions:
454
+ zc.question_history.add_question_at_time(question, far_future, set())
455
+
439
456
  helper_thread = threading.Thread(
440
457
  target=get_service_info_helper,
441
458
  args=(zc, service_type, service_name),
442
459
  )
443
460
  helper_thread.start()
444
- wait_time = (const._LISTENER_TIME + info._AVOID_SYNC_DELAY_RANDOM_INTERVAL[1] + 5) / 1000
461
+ wait_time = (const._LISTENER_TIME + info._AVOID_SYNC_DELAY_RANDOM_INTERVAL[1] + 500) / 1000
445
462
 
446
463
  # Expect query for SRV, TXT, A, AAAA
447
464
  send_event.wait(wait_time)
@@ -457,64 +474,29 @@ class TestServiceInfo(unittest.TestCase):
457
474
  # by the question history
458
475
  last_sent = None
459
476
  send_event.clear()
460
- for _ in range(3):
461
- send_event.wait(
462
- wait_time * 0.25
463
- ) # Wait long enough to be inside the question history window
464
- now = r.current_time_millis()
465
- zc.question_history.add_question_at_time(
466
- r.DNSQuestion(service_name, const._TYPE_A, const._CLASS_IN),
467
- now,
468
- set(),
469
- )
470
- zc.question_history.add_question_at_time(
471
- r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN),
472
- now,
473
- set(),
474
- )
475
- zc.question_history.add_question_at_time(
476
- r.DNSQuestion(service_name, const._TYPE_TXT, const._CLASS_IN),
477
- now,
478
- set(),
479
- )
480
- send_event.wait(wait_time * 0.25)
477
+ send_event.wait(wait_time)
481
478
  assert last_sent is not None
482
479
  assert len(last_sent.questions) == 1 # type: ignore[unreachable]
483
480
  assert r.DNSQuestion(service_name, const._TYPE_SRV, const._CLASS_IN) in last_sent.questions
484
481
  assert service_info is None
485
482
 
483
+ # Future-date SRV too: the SRV entry added by the previous
484
+ # QM query has `than = now`, so it expires after
485
+ # _DUPLICATE_QUESTION_INTERVAL — before the next scheduled
486
+ # QM query (~1s + jitter later).
487
+ zc.question_history.add_question_at_time(
488
+ r.DNSQuestion(service_name, const._TYPE_SRV, const._CLASS_IN),
489
+ r.current_time_millis() + 60_000,
490
+ set(),
491
+ )
492
+
486
493
  wait_time = (
487
- const._DUPLICATE_QUESTION_INTERVAL + info._AVOID_SYNC_DELAY_RANDOM_INTERVAL[1] + 5
494
+ const._DUPLICATE_QUESTION_INTERVAL + info._AVOID_SYNC_DELAY_RANDOM_INTERVAL[1] + 500
488
495
  ) / 1000
489
496
  # Expect no queries as all are suppressed by the question history
490
497
  last_sent = None
491
498
  send_event.clear()
492
- for _ in range(3):
493
- send_event.wait(
494
- wait_time * 0.25
495
- ) # Wait long enough to be inside the question history window
496
- now = r.current_time_millis()
497
- zc.question_history.add_question_at_time(
498
- r.DNSQuestion(service_name, const._TYPE_A, const._CLASS_IN),
499
- now,
500
- set(),
501
- )
502
- zc.question_history.add_question_at_time(
503
- r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN),
504
- now,
505
- set(),
506
- )
507
- zc.question_history.add_question_at_time(
508
- r.DNSQuestion(service_name, const._TYPE_TXT, const._CLASS_IN),
509
- now,
510
- set(),
511
- )
512
- zc.question_history.add_question_at_time(
513
- r.DNSQuestion(service_name, const._TYPE_SRV, const._CLASS_IN),
514
- now,
515
- set(),
516
- )
517
- send_event.wait(wait_time * 0.25)
499
+ send_event.wait(wait_time)
518
500
  # All questions are suppressed so no query should be sent
519
501
  assert last_sent is None
520
502
  assert service_info is None
@@ -1547,6 +1529,7 @@ async def test_bad_ip_addresses_ignored_in_cache():
1547
1529
  info = ServiceInfo(type_, registration_name)
1548
1530
  info.load_from_cache(aiozc.zeroconf)
1549
1531
  assert info.addresses_by_version(IPVersion.V4Only) == [b"\x7f\x00\x00\x01"]
1532
+ await aiozc.async_close()
1550
1533
 
1551
1534
 
1552
1535
  @pytest.mark.asyncio
@@ -1822,6 +1805,7 @@ async def test_address_resolver():
1822
1805
  aiozc.zeroconf.async_send(outgoing)
1823
1806
  assert await resolve_task
1824
1807
  assert resolver.addresses == [b"\x7f\x00\x00\x01"]
1808
+ await aiozc.async_close()
1825
1809
 
1826
1810
 
1827
1811
  @pytest.mark.asyncio
@@ -1846,6 +1830,7 @@ async def test_address_resolver_ipv4():
1846
1830
  aiozc.zeroconf.async_send(outgoing)
1847
1831
  assert await resolve_task
1848
1832
  assert resolver.addresses == [b"\x7f\x00\x00\x01"]
1833
+ await aiozc.async_close()
1849
1834
 
1850
1835
 
1851
1836
  @pytest.mark.asyncio
@@ -1872,6 +1857,7 @@ async def test_address_resolver_ipv6():
1872
1857
  aiozc.zeroconf.async_send(outgoing)
1873
1858
  assert await resolve_task
1874
1859
  assert resolver.ip_addresses_by_version(IPVersion.All) == [ip_address("fe80::52e:c2f2:bc5f:e9c6")]
1860
+ await aiozc.async_close()
1875
1861
 
1876
1862
 
1877
1863
  @pytest.mark.asyncio
@@ -47,10 +47,10 @@ def test_integration_with_listener(disable_duplicate_packet_suppression):
47
47
  )
48
48
  zeroconf_registrar.registry.async_add(info)
49
49
  try:
50
- service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=2)
50
+ service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=0.5)
51
51
  assert type_ in service_types
52
52
  _clear_cache(zeroconf_registrar)
53
- service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=2)
53
+ service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=0.5)
54
54
  assert type_ in service_types
55
55
 
56
56
  finally:
@@ -79,10 +79,10 @@ def test_integration_with_listener_v6_records(disable_duplicate_packet_suppressi
79
79
  )
80
80
  zeroconf_registrar.registry.async_add(info)
81
81
  try:
82
- service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=2)
82
+ service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=0.5)
83
83
  assert type_ in service_types
84
84
  _clear_cache(zeroconf_registrar)
85
- service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=2)
85
+ service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=0.5)
86
86
  assert type_ in service_types
87
87
 
88
88
  finally:
@@ -115,10 +115,10 @@ def test_integration_with_listener_ipv6(disable_duplicate_packet_suppression):
115
115
  )
116
116
  zeroconf_registrar.registry.async_add(info)
117
117
  try:
118
- service_types = ZeroconfServiceTypes.find(ip_version=r.IPVersion.V6Only, timeout=2)
118
+ service_types = ZeroconfServiceTypes.find(ip_version=r.IPVersion.V6Only, timeout=0.5)
119
119
  assert type_ in service_types
120
120
  _clear_cache(zeroconf_registrar)
121
- service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=2)
121
+ service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=0.5)
122
122
  assert type_ in service_types
123
123
 
124
124
  finally:
@@ -147,10 +147,10 @@ def test_integration_with_subtype_and_listener(disable_duplicate_packet_suppress
147
147
  )
148
148
  zeroconf_registrar.registry.async_add(info)
149
149
  try:
150
- service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=2)
150
+ service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=0.5)
151
151
  assert discovery_type in service_types
152
152
  _clear_cache(zeroconf_registrar)
153
- service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=2)
153
+ service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=0.5)
154
154
  assert discovery_type in service_types
155
155
 
156
156
  finally:
@@ -126,7 +126,7 @@ async def test_sync_within_event_loop_executor() -> None:
126
126
 
127
127
 
128
128
  @pytest.mark.asyncio
129
- async def test_async_service_registration() -> None:
129
+ async def test_async_service_registration(quick_timing: None) -> None:
130
130
  """Test registering services broadcasts the registration by default."""
131
131
  aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
132
132
  type_ = "_test1-srvc-type._tcp.local."
@@ -193,7 +193,7 @@ async def test_async_service_registration() -> None:
193
193
 
194
194
 
195
195
  @pytest.mark.asyncio
196
- async def test_async_service_registration_with_server_missing() -> None:
196
+ async def test_async_service_registration_with_server_missing(quick_timing: None) -> None:
197
197
  """Test registering a service with the server not specified.
198
198
 
199
199
  For backwards compatibility, the server should be set to the
@@ -394,7 +394,7 @@ async def test_async_service_registration_same_server_same_ports(quick_timing: N
394
394
 
395
395
 
396
396
  @pytest.mark.asyncio
397
- async def test_async_service_registration_name_conflict() -> None:
397
+ async def test_async_service_registration_name_conflict(quick_timing: None) -> None:
398
398
  """Test registering services throws on name conflict."""
399
399
  aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
400
400
  type_ = "_test-srvc2-type._tcp.local."
@@ -500,10 +500,11 @@ async def test_async_service_registration_name_strict_check(quick_timing: None)
500
500
 
501
501
  await aiozc.async_unregister_service(info)
502
502
  await aiozc.async_close()
503
+ zc.close()
503
504
 
504
505
 
505
506
  @pytest.mark.asyncio
506
- async def test_async_tasks() -> None:
507
+ async def test_async_tasks(quick_timing: None) -> None:
507
508
  """Test awaiting broadcast tasks"""
508
509
 
509
510
  aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
@@ -569,7 +570,7 @@ async def test_async_tasks() -> None:
569
570
 
570
571
 
571
572
  @pytest.mark.asyncio
572
- async def test_async_wait_unblocks_on_update() -> None:
573
+ async def test_async_wait_unblocks_on_update(quick_timing: None) -> None:
573
574
  """Test async_wait will unblock on update."""
574
575
 
575
576
  aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
@@ -605,7 +606,7 @@ async def test_async_wait_unblocks_on_update() -> None:
605
606
 
606
607
 
607
608
  @pytest.mark.asyncio
608
- async def test_service_info_async_request() -> None:
609
+ async def test_service_info_async_request(quick_timing: None) -> None:
609
610
  """Test registering services broadcasts and query with AsyncServceInfo.async_request."""
610
611
  if not has_working_ipv6() or os.environ.get("SKIP_IPV6"):
611
612
  pytest.skip("Requires IPv6")
@@ -695,26 +696,41 @@ async def test_service_info_async_request() -> None:
695
696
  assert aiosinfos[1] is not None
696
697
  assert aiosinfos[1].addresses == [socket.inet_aton("10.0.1.5")]
697
698
 
699
+ # Drop pending multicast responses queued under the original
700
+ # `info` / `info2` registrations. Their snapshots predate the
701
+ # `async_update_service` swap, so if they flushed after
702
+ # `_clear_cache` below they would poison `aiosinfo.server`
703
+ # with `ash-1.local.` and the `_is_complete=False` loop would
704
+ # then keep asking for an A record nobody answers. The pending
705
+ # `loop.call_at` for each queue fires harmlessly on the empty
706
+ # deque.
707
+ aiozc.zeroconf.out_queue.queue.clear()
708
+ aiozc.zeroconf.out_delay_queue.queue.clear()
698
709
  aiosinfo = AsyncServiceInfo(type_, registration_name)
699
710
  _clear_cache(aiozc.zeroconf)
700
711
  # Generating the race condition is almost impossible
701
- # without patching since its a TOCTOU race
712
+ # without patching since its a TOCTOU race. 1500ms covers
713
+ # the initial _LISTENER_TIME + random delay (200-320ms) and
714
+ # leaves plenty of margin for the loopback response to land
715
+ # before the loop times out.
702
716
  with patch("zeroconf.asyncio.AsyncServiceInfo._is_complete", False):
703
- await aiosinfo.async_request(aiozc.zeroconf, 3000)
717
+ await aiosinfo.async_request(aiozc.zeroconf, 1500)
704
718
  assert aiosinfo is not None
705
719
  assert aiosinfo.addresses == [socket.inet_aton("10.0.1.3")]
706
720
 
707
721
  task = await aiozc.async_unregister_service(new_info)
708
722
  await task
709
723
 
710
- aiosinfo = await aiozc.async_get_service_info(type_, registration_name)
724
+ # Cap timeout: the service is gone, so this is expected to return None;
725
+ # waiting the default 3000ms is pure overhead.
726
+ aiosinfo = await aiozc.async_get_service_info(type_, registration_name, timeout=200)
711
727
  assert aiosinfo is None
712
728
 
713
729
  await aiozc.async_close()
714
730
 
715
731
 
716
732
  @pytest.mark.asyncio
717
- async def test_async_service_browser() -> None:
733
+ async def test_async_service_browser(quick_timing: None) -> None:
718
734
  """Test AsyncServiceBrowser."""
719
735
  aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
720
736
  type_ = "_test9-srvc-type._tcp.local."
@@ -774,7 +790,7 @@ async def test_async_service_browser() -> None:
774
790
 
775
791
 
776
792
  @pytest.mark.asyncio
777
- async def test_async_context_manager() -> None:
793
+ async def test_async_context_manager(quick_timing: None) -> None:
778
794
  """Test using an async context manager."""
779
795
  type_ = "_test10-sr-type._tcp.local."
780
796
  name = "xxxyyy"
@@ -906,10 +922,10 @@ async def test_async_zeroconf_service_types(quick_timing: None) -> None:
906
922
  await asyncio.sleep(0.2)
907
923
  _clear_cache(zeroconf_registrar.zeroconf)
908
924
  try:
909
- service_types = await AsyncZeroconfServiceTypes.async_find(interfaces=["127.0.0.1"], timeout=2)
925
+ service_types = await AsyncZeroconfServiceTypes.async_find(interfaces=["127.0.0.1"], timeout=0.5)
910
926
  assert type_ in service_types
911
927
  _clear_cache(zeroconf_registrar.zeroconf)
912
- service_types = await AsyncZeroconfServiceTypes.async_find(aiozc=zeroconf_registrar, timeout=2)
928
+ service_types = await AsyncZeroconfServiceTypes.async_find(aiozc=zeroconf_registrar, timeout=0.5)
913
929
  assert type_ in service_types
914
930
 
915
931
  finally:
@@ -974,7 +990,7 @@ async def test_service_browser_instantiation_generates_add_events_from_cache():
974
990
 
975
991
 
976
992
  @pytest.mark.asyncio
977
- async def test_integration():
993
+ async def test_integration(quick_timing: None) -> None:
978
994
  service_added = asyncio.Event()
979
995
  service_removed = asyncio.Event()
980
996
  unexpected_ttl = asyncio.Event()
@@ -1166,9 +1182,11 @@ async def test_info_asking_default_is_asking_qm_questions_after_the_first_qu():
1166
1182
  # patch the zeroconf send
1167
1183
  with patch.object(zeroconf_info, "async_send", send):
1168
1184
  aiosinfo = AsyncServiceInfo(type_, registration_name)
1169
- # Patch _is_complete so we send multiple times
1185
+ # Patch _is_complete so we send multiple times. 500ms covers
1186
+ # the QU query at 0ms plus the QM query at ~_LISTENER_TIME +
1187
+ # max random delay (~320ms).
1170
1188
  with patch("zeroconf.asyncio.AsyncServiceInfo._is_complete", False):
1171
- await aiosinfo.async_request(aiozc.zeroconf, 1200)
1189
+ await aiosinfo.async_request(aiozc.zeroconf, 500)
1172
1190
  try:
1173
1191
  assert first_outgoing.questions[0].unicast is True # type: ignore[union-attr]
1174
1192
  assert second_outgoing.questions[0].unicast is False # type: ignore[attr-defined]