Skip to content

Commit c94dd78

Browse files
authored
Merge branch 'master' into drop_python_36
2 parents d3bfe4e + 2996e64 commit c94dd78

26 files changed

Lines changed: 403 additions & 81 deletions

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ jobs:
4242
python -m venv env
4343
${{ matrix.venvcmd }}
4444
pip install --upgrade -r requirements-dev.txt pytest-github-actions-annotate-failures
45+
- name: Validate readme
46+
if: ${{ runner.os == 'Linux' && matrix.python-version != 'pypy3' }}
47+
run: |
48+
${{ matrix.venvcmd }}
49+
python -m readme_renderer README.rst -o -
4550
- name: Run flake8
4651
if: ${{ runner.os == 'Linux' && matrix.python-version != 'pypy3' }}
4752
run: |

README.rst

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,67 @@ See examples directory for more.
138138
Changelog
139139
=========
140140

141+
0.37.0
142+
======
143+
144+
Technically backwards incompatible:
145+
146+
* Adding a listener that does not inherit from RecordUpdateListener now logs an error (#1034) @bdraco
147+
* The NotRunningException exception is now thrown when Zeroconf is not running (#1033) @bdraco
148+
149+
Before this change the consumer would get a timeout or an EventLoopBlocked
150+
exception when calling `ServiceInfo.*request` when the instance had already been shutdown
151+
or had failed to startup.
152+
153+
* The EventLoopBlocked exception is now thrown when a coroutine times out (#1032) @bdraco
154+
155+
Previously `concurrent.futures.TimeoutError` would have been raised
156+
instead. This is never expected to happen during normal operation.
157+
158+
0.36.13
159+
=======
160+
161+
* Unavailable interfaces are now skipped during socket bind (#1028) @bdraco
162+
* Downgraded incoming corrupt packet logging to debug (#1029) @bdraco
163+
164+
Warning about network traffic we have no control over is confusing
165+
to users as they think there is something wrong with zeroconf
166+
167+
0.36.12
168+
=======
169+
170+
* Prevented service lookups from deadlocking if time abruptly moves backwards (#1006) @bdraco
171+
172+
The typical reason time moves backwards is via an ntp update
173+
174+
0.36.11
175+
=======
176+
177+
No functional changes from 0.36.10. This release corrects an error in the README.rst file
178+
that prevented the build from uploading to PyPI
179+
180+
0.36.10
181+
=======
182+
183+
* scope_id is now stripped from IPv6 addresses if given (#1020) @StevenLooman
184+
185+
cpython 3.9 allows a suffix %scope_id in IPv6Address. This caused an error
186+
with the existing code if it was not stripped
187+
* Optimized decoding labels from incoming packets (#1019) @bdraco
188+
189+
0.36.9
190+
======
191+
192+
* Ensure ServiceInfo orders newest addresses first (#1012) @bdraco
193+
194+
This change effectively restored the behavior before 1s cache flush
195+
expire behavior described in rfc6762 section 10.2 was added for callers that rely on this.
196+
197+
0.36.8
198+
======
199+
200+
* Fixed ServiceBrowser infinite loop when zeroconf is closed before it is canceled (#1008) @bdraco
201+
141202
0.36.7
142203
======
143204

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ pytest
1414
pytest-asyncio
1515
pytest-cov
1616
pytest-timeout
17+
readme_renderer

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.36.7
2+
current_version = 0.37.0
33
commit = True
44
tag = True
55
tag_name = {new_version}

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def has_working_ipv6():
5959
if not socket.has_ipv6:
6060
return False
6161

62+
sock = None
6263
try:
6364
sock = socket.socket(socket.AF_INET6)
6465
sock.bind(('::1', 0))

tests/services/test_browser.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ def test_backoff():
441441
old_send = zeroconf_browser.async_send
442442

443443
time_offset = 0.0
444-
start_time = time.time() * 1000
444+
start_time = time.monotonic() * 1000
445445
initial_query_interval = _services_browser._BROWSER_TIME / 1000
446446

447447
def current_time_millis():
@@ -988,32 +988,32 @@ async def test_query_scheduler():
988988

989989
assert set(query_scheduler.process_ready_types(now)) == types_
990990
assert set(query_scheduler.process_ready_types(now)) == set()
991-
assert query_scheduler.millis_to_wait(now) == delay
991+
assert query_scheduler.millis_to_wait(now) == pytest.approx(delay, 0.00001)
992992

993993
assert set(query_scheduler.process_ready_types(now + delay)) == types_
994994
assert set(query_scheduler.process_ready_types(now + delay)) == set()
995-
assert query_scheduler.millis_to_wait(now) == delay * 3
995+
assert query_scheduler.millis_to_wait(now) == pytest.approx(delay * 3, 0.00001)
996996

997997
assert set(query_scheduler.process_ready_types(now + delay * 3)) == types_
998998
assert set(query_scheduler.process_ready_types(now + delay * 3)) == set()
999-
assert query_scheduler.millis_to_wait(now) == delay * 7
999+
assert query_scheduler.millis_to_wait(now) == pytest.approx(delay * 7, 0.00001)
10001000

10011001
assert set(query_scheduler.process_ready_types(now + delay * 7)) == types_
10021002
assert set(query_scheduler.process_ready_types(now + delay * 7)) == set()
1003-
assert query_scheduler.millis_to_wait(now) == delay * 15
1003+
assert query_scheduler.millis_to_wait(now) == pytest.approx(delay * 15, 0.00001)
10041004

10051005
assert set(query_scheduler.process_ready_types(now + delay * 15)) == types_
10061006
assert set(query_scheduler.process_ready_types(now + delay * 15)) == set()
10071007

10081008
# Test if we reschedule 1 second later, the millis_to_wait goes up by 1
10091009
query_scheduler.reschedule_type("_hap._tcp.local.", now + delay * 16)
1010-
assert query_scheduler.millis_to_wait(now) == delay * 16
1010+
assert query_scheduler.millis_to_wait(now) == pytest.approx(delay * 16, 0.00001)
10111011

10121012
assert set(query_scheduler.process_ready_types(now + delay * 15)) == set()
10131013

10141014
# Test if we reschedule 1 second later... and its ready for processing
10151015
assert set(query_scheduler.process_ready_types(now + delay * 16)) == {"_hap._tcp.local."}
1016-
assert query_scheduler.millis_to_wait(now) == delay * 31
1016+
assert query_scheduler.millis_to_wait(now) == pytest.approx(delay * 31, 0.00001)
10171017
assert set(query_scheduler.process_ready_types(now + delay * 20)) == set()
10181018

10191019
assert set(query_scheduler.process_ready_types(now + delay * 31)) == {"_http._tcp.local."}

tests/services/test_info.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,27 @@ def test_multiple_addresses():
559559
# This test uses asyncio because it needs to access the cache directly
560560
# which is not threadsafe
561561
@pytest.mark.asyncio
562-
async def test_multiple_a_addresses():
562+
async def test_multiple_a_addresses_newest_address_first():
563+
"""Test that info.addresses returns the newest seen address first."""
564+
type_ = "_http._tcp.local."
565+
registration_name = "multiarec.%s" % type_
566+
desc = {'path': '/~paulsm/'}
567+
aiozc = AsyncZeroconf(interfaces=['127.0.0.1'])
568+
cache = aiozc.zeroconf.cache
569+
host = "multahost.local."
570+
record1 = r.DNSAddress(host, const._TYPE_A, const._CLASS_IN, 1000, b'\x7f\x00\x00\x01')
571+
record2 = r.DNSAddress(host, const._TYPE_A, const._CLASS_IN, 1000, b'\x7f\x00\x00\x02')
572+
cache.async_add_records([record1, record2])
573+
574+
# New kwarg way
575+
info = ServiceInfo(type_, registration_name, 80, 0, 0, desc, host)
576+
info.load_from_cache(aiozc.zeroconf)
577+
assert info.addresses == [b'\x7f\x00\x00\x02', b'\x7f\x00\x00\x01']
578+
await aiozc.async_close()
579+
580+
581+
@pytest.mark.asyncio
582+
async def test_invalid_a_addresses(caplog):
563583
type_ = "_http._tcp.local."
564584
registration_name = "multiarec.%s" % type_
565585
desc = {'path': '/~paulsm/'}
@@ -573,7 +593,9 @@ async def test_multiple_a_addresses():
573593
# New kwarg way
574594
info = ServiceInfo(type_, registration_name, 80, 0, 0, desc, host)
575595
info.load_from_cache(aiozc.zeroconf)
576-
assert set(info.addresses) == {b'a', b'b'}
596+
assert not info.addresses
597+
assert "Encountered invalid address while processing record" in caplog.text
598+
577599
await aiozc.async_close()
578600

579601

tests/test_asyncio.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
DNSService,
2424
DNSAddress,
2525
DNSText,
26+
NotRunningException,
2627
ServiceStateChange,
2728
Zeroconf,
2829
const,
@@ -850,7 +851,7 @@ def on_service_state_change(zeroconf, service_type, state_change, name):
850851

851852
def _new_current_time_millis():
852853
"""Current system time in milliseconds"""
853-
return (time.time() * 1000) + (time_offset * 1000)
854+
return (time.monotonic() * 1000) + (time_offset * 1000)
854855

855856
expected_ttl = const._DNS_HOST_TTL
856857
nbr_answers = 0
@@ -1090,6 +1091,15 @@ async def test_async_request_timeout():
10901091
assert (end_time - start_time) < 3000 + 1000
10911092

10921093

1094+
@pytest.mark.asyncio
1095+
async def test_async_request_non_running_instance():
1096+
"""Test that the async_request throws when zeroconf is not running."""
1097+
aiozc = AsyncZeroconf(interfaces=['127.0.0.1'])
1098+
await aiozc.async_close()
1099+
with pytest.raises(NotRunningException):
1100+
await aiozc.async_get_service_info("_notfound.local.", "notthere._notfound.local.")
1101+
1102+
10931103
@pytest.mark.asyncio
10941104
async def test_legacy_unicast_response(run_isolated):
10951105
"""Verify legacy unicast responses include questions and correct id."""

tests/test_exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,11 @@ def test_invalid_addresses(self):
141141
name = "xxxyyy"
142142
registration_name = f"{name}.{type_}"
143143

144-
bad = ('127.0.0.1', '::1', 42)
144+
bad = (b'127.0.0.1', b'::1')
145145
for addr in bad:
146146
self.assertRaisesRegex(
147147
TypeError,
148-
'Addresses must be bytes',
148+
'Addresses must either ',
149149
ServiceInfo,
150150
type_,
151151
registration_name,

tests/test_handlers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1538,3 +1538,25 @@ async def test_future_answers_are_removed_on_send():
15381538

15391539
# But the one we have not sent yet shoudl still go out later
15401540
assert info2.dns_pointer() in outgoing_queue.queue[0].answers
1541+
1542+
1543+
@pytest.mark.asyncio
1544+
async def test_add_listener_warns_when_not_using_record_update_listener(caplog):
1545+
"""Log when a listener is added that is not using RecordUpdateListener as a base class."""
1546+
1547+
aiozc = AsyncZeroconf(interfaces=['127.0.0.1'])
1548+
zc: Zeroconf = aiozc.zeroconf
1549+
updated = []
1550+
1551+
class MyListener:
1552+
"""A RecordUpdateListener that does not implement update_records."""
1553+
1554+
def async_update_records(self, zc: 'Zeroconf', now: float, records: List[r.RecordUpdate]) -> None:
1555+
"""Update multiple records in one shot."""
1556+
updated.extend(records)
1557+
1558+
zc.add_listener(MyListener(), None)
1559+
await asyncio.sleep(0) # flush out any call soons
1560+
assert "listeners passed to async_add_listener must inherit from RecordUpdateListener" in caplog.text
1561+
1562+
await aiozc.async_close()

0 commit comments

Comments
 (0)