Skip to content

Commit c96a997

Browse files
bluetoothbotbdraco
andauthored
feat(core): add use_asyncio kwarg to Zeroconf (#1684)
Co-authored-by: J. Nick Koston <nick@koston.org>
1 parent dfd9e9e commit c96a997

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

src/zeroconf/_core.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def __init__(
154154
unicast: bool = False,
155155
ip_version: IPVersion | None = None,
156156
apple_p2p: bool = False,
157+
use_asyncio: bool | None = None,
157158
) -> None:
158159
"""Creates an instance of the Zeroconf class, establishing
159160
multicast communications, listening and reaping threads.
@@ -169,6 +170,14 @@ def __init__(
169170
:param ip_version: IP versions to support. If `choice` is a list, the default is detected
170171
from it. Otherwise defaults to V4 only for backward compatibility.
171172
:param apple_p2p: use AWDL interface (only macOS)
173+
:param use_asyncio: explicitly control whether to attach to the running
174+
asyncio event loop (``True``) or run an internal thread with its
175+
own loop (``False``). ``None`` (default) keeps the historic
176+
behavior: attach if an event loop is running, otherwise start a
177+
thread. Set to ``False`` when running inside an environment that
178+
already has an event loop (e.g. Jupyter) but you want blocking
179+
semantics. ``True`` raises :class:`RuntimeError` immediately if no
180+
running event loop is found, instead of falling back to the thread.
172181
"""
173182
if ip_version is None:
174183
ip_version = autodetect_ip_version(interfaces)
@@ -178,7 +187,11 @@ def __init__(
178187
if apple_p2p and sys.platform != "darwin":
179188
raise RuntimeError("Option `apple_p2p` is not supported on non-Apple platforms.")
180189

190+
if use_asyncio is True and get_running_loop() is None:
191+
raise RuntimeError("use_asyncio=True requires a running asyncio event loop")
192+
181193
self.unicast = unicast
194+
self._use_asyncio = use_asyncio
182195
listen_socket, respond_sockets = create_sockets(interfaces, unicast, ip_version, apple_p2p=apple_p2p)
183196
log.debug("Listen socket %s, respond sockets %s", listen_socket, respond_sockets)
184197

@@ -216,7 +229,7 @@ def started(self) -> bool:
216229

217230
def start(self) -> None:
218231
"""Start Zeroconf."""
219-
self.loop = get_running_loop()
232+
self.loop = None if self._use_asyncio is False else get_running_loop()
220233
if self.loop:
221234
self.engine.setup(self.loop, None)
222235
return

tests/test_core.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,36 @@ def test_launch_and_close_apple_p2p_on_mac(self):
154154
rv = r.Zeroconf(apple_p2p=True)
155155
rv.close()
156156

157+
def test_use_asyncio_false_forces_thread_when_loop_running(self):
158+
"""use_asyncio=False starts a thread even with a running event loop."""
159+
160+
async def run() -> r.Zeroconf:
161+
return r.Zeroconf(interfaces=["127.0.0.1"], use_asyncio=False)
162+
163+
loop = asyncio.new_event_loop()
164+
zc: r.Zeroconf | None = None
165+
try:
166+
zc = loop.run_until_complete(run())
167+
assert zc._loop_thread is not None
168+
assert zc.loop is not loop
169+
finally:
170+
if zc is not None:
171+
zc.close()
172+
loop.close()
173+
174+
def test_use_asyncio_true_requires_running_loop(self):
175+
"""use_asyncio=True without a running loop raises RuntimeError."""
176+
with pytest.raises(RuntimeError, match="requires a running asyncio event loop"):
177+
r.Zeroconf(interfaces=["127.0.0.1"], use_asyncio=True)
178+
179+
def test_use_asyncio_default_starts_thread_without_loop(self):
180+
"""use_asyncio=None (default) keeps the historic auto-detect behavior."""
181+
zc = r.Zeroconf(interfaces=["127.0.0.1"])
182+
try:
183+
assert zc._loop_thread is not None
184+
finally:
185+
zc.close()
186+
157187
def test_async_updates_from_response(self):
158188
def mock_incoming_msg(
159189
service_state_change: r.ServiceStateChange,

0 commit comments

Comments
 (0)