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:


Reply via email to