Skip to content

Commit bd8d846

Browse files
feat: context managers in ServiceBrowser and AsyncServiceBrowser (#1233)
Co-authored-by: J. Nick Koston <nick@koston.org>
1 parent 041549c commit bd8d846

4 files changed

Lines changed: 84 additions & 1 deletion

File tree

src/zeroconf/_services/browser.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import threading
2727
import warnings
2828
from abc import abstractmethod
29+
from types import TracebackType # noqa # used in type hints
2930
from typing import (
3031
TYPE_CHECKING,
3132
Callable,
@@ -35,6 +36,7 @@
3536
Optional,
3637
Set,
3738
Tuple,
39+
Type,
3840
Union,
3941
cast,
4042
)
@@ -576,3 +578,15 @@ def async_update_records_complete(self) -> None:
576578
for pending in self._pending_handlers.items():
577579
self.queue.put(pending)
578580
self._pending_handlers.clear()
581+
582+
def __enter__(self) -> 'ServiceBrowser':
583+
return self
584+
585+
def __exit__( # pylint: disable=useless-return
586+
self,
587+
exc_type: Optional[Type[BaseException]],
588+
exc_val: Optional[BaseException],
589+
exc_tb: Optional[TracebackType],
590+
) -> Optional[bool]:
591+
self.cancel()
592+
return None

src/zeroconf/asyncio.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,18 @@ def async_update_records_complete(self) -> None:
9393
self._fire_service_state_changed_event(pending)
9494
self._pending_handlers.clear()
9595

96+
async def __aenter__(self) -> 'AsyncServiceBrowser':
97+
return self
98+
99+
async def __aexit__(
100+
self,
101+
exc_type: Optional[Type[BaseException]],
102+
exc_val: Optional[BaseException],
103+
exc_tb: Optional[TracebackType],
104+
) -> Optional[bool]:
105+
await self.async_cancel()
106+
return None
107+
96108

97109
class AsyncZeroconfServiceTypes(ZeroconfServiceTypes):
98110
"""An async version of ZeroconfServiceTypes."""

tests/services/test_browser.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33

44
""" Unit tests for zeroconf._services.browser. """
55

6+
import asyncio
67
import logging
78
import os
89
import socket
910
import time
1011
import unittest
1112
from threading import Event
12-
from typing import Iterable, Set
13+
from typing import Iterable, Set, cast
1314
from unittest.mock import patch
1415

1516
import pytest
@@ -75,6 +76,35 @@ class MyServiceListener(r.ServiceListener):
7576
zc.close()
7677

7778

79+
def test_service_browser_cancel_context_manager():
80+
"""Test we can cancel a ServiceBrowser with it being used as a context manager."""
81+
82+
# instantiate a zeroconf instance
83+
zc = Zeroconf(interfaces=['127.0.0.1'])
84+
# start a browser
85+
type_ = "_hap._tcp.local."
86+
87+
class MyServiceListener(r.ServiceListener):
88+
pass
89+
90+
listener = MyServiceListener()
91+
92+
browser = r.ServiceBrowser(zc, type_, None, listener)
93+
94+
assert cast(bool, browser.done) is False
95+
96+
with browser:
97+
pass
98+
99+
# ensure call_soon_threadsafe in ServiceBrowser.cancel is run
100+
assert zc.loop is not None
101+
asyncio.run_coroutine_threadsafe(asyncio.sleep(0), zc.loop).result()
102+
103+
assert cast(bool, browser.done) is True
104+
105+
zc.close()
106+
107+
78108
def test_service_browser_cancel_multiple_times_after_close():
79109
"""Test we can cancel a ServiceBrowser multiple times after close."""
80110

tests/test_asyncio.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import socket
1010
import threading
1111
import time
12+
from typing import cast
1213
from unittest.mock import ANY, call, patch
1314

1415
import pytest
@@ -779,6 +780,32 @@ async def test_async_context_manager() -> None:
779780
assert aiosinfo is not None
780781

781782

783+
@pytest.mark.asyncio
784+
async def test_service_browser_cancel_async_context_manager():
785+
"""Test we can cancel an AsyncServiceBrowser with it being used as an async context manager."""
786+
787+
# instantiate a zeroconf instance
788+
aiozc = AsyncZeroconf(interfaces=['127.0.0.1'])
789+
zc = aiozc.zeroconf
790+
type_ = "_hap._tcp.local."
791+
792+
class MyServiceListener(ServiceListener):
793+
pass
794+
795+
listener = MyServiceListener()
796+
797+
browser = AsyncServiceBrowser(zc, type_, None, listener)
798+
799+
assert cast(bool, browser.done) is False
800+
801+
async with browser:
802+
pass
803+
804+
assert cast(bool, browser.done) is True
805+
806+
await aiozc.async_close()
807+
808+
782809
@pytest.mark.asyncio
783810
async def test_async_unregister_all_services() -> None:
784811
"""Test unregistering all services."""

0 commit comments

Comments
 (0)