Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/zeroconf/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def __init__(
unicast: bool = False,
ip_version: IPVersion | None = None,
apple_p2p: bool = False,
use_asyncio: bool | None = None,
) -> None:
"""Creates an instance of the Zeroconf class, establishing
multicast communications, listening and reaping threads.
Expand All @@ -169,6 +170,14 @@ def __init__(
:param ip_version: IP versions to support. If `choice` is a list, the default is detected
from it. Otherwise defaults to V4 only for backward compatibility.
:param apple_p2p: use AWDL interface (only macOS)
:param use_asyncio: explicitly control whether to attach to the running
asyncio event loop (``True``) or run an internal thread with its
own loop (``False``). ``None`` (default) keeps the historic
behavior: attach if an event loop is running, otherwise start a
thread. Set to ``False`` when running inside an environment that
already has an event loop (e.g. Jupyter) but you want blocking
semantics. ``True`` raises :class:`RuntimeError` immediately if no
running event loop is found, instead of falling back to the thread.
"""
if ip_version is None:
ip_version = autodetect_ip_version(interfaces)
Expand All @@ -178,7 +187,11 @@ def __init__(
if apple_p2p and sys.platform != "darwin":
raise RuntimeError("Option `apple_p2p` is not supported on non-Apple platforms.")

if use_asyncio is True and get_running_loop() is None:
raise RuntimeError("use_asyncio=True requires a running asyncio event loop")

self.unicast = unicast
self._use_asyncio = use_asyncio
Comment thread
bdraco marked this conversation as resolved.
listen_socket, respond_sockets = create_sockets(interfaces, unicast, ip_version, apple_p2p=apple_p2p)
log.debug("Listen socket %s, respond sockets %s", listen_socket, respond_sockets)

Expand Down Expand Up @@ -216,7 +229,7 @@ def started(self) -> bool:

def start(self) -> None:
"""Start Zeroconf."""
self.loop = get_running_loop()
self.loop = None if self._use_asyncio is False else get_running_loop()
if self.loop:
self.engine.setup(self.loop, None)
return
Expand Down
30 changes: 30 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,36 @@ def test_launch_and_close_apple_p2p_on_mac(self):
rv = r.Zeroconf(apple_p2p=True)
rv.close()

def test_use_asyncio_false_forces_thread_when_loop_running(self):
"""use_asyncio=False starts a thread even with a running event loop."""

async def run() -> r.Zeroconf:
return r.Zeroconf(interfaces=["127.0.0.1"], use_asyncio=False)

loop = asyncio.new_event_loop()
zc: r.Zeroconf | None = None
try:
zc = loop.run_until_complete(run())
assert zc._loop_thread is not None
assert zc.loop is not loop
finally:
if zc is not None:
zc.close()
loop.close()

def test_use_asyncio_true_requires_running_loop(self):
"""use_asyncio=True without a running loop raises RuntimeError."""
with pytest.raises(RuntimeError, match="requires a running asyncio event loop"):
r.Zeroconf(interfaces=["127.0.0.1"], use_asyncio=True)

def test_use_asyncio_default_starts_thread_without_loop(self):
"""use_asyncio=None (default) keeps the historic auto-detect behavior."""
zc = r.Zeroconf(interfaces=["127.0.0.1"])
try:
assert zc._loop_thread is not None
finally:
zc.close()

def test_async_updates_from_response(self):
def mock_incoming_msg(
service_state_change: r.ServiceStateChange,
Expand Down
Loading