Skip to content

Commit cb069ef

Browse files
feat: context managers in ServiceBrowser and AsyncServiceBrowser
1 parent 84054ce commit cb069ef

4 files changed

Lines changed: 81 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: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import time
1010
import unittest
1111
from threading import Event
12-
from typing import Iterable, Set
12+
from typing import cast, Iterable, Set
1313
from unittest.mock import patch
1414

1515
import pytest
@@ -70,6 +70,31 @@ class MyServiceListener(r.ServiceListener):
7070
zc.close()
7171

7272

73+
def test_service_browser_cancel_context_manager():
74+
"""Test we can cancel a ServiceBrowser with it being used as a context manager."""
75+
76+
# instantiate a zeroconf instance
77+
zc = Zeroconf(interfaces=['127.0.0.1'])
78+
# start a browser
79+
type_ = "_hap._tcp.local."
80+
81+
class MyServiceListener(r.ServiceListener):
82+
pass
83+
84+
listener = MyServiceListener()
85+
86+
browser = r.ServiceBrowser(zc, type_, None, listener)
87+
88+
assert cast(browser.done, bool) is False
89+
90+
with browser:
91+
pass
92+
93+
assert cast(browser.done, bool) is True
94+
95+
zc.close()
96+
97+
7398
def test_service_browser_cancel_multiple_times_after_close():
7499
"""Test we can cancel a ServiceBrowser multiple times after close."""
75100

tests/test_asyncio.py

Lines changed: 29 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,34 @@ 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(browser.done, bool) is False
800+
801+
async with browser:
802+
pass
803+
804+
await asyncio.sleep(0)
805+
806+
assert cast(browser.done, bool) is True
807+
808+
await aiozc.async_close()
809+
810+
782811
@pytest.mark.asyncio
783812
async def test_async_unregister_all_services() -> None:
784813
"""Test unregistering all services."""

0 commit comments

Comments
 (0)