Hello community,
here is the log from the commit of package python-zeroconf for openSUSE:Factory
checked in at 2019-05-12 11:34:20
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-zeroconf (Old)
and /work/SRC/openSUSE:Factory/.python-zeroconf.new.5148 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-zeroconf"
Sun May 12 11:34:20 2019 rev:8 rq:701032 version:0.22.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-zeroconf/python-zeroconf.changes
2019-03-18 10:38:34.555487351 +0100
+++
/work/SRC/openSUSE:Factory/.python-zeroconf.new.5148/python-zeroconf.changes
2019-05-12 11:34:20.818036477 +0200
@@ -1,0 +2,15 @@
+Mon May 6 09:00:04 UTC 2019 - [email protected]
+
+- version update to 0.22.0
+ * A lot of maintenance work (tooling, typing coverage and improvements,
+ spelling)
+ * Provided saner defaults in ServiceInfo's constructor, thanks to
+ Jorge Miranda
+ * Fixed service removal packets not being sent on shutdown, thanks to
+ Andrew Bonney
+ * Added a way to define TTL-s through ServiceInfo contructor parameters,
+ thanks to Andrew Bonney
+ * Adjusted query intervals to match RFC 6762, thanks to Andrew Bonney
+ * Made default TTL-s match RFC 6762, thanks to Andrew Bonney
+
+-------------------------------------------------------------------
Old:
----
0.21.3.tar.gz
New:
----
0.22.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-zeroconf.spec ++++++
--- /var/tmp/diff_new_pack.kVyvs8/_old 2019-05-12 11:34:21.502038480 +0200
+++ /var/tmp/diff_new_pack.kVyvs8/_new 2019-05-12 11:34:21.506038492 +0200
@@ -19,7 +19,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%define skip_python2 1
Name: python-zeroconf
-Version: 0.21.3
+Version: 0.22.0
Release: 0
Summary: Pure Python Multicast DNS Service Discovery Library
(Bonjour/Avahi compatible)
License: LGPL-2.0-only
++++++ 0.21.3.tar.gz -> 0.22.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.21.3/.travis.yml
new/python-zeroconf-0.22.0/.travis.yml
--- old/python-zeroconf-0.21.3/.travis.yml 2018-09-21 21:42:53.000000000
+0200
+++ new/python-zeroconf-0.22.0/.travis.yml 2019-04-27 21:18:46.000000000
+0200
@@ -3,7 +3,7 @@
- "3.4"
- "3.5"
- "3.6"
- - "pypy3.5-5.8.0"
+ - "pypy3.5-5.10.1"
matrix:
fast_finish: true
include:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.21.3/Makefile
new/python-zeroconf-0.22.0/Makefile
--- old/python-zeroconf-0.21.3/Makefile 2018-09-21 21:42:53.000000000 +0200
+++ new/python-zeroconf-0.22.0/Makefile 2019-04-27 21:18:46.000000000 +0200
@@ -14,7 +14,7 @@
flake8 --max-line-length=$(MAX_LINE_LENGTH) examples *.py
mypy:
- mypy examples/*.py zeroconf.py
+ mypy examples/*.py test_zeroconf.py zeroconf.py
test:
nosetests -v
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.21.3/README.rst
new/python-zeroconf-0.22.0/README.rst
--- old/python-zeroconf-0.21.3/README.rst 2018-09-21 21:42:53.000000000
+0200
+++ new/python-zeroconf-0.22.0/README.rst 2019-04-27 21:18:46.000000000
+0200
@@ -120,11 +120,25 @@
Changelog
=========
+0.22.0
+------
+
+* A lot of maintenance work (tooling, typing coverage and improvements,
spelling) done, thanks to Ville Skyttä
+* Provided saner defaults in ServiceInfo's constructor, thanks to Jorge Miranda
+* Fixed service removal packets not being sent on shutdown, thanks to Andrew
Bonney
+* Added a way to define TTL-s through ServiceInfo contructor parameters,
thanks to Andrew Bonney
+
+Technically backwards incompatible:
+
+* Adjusted query intervals to match RFC 6762, thanks to Andrew Bonney
+* Made default TTL-s match RFC 6762, thanks to Andrew Bonney
+
+
0.21.3
------
* This time really allowed incoming service names to contain underscores
(patch released
- as part of 0.20.0 was defective)
+ as part of 0.21.0 was defective)
0.21.2
------
@@ -143,7 +157,7 @@
* Fixed TTL handling for published service
* Implemented unicast support
* Fixed WSL (Windows Subsystem for Linux) compatibility
-* Fixed occassional UnboundLocalError issue
+* Fixed occasional UnboundLocalError issue
* Fixed UTF-8 multibyte name compression
* Switched from netifaces to ifaddr (pure Python)
* Allowed incoming service names to contain underscores
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.21.3/mypy.ini
new/python-zeroconf-0.22.0/mypy.ini
--- old/python-zeroconf-0.21.3/mypy.ini 2018-09-21 21:42:53.000000000 +0200
+++ new/python-zeroconf-0.22.0/mypy.ini 1970-01-01 01:00:00.000000000 +0100
@@ -1,7 +0,0 @@
-[mypy]
-ignore_missing_imports = true
-follow_imports = error
-warn_no_return = true
-warn_redundant_casts = true
-# TODO: disallow untyped defs once we have full type hint coverage
-disallow_untyped_defs = false
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.21.3/requirements-dev.txt
new/python-zeroconf-0.22.0/requirements-dev.txt
--- old/python-zeroconf-0.21.3/requirements-dev.txt 2018-09-21
21:42:53.000000000 +0200
+++ new/python-zeroconf-0.22.0/requirements-dev.txt 2019-04-27
21:18:46.000000000 +0200
@@ -1,11 +1,9 @@
autopep8
coveralls
coverage
-flake8
-flake8-blind-except
+# Version restricted because of https://github.com/PyCQA/pycodestyle/issues/741
+flake8>=3.6.0
flake8-import-order
ifaddr
nose
pep8-naming!=0.6.0
-# Version restricted because of https://github.com/PyCQA/pycodestyle/issues/741
-pycodestyle<2.4.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.21.3/setup.cfg
new/python-zeroconf-0.22.0/setup.cfg
--- old/python-zeroconf-0.21.3/setup.cfg 2018-09-21 21:42:53.000000000
+0200
+++ new/python-zeroconf-0.22.0/setup.cfg 2019-04-27 21:18:46.000000000
+0200
@@ -1,7 +1,19 @@
-[wheel]
-universal = 1
-
[flake8]
show-source = 1
application-import-names=zeroconf
max-line-length=110
+
+[mypy]
+ignore_missing_imports = true
+follow_imports = error
+check_untyped_defs = true
+no_implicit_optional = true
+warn_incomplete_stub = true
+warn_no_return = true
+warn_redundant_casts = true
+warn_unused_configs = true
+warn_unused_ignores = true
+warn_return_any = true
+# TODO: disallow untyped calls and defs once we have full type hint coverage
+disallow_untyped_calls = false
+disallow_untyped_defs = false
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.21.3/test_zeroconf.py
new/python-zeroconf-0.22.0/test_zeroconf.py
--- old/python-zeroconf-0.21.3/test_zeroconf.py 2018-09-21 21:42:53.000000000
+0200
+++ new/python-zeroconf-0.22.0/test_zeroconf.py 2019-04-27 21:18:46.000000000
+0200
@@ -11,6 +11,8 @@
import time
import unittest
from threading import Event
+from typing import Dict, Optional # noqa # used in type hints
+from typing import cast
import zeroconf as r
@@ -25,16 +27,18 @@
)
log = logging.getLogger('zeroconf')
-original_logging_level = [None]
+original_logging_level = logging.NOTSET
def setup_module():
- original_logging_level[0] = log.level
+ global original_logging_level
+ original_logging_level = log.level
log.setLevel(logging.DEBUG)
def teardown_module():
- log.setLevel(original_logging_level[0])
+ if original_logging_level != logging.NOTSET:
+ log.setLevel(original_logging_level)
class TestDunder(unittest.TestCase):
@@ -55,7 +59,7 @@
def test_dns_pointer_repr(self):
pointer = r.DNSPointer(
- 'irrelevant', r._TYPE_PTR, r._CLASS_IN, r._DNS_TTL, '123')
+ 'irrelevant', r._TYPE_PTR, r._CLASS_IN, r._DNS_OTHER_TTL, '123')
repr(pointer)
def test_dns_address_repr(self):
@@ -70,11 +74,11 @@
def test_dns_service_repr(self):
service = r.DNSService(
- 'irrelevant', r._TYPE_SRV, r._CLASS_IN, r._DNS_TTL, 0, 0, 80, b'a')
+ 'irrelevant', r._TYPE_SRV, r._CLASS_IN, r._DNS_HOST_TTL, 0, 0, 80,
b'a')
repr(service)
def test_dns_record_abc(self):
- record = r.DNSRecord('irrelevant', r._TYPE_SRV, r._CLASS_IN,
r._DNS_TTL)
+ record = r.DNSRecord('irrelevant', r._TYPE_SRV, r._CLASS_IN,
r._DNS_HOST_TTL)
self.assertRaises(r.AbstractMethodException, record.__eq__, record)
self.assertRaises(r.AbstractMethodException, record.write, None)
@@ -90,6 +94,18 @@
assert not info != info
repr(info)
+ def test_service_info_text_properties_not_given(self):
+ type_ = "_test-srvc-type._tcp.local."
+ name = "xxxyyy"
+ registration_name = "%s.%s" % (name, type_)
+ info = ServiceInfo(
+ type_=type_, name=registration_name,
+ address=socket.inet_aton("10.0.1.2"),
+ port=80, server="ash-2.local.")
+
+ assert isinstance(info.text, bytes)
+ repr(info)
+
def test_dns_outgoing_repr(self):
dns_outgoing = r.DNSOutgoing(r._FLAGS_QR_QUERY)
repr(dns_outgoing)
@@ -118,7 +134,7 @@
def test_parse_own_packet_response(self):
generated = r.DNSOutgoing(r._FLAGS_QR_RESPONSE)
generated.add_answer_at_time(r.DNSService(
- "æøå.local.", r._TYPE_SRV, r._CLASS_IN, r._DNS_TTL, 0, 0, 80,
"foo.local."), 0)
+ "æøå.local.", r._TYPE_SRV, r._CLASS_IN, r._DNS_HOST_TTL, 0, 0, 80,
"foo.local."), 0)
parsed = r.DNSIncoming(generated.packet())
self.assertEqual(len(generated.answers), 1)
self.assertEqual(len(generated.answers), len(parsed.answers))
@@ -137,11 +153,11 @@
question = r.DNSQuestion("testname.local.", r._TYPE_SRV, r._CLASS_IN)
query_generated.add_question(question)
answer1 = r.DNSService(
- "testname1.local.", r._TYPE_SRV, r._CLASS_IN, r._DNS_TTL, 0, 0,
80, "foo.local.")
+ "testname1.local.", r._TYPE_SRV, r._CLASS_IN, r._DNS_HOST_TTL, 0,
0, 80, "foo.local.")
staleanswer2 = r.DNSService(
- "testname2.local.", r._TYPE_SRV, r._CLASS_IN, r._DNS_TTL/2, 0, 0,
80, "foo.local.")
+ "testname2.local.", r._TYPE_SRV, r._CLASS_IN, r._DNS_HOST_TTL/2,
0, 0, 80, "foo.local.")
answer2 = r.DNSService(
- "testname2.local.", r._TYPE_SRV, r._CLASS_IN, r._DNS_TTL, 0, 0,
80, "foo.local.")
+ "testname2.local.", r._TYPE_SRV, r._CLASS_IN, r._DNS_HOST_TTL, 0,
0, 80, "foo.local.")
query_generated.add_answer_at_time(answer1, 0)
query_generated.add_answer_at_time(staleanswer2, 0)
query = r.DNSIncoming(query_generated.packet())
@@ -180,8 +196,9 @@
generated.add_additional_answer(
DNSHinfo('irrelevant', r._TYPE_HINFO, 0, 0, 'cpu', 'os'))
parsed = r.DNSIncoming(generated.packet())
- self.assertEqual(parsed.answers[0].cpu, u'cpu')
- self.assertEqual(parsed.answers[0].os, u'os')
+ answer = cast(r.DNSHinfo, parsed.answers[0])
+ self.assertEqual(answer.cpu, u'cpu')
+ self.assertEqual(answer.os, u'os')
generated = r.DNSOutgoing(0)
generated.add_additional_answer(
@@ -282,19 +299,20 @@
# we are going to monkey patch the zeroconf send to check packet sizes
old_send = zc.send
- # needs to be a list so that we can modify it in our phony send
- longest_packet = [0, None]
+ longest_packet_len = 0
+ longest_packet = None # type: Optional[r.DNSOutgoing]
def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT):
"""Sends an outgoing packet."""
packet = out.packet()
- if longest_packet[0] < len(packet):
- longest_packet[0] = len(packet)
- longest_packet[1] = out
+ nonlocal longest_packet_len, longest_packet
+ if longest_packet_len < len(packet):
+ longest_packet_len = len(packet)
+ longest_packet = out
old_send(out, addr=addr, port=port)
# monkey patch the zeroconf send
- zc.send = send
+ setattr(zc, "send", send)
# dummy service callback
def on_service_state_change(zeroconf, service_type, state_change,
name):
@@ -306,7 +324,7 @@
# wait until the browse request packet has maxed out in size
sleep_count = 0
while sleep_count < 100 and \
- longest_packet[0] < r._MAX_MSG_ABSOLUTE - 100:
+ longest_packet_len < r._MAX_MSG_ABSOLUTE - 100:
sleep_count += 1
time.sleep(0.1)
@@ -315,11 +333,11 @@
import zeroconf
zeroconf.log.debug('sleep_count %d, sized %d',
- sleep_count, longest_packet[0])
+ sleep_count, longest_packet_len)
# now the browser has sent at least one request, verify the size
- assert longest_packet[0] <= r._MAX_MSG_ABSOLUTE
- assert longest_packet[0] >= r._MAX_MSG_ABSOLUTE - 100
+ assert longest_packet_len <= r._MAX_MSG_ABSOLUTE
+ assert longest_packet_len >= r._MAX_MSG_ABSOLUTE - 100
# mock zeroconf's logger warning() and debug()
from unittest.mock import patch
@@ -330,7 +348,8 @@
# now that we have a long packet in our possession, let's verify the
# exception handling.
- out = longest_packet[1]
+ out = longest_packet
+ assert out is not None
out.data.append(b'\0' * 1000)
# mock the zeroconf logger and check for the correct logging backoff
@@ -422,10 +441,10 @@
out = r.DNSOutgoing(r._FLAGS_QR_RESPONSE | r._FLAGS_AA)
out.add_answer_at_time(
r.DNSPointer(type_, r._TYPE_PTR, r._CLASS_IN,
- r._DNS_TTL, name), 0)
+ r._DNS_OTHER_TTL, name), 0)
out.add_answer_at_time(
r.DNSService(type_, r._TYPE_SRV, r._CLASS_IN,
- r._DNS_TTL, 0, 0, 80,
+ r._DNS_HOST_TTL, 0, 0, 80,
name), 0)
zc.send(out)
@@ -570,32 +589,39 @@
# we are going to monkey patch the zeroconf send to check packet sizes
old_send = zc.send
- # needs to be a list so that we can modify it in our phony send
- nbr_answers = [0, None]
- nbr_additionals = [0, None]
- nbr_authorities = [0, None]
+ nbr_answers = nbr_additionals = nbr_authorities = 0
+
+ def get_ttl(record_type):
+ if expected_ttl is not None:
+ return expected_ttl
+ elif record_type in [r._TYPE_A, r._TYPE_SRV]:
+ return r._DNS_HOST_TTL
+ else:
+ return r._DNS_OTHER_TTL
def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT):
"""Sends an outgoing packet."""
+ nonlocal nbr_answers, nbr_additionals, nbr_authorities
+
for answer, time_ in out.answers:
- nbr_answers[0] += 1
- assert answer.ttl == expected_ttl
+ nbr_answers += 1
+ assert answer.ttl == get_ttl(answer.type)
for answer in out.additionals:
- nbr_additionals[0] += 1
- assert answer.ttl == expected_ttl
+ nbr_additionals += 1
+ assert answer.ttl == get_ttl(answer.type)
for answer in out.authorities:
- nbr_authorities[0] += 1
- assert answer.ttl == expected_ttl
+ nbr_authorities += 1
+ assert answer.ttl == get_ttl(answer.type)
old_send(out, addr=addr, port=port)
# monkey patch the zeroconf send
- zc.send = send
+ setattr(zc, "send", send)
# register service with default TTL
- expected_ttl = r._DNS_TTL
+ expected_ttl = None
zc.register_service(info)
- assert nbr_answers[0] == 12 and nbr_additionals[0] == 0 and
nbr_authorities[0] == 3
- nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0
+ assert nbr_answers == 12 and nbr_additionals == 0 and nbr_authorities
== 3
+ nbr_answers = nbr_additionals = nbr_authorities = 0
# query
query = r.DNSOutgoing(r._FLAGS_QR_QUERY | r._FLAGS_AA)
@@ -603,22 +629,22 @@
query.add_question(r.DNSQuestion(info.name, r._TYPE_SRV, r._CLASS_IN))
query.add_question(r.DNSQuestion(info.name, r._TYPE_TXT, r._CLASS_IN))
query.add_question(r.DNSQuestion(info.server, r._TYPE_A, r._CLASS_IN))
- zc.handle_query(query, r._MDNS_ADDR, r._MDNS_PORT)
- assert nbr_answers[0] == 4 and nbr_additionals[0] == 1 and
nbr_authorities[0] == 0
- nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0
+ zc.handle_query(r.DNSIncoming(query.packet()), r._MDNS_ADDR,
r._MDNS_PORT)
+ assert nbr_answers == 4 and nbr_additionals == 1 and nbr_authorities
== 0
+ nbr_answers = nbr_additionals = nbr_authorities = 0
# unregister
expected_ttl = 0
zc.unregister_service(info)
- assert nbr_answers[0] == 12 and nbr_additionals[0] == 0 and
nbr_authorities[0] == 0
- nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0
+ assert nbr_answers == 12 and nbr_additionals == 0 and nbr_authorities
== 0
+ nbr_answers = nbr_additionals = nbr_authorities = 0
# register service with custom TTL
- expected_ttl = r._DNS_TTL * 2
- assert expected_ttl != r._DNS_TTL
+ expected_ttl = r._DNS_HOST_TTL * 2
+ assert expected_ttl != r._DNS_HOST_TTL
zc.register_service(info, ttl=expected_ttl)
- assert nbr_answers[0] == 12 and nbr_additionals[0] == 0 and
nbr_authorities[0] == 3
- nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0
+ assert nbr_answers == 12 and nbr_additionals == 0 and nbr_authorities
== 3
+ nbr_answers = nbr_additionals = nbr_authorities = 0
# query
query = r.DNSOutgoing(r._FLAGS_QR_QUERY | r._FLAGS_AA)
@@ -626,15 +652,15 @@
query.add_question(r.DNSQuestion(info.name, r._TYPE_SRV, r._CLASS_IN))
query.add_question(r.DNSQuestion(info.name, r._TYPE_TXT, r._CLASS_IN))
query.add_question(r.DNSQuestion(info.server, r._TYPE_A, r._CLASS_IN))
- zc.handle_query(query, r._MDNS_ADDR, r._MDNS_PORT)
- assert nbr_answers[0] == 4 and nbr_additionals[0] == 1 and
nbr_authorities[0] == 0
- nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0
+ zc.handle_query(r.DNSIncoming(query.packet()), r._MDNS_ADDR,
r._MDNS_PORT)
+ assert nbr_answers == 4 and nbr_additionals == 1 and nbr_authorities
== 0
+ nbr_answers = nbr_additionals = nbr_authorities = 0
# unregister
expected_ttl = 0
zc.unregister_service(info)
- assert nbr_answers[0] == 12 and nbr_additionals[0] == 0 and
nbr_authorities[0] == 0
- nbr_answers[0] = nbr_additionals[0] = nbr_authorities[0] = 0
+ assert nbr_answers == 12 and nbr_additionals == 0 and nbr_authorities
== 0
+ nbr_answers = nbr_additionals = nbr_authorities = 0
class TestDNSCache(unittest.TestCase):
@@ -718,7 +744,7 @@
name = "xxxyyyæøå"
registration_name = "%s.%s" % (name, type_)
- class MyListener:
+ class MyListener(r.ServiceListener):
def add_service(self, zeroconf, type, name):
zeroconf.get_service_info(type, name)
service_added.set()
@@ -740,7 +766,7 @@
)
zeroconf_registrar = Zeroconf(interfaces=['127.0.0.1'])
- desc = {'path': '/~paulsm/'}
+ desc = {'path': '/~paulsm/'} # type: r.ServicePropertiesType
desc.update(properties)
info_service = ServiceInfo(
subtype, registration_name,
@@ -761,7 +787,7 @@
# get service info without answer cache
info = zeroconf_browser.get_service_info(type_, registration_name)
-
+ assert info is not None
assert info.properties[b'prop_none'] is False
assert info.properties[b'prop_string'] == properties['prop_string']
assert info.properties[b'prop_float'] is False
@@ -770,6 +796,7 @@
assert info.properties[b'prop_false'] is False
info = zeroconf_browser.get_service_info(subtype,
registration_name)
+ assert info is not None
assert info.properties[b'prop_none'] is False
zeroconf_registrar.unregister_service(info_service)
@@ -781,6 +808,74 @@
zeroconf_browser.close()
+def test_backoff():
+ got_query = Event()
+
+ type_ = "_http._tcp.local."
+ zeroconf_browser = Zeroconf(interfaces=['127.0.0.1'])
+
+ # we are going to monkey patch the zeroconf send to check query
transmission
+ old_send = zeroconf_browser.send
+
+ time_offset = 0.0
+ start_time = time.time() * 1000
+ initial_query_interval = r._BROWSER_TIME / 1000
+
+ def current_time_millis():
+ """Current system time in milliseconds"""
+ return start_time + time_offset * 1000
+
+ def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT):
+ """Sends an outgoing packet."""
+ got_query.set()
+ old_send(out, addr=addr, port=port)
+
+ # monkey patch the zeroconf send
+ setattr(zeroconf_browser, "send", send)
+
+ # monkey patch the zeroconf current_time_millis
+ r.current_time_millis = current_time_millis
+
+ # monkey patch the backoff limit to prevent test running forever
+ r._BROWSER_BACKOFF_LIMIT = 10 # seconds
+
+ # dummy service callback
+ def on_service_state_change(zeroconf, service_type, state_change, name):
+ pass
+
+ browser = ServiceBrowser(zeroconf_browser, type_,
[on_service_state_change])
+
+ try:
+ # Test that queries are sent at increasing intervals
+ sleep_count = 0
+ next_query_interval = 0.0
+ expected_query_time = 0.0
+ while True:
+ zeroconf_browser.notify_all()
+ sleep_count += 1
+ got_query.wait(0.1)
+ if time_offset == expected_query_time:
+ assert got_query.is_set()
+ got_query.clear()
+ if next_query_interval == r._BROWSER_BACKOFF_LIMIT:
+ # Only need to test up to the point where we've seen a
query
+ # after the backoff limit has been hit
+ break
+ elif next_query_interval == 0:
+ next_query_interval = initial_query_interval
+ expected_query_time = initial_query_interval
+ else:
+ next_query_interval = min(2*next_query_interval,
r._BROWSER_BACKOFF_LIMIT)
+ expected_query_time += next_query_interval
+ else:
+ assert not got_query.is_set()
+ time_offset += initial_query_interval
+
+ finally:
+ browser.cancel()
+ zeroconf_browser.close()
+
+
def test_integration():
service_added = Event()
service_removed = Event()
@@ -802,23 +897,22 @@
# we are going to monkey patch the zeroconf send to check packet sizes
old_send = zeroconf_browser.send
- time_offset = 0
+ time_offset = 0.0
def current_time_millis():
"""Current system time in milliseconds"""
return time.time() * 1000 + time_offset * 1000
- expected_ttl = r._DNS_TTL
+ expected_ttl = r._DNS_HOST_TTL
- # needs to be a list so that we can modify it in our phony send
- nbr_queries = [0, None]
+ nbr_answers = 0
def send(out, addr=r._MDNS_ADDR, port=r._MDNS_PORT):
"""Sends an outgoing packet."""
pout = r.DNSIncoming(out.packet())
-
+ nonlocal nbr_answers
for answer in pout.answers:
- nbr_queries[0] += 1
+ nbr_answers += 1
if not answer.ttl > expected_ttl / 2:
unexpected_ttl.set()
@@ -826,11 +920,14 @@
old_send(out, addr=addr, port=port)
# monkey patch the zeroconf send
- zeroconf_browser.send = send
+ setattr(zeroconf_browser, "send", send)
# monkey patch the zeroconf current_time_millis
r.current_time_millis = current_time_millis
+ # monkey patch the backoff limit to ensure we always get one query every
1/4 of the DNS TTL
+ r._BROWSER_BACKOFF_LIMIT = int(expected_ttl / 4)
+
service_added = Event()
service_removed = Event()
@@ -848,18 +945,26 @@
service_added.wait(1)
assert service_added.is_set()
+ # Test that we receive queries containing answers only if the
remaining TTL
+ # is greater than half the original TTL
sleep_count = 0
- while nbr_queries[0] < 50:
+ test_iterations = 50
+ while nbr_answers < test_iterations:
+ # Increase simulated time shift by 1/4 of the TTL in seconds
time_offset += expected_ttl / 4
zeroconf_browser.notify_all()
sleep_count += 1
- got_query.wait(1)
+ got_query.wait(0.1)
got_query.clear()
+ # Prevent the test running indefinitely in an error condition
+ assert sleep_count < test_iterations * 4
assert not unexpected_ttl.is_set()
# Don't remove service, allow close() to cleanup
finally:
zeroconf_registrar.close()
+ service_removed.wait(1)
+ assert service_removed.is_set()
browser.cancel()
zeroconf_browser.close()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python-zeroconf-0.21.3/zeroconf.py
new/python-zeroconf-0.22.0/zeroconf.py
--- old/python-zeroconf-0.21.3/zeroconf.py 2018-09-21 21:42:53.000000000
+0200
+++ new/python-zeroconf-0.22.0/zeroconf.py 2019-04-27 21:18:46.000000000
+0200
@@ -31,14 +31,14 @@
import threading
import time
from functools import reduce
-from typing import Callable # noqa # used in type hints
-from typing import Dict, List, Optional, Union
+from typing import AnyStr, Dict, List, Optional, Union, cast
+from typing import Callable, Set, Tuple # noqa # used in type hints
import ifaddr
__author__ = 'Paul Scott-Murphy, William McBrine'
__maintainer__ = 'Jakub Stasiak <[email protected]>'
-__version__ = '0.21.3'
+__version__ = '0.22.0'
__license__ = 'LGPL'
@@ -62,18 +62,20 @@
# Some timing constants
-_UNREGISTER_TIME = 125
-_CHECK_TIME = 175
-_REGISTER_TIME = 225
-_LISTENER_TIME = 200
-_BROWSER_TIME = 500
+_UNREGISTER_TIME = 125 # ms
+_CHECK_TIME = 175 # ms
+_REGISTER_TIME = 225 # ms
+_LISTENER_TIME = 200 # ms
+_BROWSER_TIME = 1000 # ms
+_BROWSER_BACKOFF_LIMIT = 3600 # s
# Some DNS constants
_MDNS_ADDR = '224.0.0.251'
_MDNS_PORT = 5353
_DNS_PORT = 53
-_DNS_TTL = 120 # two minutes default TTL as recommended by RFC6762
+_DNS_HOST_TTL = 120 # two minute for host records (A, SRV etc) as-per RFC6762
+_DNS_OTHER_TTL = 4500 # 75 minutes for non-host records (PTR, TXT etc) as-per
RFC6762
_MAX_MSG_TYPICAL = 1460 # unused
_MAX_MSG_ABSOLUTE = 8966
@@ -332,7 +334,7 @@
logger = log.debug
if logger_data is not None:
logger(*logger_data)
- logger('Exception occurred:', exc_info=exc_info)
+ logger('Exception occurred:', exc_info=True)
@classmethod
def log_warning_once(cls, *args):
@@ -350,7 +352,7 @@
"""A DNS entry"""
- def __init__(self, name, type_, class_):
+ def __init__(self, name: str, type_: int, class_):
self.key = name.lower()
self.name = name
self.type = type_
@@ -378,7 +380,7 @@
"""Type accessor"""
return _TYPES.get(t, "?(%s)" % t)
- def to_string(self, hdr, other):
+ def to_string(self, hdr, other) -> str:
"""String representation with additional information"""
result = "%s[%s,%s" % (hdr, self.get_type(self.type),
self.get_class_(self.class_))
@@ -416,7 +418,7 @@
"""A DNS record - like a DNS entry, but has a TTL"""
- def __init__(self, name, type_, class_, ttl):
+ def __init__(self, name, type_, class_, ttl: float):
DNSEntry.__init__(self, name, type_, class_)
self.ttl = ttl
self.created = current_time_millis()
@@ -442,7 +444,7 @@
and if its TTL is at least half of this record's."""
return self == other and other.ttl > (self.ttl / 2)
- def get_expiration_time(self, percent):
+ def get_expiration_time(self, percent: float) -> float:
"""Returns the time at which this record will have expired
by a certain percentage."""
return self.created + (percent * self.ttl * 10)
@@ -451,7 +453,7 @@
"""Returns the remaining TTL in seconds."""
return max(0, (self.get_expiration_time(100) - now) / 1000.0)
- def is_expired(self, now) -> bool:
+ def is_expired(self, now: float) -> bool:
"""Returns true if this record has expired."""
return self.get_expiration_time(100) <= now
@@ -640,10 +642,10 @@
"""Constructor from string holding bytes of packet"""
self.offset = 0
self.data = data
- self.questions = []
- self.answers = []
+ self.questions = [] # type: List[DNSQuestion]
+ self.answers = [] # type: List[DNSRecord]
self.id = 0
- self.flags = 0
+ self.flags = 0 # type: int
self.num_questions = 0
self.num_answers = 0
self.num_authorities = 0
@@ -709,7 +711,7 @@
domain = self.read_name()
type_, class_, ttl, length = self.unpack(b'!HHiH')
- rec = None
+ rec = None # type: Optional[DNSRecord]
if type_ == _TYPE_A:
rec = DNSAddress(
domain, type_, class_, ttl, self.read_string(4))
@@ -796,15 +798,15 @@
self.id = 0
self.multicast = multicast
self.flags = flags
- self.names = {}
- self.data = []
+ self.names = {} # type: Dict[str, int]
+ self.data = [] # type: List[bytes]
self.size = 12
self.state = self.State.init
- self.questions = []
- self.answers = []
- self.authorities = []
- self.additionals = []
+ self.questions = [] # type: List[DNSQuestion]
+ self.answers = [] # type: List[Tuple[DNSEntry, float]]
+ self.authorities = [] # type: List[DNSPointer]
+ self.additionals = [] # type: List[DNSAddress]
def __repr__(self):
return '<DNSOutgoing:{%s}>' % ', '.join([
@@ -1047,7 +1049,7 @@
"""A cache of DNS entries"""
def __init__(self):
- self.cache = {}
+ self.cache = {} # type: Dict[str, List[DNSEntry]]
def add(self, entry):
"""Adds an entry"""
@@ -1121,7 +1123,7 @@
threading.Thread.__init__(self, name='zeroconf-Engine')
self.daemon = True
self.zc = zc
- self.readers = {} # maps socket to reader
+ self.readers = {} # type: Dict[socket.socket, Listener]
self.timeout = 5
self.condition = threading.Condition()
self.start()
@@ -1228,14 +1230,14 @@
class Signal:
def __init__(self):
- self._handlers = []
+ self._handlers = [] # type: List[Callable[..., None]]
- def fire(self, **kwargs):
+ def fire(self, **kwargs) -> None:
for h in list(self._handlers):
h(**kwargs)
@property
- def registration_interface(self):
+ def registration_interface(self) -> 'SignalRegistrationInterface':
return SignalRegistrationInterface(self._handlers)
@@ -1258,6 +1260,14 @@
raise NotImplementedError()
+class ServiceListener:
+ def add_service(self, zc, type_, name) -> None:
+ raise NotImplementedError()
+
+ def remove_service(self, zc, type_, name) -> None:
+ raise NotImplementedError()
+
+
class ServiceBrowser(RecordUpdateListener, threading.Thread):
"""Used to browse for a service of a specific type.
@@ -1375,22 +1385,23 @@
self.zc.send(out, addr=self.addr, port=self.port)
self.next_time = now + self.delay
- self.delay = min(20 * 1000, self.delay * 2)
+ self.delay = min(_BROWSER_BACKOFF_LIMIT * 1000, self.delay * 2)
if len(self._handlers_to_call) > 0 and not self.zc.done:
handler = self._handlers_to_call.pop(0)
handler(self.zc)
-ServicePropertiesType = Dict[bytes, Union[bool, str]]
+ServicePropertiesType = Dict[AnyStr, Union[None, bool, AnyStr, float]]
class ServiceInfo(RecordUpdateListener):
"""Service information"""
- def __init__(self, type_: str, name: str, address: bytes = None, port: int
= None, weight: int = 0,
- priority: int = 0, properties=None, server: str = None) ->
None:
+ def __init__(self, type_: str, name: str, address: Optional[bytes] = None,
port: Optional[int] = None,
+ weight: int = 0, priority: int = 0, properties=b'', server:
Optional[str] = None,
+ host_ttl: int = _DNS_HOST_TTL, other_ttl: int =
_DNS_OTHER_TTL) -> None:
"""Create a service description.
type_: fully qualified service type name
@@ -1401,7 +1412,9 @@
priority: priority of the service
properties: dictionary of properties (or a string holding the
bytes for the text field)
- server: fully qualified name for service host (defaults to name)"""
+ server: fully qualified name for service host (defaults to name)
+ host_ttl: ttl used for A/SRV records
+ other_ttl: ttl used for PTR/TXT records"""
if not type_.endswith(service_type_name(name, allow_underscores=True)):
raise BadTypeInNameException
@@ -1417,9 +1430,8 @@
self.server = name
self._properties = {} # type: ServicePropertiesType
self._set_properties(properties)
- # FIXME: this is here only so that mypy doesn't complain when we set
and then use the attribute when
- # registering services. See if setting this to None by default is the
right way to go.
- self.ttl = None # type: Optional[int]
+ self.host_ttl = host_ttl
+ self.other_ttl = other_ttl
@property
def properties(self) -> ServicePropertiesType:
@@ -1458,7 +1470,7 @@
def _set_text(self, text):
"""Sets properties and text given a text field"""
self.text = text
- result = {}
+ result = {} # type: ServicePropertiesType
end = len(text)
index = 0
strs = []
@@ -1599,12 +1611,12 @@
)
-class ZeroconfServiceTypes:
+class ZeroconfServiceTypes(ServiceListener):
"""
Return all of the advertised services on any local networks
"""
def __init__(self):
- self.found_services = set()
+ self.found_services = set() # type: Set[str]
def add_service(self, zc, type_, name):
self.found_services.add(name)
@@ -1665,7 +1677,7 @@
# SO_REUSEADDR should be equivalent to SO_REUSEPORT for
# multicast UDP sockets (p 731, "TCP/IP Illustrated,
# Volume 2"), but some BSD-derived systems require
- # SO_REUSEPORT to be specified explicity. Also, not all
+ # SO_REUSEPORT to be specified explicitly. Also, not all
# versions of Python have SO_REUSEPORT available.
# Catch OSError and socket.error for kernel versions <3.9 because lacking
# SO_REUSEPORT support.
@@ -1695,7 +1707,7 @@
def get_errno(e: Exception) -> int:
assert isinstance(e, socket.error)
- return e.args[0]
+ return cast(int, e.args[0])
class Zeroconf(QuietLogger):
@@ -1764,7 +1776,7 @@
self._respond_sockets.append(respond_socket)
self.listeners = [] # type: List[RecordUpdateListener]
- self.browsers = {} # type: Dict[RecordUpdateListener, ServiceBrowser]
+ self.browsers = {} # type: Dict[ServiceListener, ServiceBrowser]
self.services = {} # type: Dict[str, ServiceInfo]
self.servicetypes = {} # type: Dict[str, int]
@@ -1807,14 +1819,14 @@
return info
return None
- def add_service_listener(self, type_: str, listener: RecordUpdateListener)
-> None:
+ def add_service_listener(self, type_: str, listener: ServiceListener) ->
None:
"""Adds a listener for a particular service type. This object
- will then have its update_record method called when information
- arrives for that type."""
+ will then have its add_service and remove_service methods called when
+ services of that type become available and unavailable."""
self.remove_service_listener(listener)
self.browsers[listener] = ServiceBrowser(self, type_, listener)
- def remove_service_listener(self, listener: RecordUpdateListener) -> None:
+ def remove_service_listener(self, listener: ServiceListener) -> None:
"""Removes a listener from the set that is currently listening."""
if listener in self.browsers:
self.browsers[listener].cancel()
@@ -1826,13 +1838,17 @@
self.remove_service_listener(listener)
def register_service(
- self, info: ServiceInfo, ttl: int = _DNS_TTL, allow_name_change: bool
= False,
+ self, info: ServiceInfo, ttl: Optional[int] = None, allow_name_change:
bool = False,
) -> None:
- """Registers service information to the network with a default TTL
- of 60 seconds. Zeroconf will then respond to requests for
- information for that service. The name of the service may be
- changed if needed to make it unique on the network."""
- info.ttl = ttl
+ """Registers service information to the network with a default TTL.
+ Zeroconf will then respond to requests for information for that
+ service. The name of the service may be changed if needed to make
+ it unique on the network."""
+ if ttl is not None:
+ # ttl argument is used to maintain backward compatibility
+ # Setting TTLs via ServiceInfo is preferred
+ info.host_ttl = ttl
+ info.other_ttl = ttl
self.check_service(info, allow_name_change)
self.services[info.name.lower()] = info
if info.type in self.servicetypes:
@@ -1849,18 +1865,18 @@
continue
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
out.add_answer_at_time(
- DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0)
+ DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, info.other_ttl,
info.name), 0)
out.add_answer_at_time(
DNSService(info.name, _TYPE_SRV, _CLASS_IN,
- ttl, info.priority, info.weight, info.port,
+ info.host_ttl, info.priority, info.weight,
info.port,
info.server), 0)
out.add_answer_at_time(
- DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0)
+ DNSText(info.name, _TYPE_TXT, _CLASS_IN, info.other_ttl,
info.text), 0)
if info.address:
out.add_answer_at_time(
DNSAddress(info.server, _TYPE_A, _CLASS_IN,
- ttl, info.address), 0)
+ info.host_ttl, info.address), 0)
self.send(out)
i += 1
next_time += _REGISTER_TIME
@@ -1968,7 +1984,7 @@
self.debug = out
out.add_question(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
out.add_authorative_answer(DNSPointer(
- info.type, _TYPE_PTR, _CLASS_IN, info.ttl, info.name))
+ info.type, _TYPE_PTR, _CLASS_IN, info.other_ttl, info.name))
self.send(out)
i += 1
next_time += _CHECK_TIME
@@ -2039,14 +2055,14 @@
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
out.add_answer(msg, DNSPointer(
"_services._dns-sd._udp.local.", _TYPE_PTR,
- _CLASS_IN, _DNS_TTL, stype))
+ _CLASS_IN, _DNS_OTHER_TTL, stype))
for service in self.services.values():
if question.name == service.type:
if out is None:
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
out.add_answer(msg, DNSPointer(
service.type, _TYPE_PTR,
- _CLASS_IN, service.ttl, service.name))
+ _CLASS_IN, service.other_ttl, service.name))
else:
try:
if out is None:
@@ -2059,7 +2075,7 @@
out.add_answer(msg, DNSAddress(
question.name, _TYPE_A,
_CLASS_IN | _CLASS_UNIQUE,
- service.ttl, service.address))
+ service.host_ttl, service.address))
name_to_find = question.name.lower()
if name_to_find not in self.services:
@@ -2069,16 +2085,16 @@
if question.type in (_TYPE_SRV, _TYPE_ANY):
out.add_answer(msg, DNSService(
question.name, _TYPE_SRV, _CLASS_IN |
_CLASS_UNIQUE,
- service.ttl, service.priority, service.weight,
+ service.host_ttl, service.priority, service.weight,
service.port, service.server))
if question.type in (_TYPE_TXT, _TYPE_ANY):
out.add_answer(msg, DNSText(
question.name, _TYPE_TXT, _CLASS_IN |
_CLASS_UNIQUE,
- service.ttl, service.text))
+ service.other_ttl, service.text))
if question.type == _TYPE_SRV:
out.add_additional_answer(DNSAddress(
service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE,
- service.ttl, service.address))
+ service.host_ttl, service.address))
except Exception: # TODO stop catching all Exceptions
self.log_exception_warning()
@@ -2112,10 +2128,10 @@
"""Ends the background threads, and prevent this instance from
servicing further queries."""
if not self._GLOBAL_DONE:
- self._GLOBAL_DONE = True
# remove service listeners
self.remove_all_service_listeners()
self.unregister_all_services()
+ self._GLOBAL_DONE = True
# shutdown recv socket and thread
if not self.unicast: