Hello community,

here is the log from the commit of package python-zeroconf for openSUSE:Factory 
checked in at 2020-11-26 23:15:13
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-zeroconf (Old)
 and      /work/SRC/openSUSE:Factory/.python-zeroconf.new.5913 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-zeroconf"

Thu Nov 26 23:15:13 2020 rev:16 rq:850926 version:0.28.6

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-zeroconf/python-zeroconf.changes  
2020-09-16 19:41:59.654978397 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-zeroconf.new.5913/python-zeroconf.changes    
    2020-11-26 23:16:09.977073587 +0100
@@ -1,0 +2,10 @@
+Thu Nov 26 08:50:58 UTC 2020 - Dirk Mueller <dmuel...@suse.com>
+
+- update to 0.28.6:
+  * Loosened service name validation when receiving from the network this lets 
us handle
+  some real world devices previously causing errors
+  * Enabled ignoring duplicated messages which decreases CPU usage
+  * Fixed spurious AttributeError: module 'unittest' has no attribute 'mock'
+  * Improved cache reaper performance significantly
+
+-------------------------------------------------------------------

Old:
----
  python-zeroconf-0.28.3.tar.gz

New:
----
  python-zeroconf-0.28.6.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-zeroconf.spec ++++++
--- /var/tmp/diff_new_pack.JpBhew/_old  2020-11-26 23:16:10.641074103 +0100
+++ /var/tmp/diff_new_pack.JpBhew/_new  2020-11-26 23:16:10.645074106 +0100
@@ -19,7 +19,7 @@
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %define skip_python2 1
 Name:           python-zeroconf
-Version:        0.28.3
+Version:        0.28.6
 Release:        0
 Summary:        Pure Python Multicast DNS Service Discovery Library 
(Bonjour/Avahi compatible)
 License:        LGPL-2.0-only

++++++ python-zeroconf-0.28.3.tar.gz -> python-zeroconf-0.28.6.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.28.3/.travis.yml 
new/python-zeroconf-0.28.6/.travis.yml
--- old/python-zeroconf-0.28.3/.travis.yml      2020-08-31 12:57:18.000000000 
+0200
+++ new/python-zeroconf-0.28.6/.travis.yml      2020-10-13 20:09:25.000000000 
+0200
@@ -4,8 +4,12 @@
     - "3.6"
     - "3.7"
     - "3.8"
+    - "3.9-dev"
     - "pypy3.5"
     - "pypy3"
+matrix:
+  allow_failures:
+    - python: "3.9-dev"
 install:
     - pip install --upgrade -r requirements-dev.txt
     # mypy can't be installed on pypy
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.28.3/README.rst 
new/python-zeroconf-0.28.6/README.rst
--- old/python-zeroconf-0.28.3/README.rst       2020-08-31 12:57:18.000000000 
+0200
+++ new/python-zeroconf-0.28.6/README.rst       2020-10-13 20:09:25.000000000 
+0200
@@ -134,10 +134,29 @@
 Changelog
 =========
 
+0.28.6
+======
+
+* Loosened service name validation when receiving from the network this lets 
us handle
+  some real world devices previously causing errors, thanks to J. Nick Koston.
+
+0.28.5
+======
+
+* Enabled ignoring duplicated messages which decreases CPU usage, thanks to J. 
Nick Koston.
+* Fixed spurious AttributeError: module 'unittest' has no attribute 'mock' in 
tests.
+
+0.28.4
+======
+
+* Improved cache reaper performance significantly, thanks to J. Nick Koston.
+* Added ServiceListener to __all__ as it's part of the public API, thanks to 
Justin Nesselrotte.
+
 0.28.3
 ======
 
-* Reduced a time an internal lock is held which should eliminate deadlocks in 
high-traffic networks.
+* Reduced a time an internal lock is held which should eliminate deadlocks in 
high-traffic networks,
+  thanks to J. Nick Koston.
 
 0.28.2
 ======
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.28.3/zeroconf/__init__.py 
new/python-zeroconf-0.28.6/zeroconf/__init__.py
--- old/python-zeroconf-0.28.3/zeroconf/__init__.py     2020-08-31 
12:57:18.000000000 +0200
+++ new/python-zeroconf-0.28.6/zeroconf/__init__.py     2020-10-13 
20:09:25.000000000 +0200
@@ -35,14 +35,14 @@
 import time
 import warnings
 from collections import OrderedDict
-from typing import Dict, List, Optional, Sequence, Union, cast
+from typing import Dict, Iterable, List, Optional, Sequence, Union, cast
 from typing import Any, Callable, Set, Tuple  # noqa # used in type hints
 
 import ifaddr
 
 __author__ = 'Paul Scott-Murphy, William McBrine'
 __maintainer__ = 'Jakub Stasiak <ja...@stasiak.at>'
-__version__ = '0.28.3'
+__version__ = '0.28.6'
 __license__ = 'LGPL'
 
 
@@ -51,6 +51,7 @@
     "Zeroconf",
     "ServiceInfo",
     "ServiceBrowser",
+    "ServiceListener",
     "Error",
     "InterfaceChoice",
     "ServiceStateChange",
@@ -178,6 +179,10 @@
 _EXPIRE_STALE_TIME_PERCENT = 50
 _EXPIRE_REFRESH_TIME_PERCENT = 75
 
+_LOCAL_TRAILER = '.local.'
+_TCP_PROTOCOL_LOCAL_TRAILER = '._tcp.local.'
+_NONTCP_PROTOCOL_LOCAL_TRAILER = '._udp.local.'
+
 try:
     _IPPROTO_IPV6 = socket.IPPROTO_IPV6
 except AttributeError:
@@ -228,7 +233,7 @@
     return socket.inet_pton(address_family, address)
 
 
-def service_type_name(type_: str, *, allow_underscores: bool = False) -> str:
+def service_type_name(type_: str, *, strict: bool = True) -> str:
     """
     Validate a fully qualified service name, instance or subtype. [rfc6763]
 
@@ -245,9 +250,11 @@
       This is true because we are implementing mDNS and since the 'm' means
       multi-cast, the 'local.' domain is mandatory.
 
-    2) local is preceded with either '_udp.' or '_tcp.'
+    2) local is preceded with either '_udp.' or '_tcp.' unless
+       strict is False
 
-    3) service name <sn> precedes <_tcp|_udp>
+    3) service name <sn> precedes <_tcp|_udp> unless
+       strict is False
 
       The rules for Service Names [RFC6335] state that they may be no more
       than fifteen characters long (not counting the mandatory underscore),
@@ -268,45 +275,64 @@
     :param type_: Type, SubType or service name to validate
     :return: fully qualified service name (eg: _http._tcp.local.)
     """
-    if not (type_.endswith('._tcp.local.') or type_.endswith('._udp.local.')):
-        raise BadTypeInNameException("Type '%s' must end with '._tcp.local.' 
or '._udp.local.'" % type_)
 
-    remaining = type_[: -len('._tcp.local.')].split('.')
-    name = remaining.pop()
-    if not name:
-        raise BadTypeInNameException("No Service name found")
+    if type_.endswith(_TCP_PROTOCOL_LOCAL_TRAILER) or 
type_.endswith(_NONTCP_PROTOCOL_LOCAL_TRAILER):
+        remaining = type_[: -len(_TCP_PROTOCOL_LOCAL_TRAILER)].split('.')
+        trailer = type_[-len(_TCP_PROTOCOL_LOCAL_TRAILER) :]
+        has_protocol = True
+    elif strict:
+        raise BadTypeInNameException(
+            "Type '%s' must end with '%s' or '%s'"
+            % (type_, _TCP_PROTOCOL_LOCAL_TRAILER, 
_NONTCP_PROTOCOL_LOCAL_TRAILER)
+        )
+    elif type_.endswith(_LOCAL_TRAILER):
+        remaining = type_[: -len(_LOCAL_TRAILER)].split('.')
+        trailer = type_[-len(_LOCAL_TRAILER) + 1 :]
+        has_protocol = False
+    else:
+        raise BadTypeInNameException("Type '%s' must end with '%s'" % (type_, 
_LOCAL_TRAILER))
 
-    if len(remaining) == 1 and len(remaining[0]) == 0:
-        raise BadTypeInNameException("Type '%s' must not start with '.'" % 
type_)
+    if strict or has_protocol:
+        service_name = remaining.pop()
+        if not service_name:
+            raise BadTypeInNameException("No Service name found")
 
-    if name[0] != '_':
-        raise BadTypeInNameException("Service name (%s) must start with '_'" % 
name)
+        if len(remaining) == 1 and len(remaining[0]) == 0:
+            raise BadTypeInNameException("Type '%s' must not start with '.'" % 
type_)
 
-    # remove leading underscore
-    name = name[1:]
+        if service_name[0] != '_':
+            raise BadTypeInNameException("Service name (%s) must start with 
'_'" % service_name)
 
-    if len(name) > 15:
-        raise BadTypeInNameException("Service name (%s) must be <= 15 bytes" % 
name)
+        test_service_name = service_name[1:]
 
-    if '--' in name:
-        raise BadTypeInNameException("Service name (%s) must not contain '--'" 
% name)
+        if len(test_service_name) > 15:
+            raise BadTypeInNameException("Service name (%s) must be <= 15 
bytes" % test_service_name)
 
-    if '-' in (name[0], name[-1]):
-        raise BadTypeInNameException("Service name (%s) may not start or end 
with '-'" % name)
+        if '--' in test_service_name:
+            raise BadTypeInNameException("Service name (%s) must not contain 
'--'" % test_service_name)
 
-    if not _HAS_A_TO_Z.search(name):
-        raise BadTypeInNameException("Service name (%s) must contain at least 
one letter (eg: 'A-Z')" % name)
+        if '-' in (test_service_name[0], test_service_name[-1]):
+            raise BadTypeInNameException(
+                "Service name (%s) may not start or end with '-'" % 
test_service_name
+            )
 
-    allowed_characters_re = (
-        _HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE if allow_underscores else 
_HAS_ONLY_A_TO_Z_NUM_HYPHEN
-    )
+        if not _HAS_A_TO_Z.search(test_service_name):
+            raise BadTypeInNameException(
+                "Service name (%s) must contain at least one letter (eg: 
'A-Z')" % test_service_name
+            )
 
-    if not allowed_characters_re.search(name):
-        raise BadTypeInNameException(
-            "Service name (%s) must contain only these characters: "
-            "A-Z, a-z, 0-9, hyphen ('-')%s" % (name, ", underscore ('_')" if 
allow_underscores else "")
+        allowed_characters_re = (
+            _HAS_ONLY_A_TO_Z_NUM_HYPHEN if strict else 
_HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE
         )
 
+        if not allowed_characters_re.search(test_service_name):
+            raise BadTypeInNameException(
+                "Service name (%s) must contain only these characters: "
+                "A-Z, a-z, 0-9, hyphen ('-')%s" % (test_service_name, "" if 
strict else ", underscore ('_')")
+            )
+    else:
+        service_name = ''
+
     if remaining and remaining[-1] == '_sub':
         remaining.pop()
         if len(remaining) == 0 or len(remaining[0]) == 0:
@@ -325,7 +351,7 @@
                 "Ascii control character 0x00-0x1F and 0x7F illegal in '%s'" % 
remaining[0]
             )
 
-    return '_' + name + type_[-len('._tcp.local.') :]
+    return service_name + trailer
 
 
 # Exceptions
@@ -1268,10 +1294,21 @@
         """Returns a list of all entries"""
         if not self.cache:
             return []
-        else:
-            # avoid size change during iteration by copying the cache
-            values = list(self.cache.values())
-            return list(itertools.chain.from_iterable(values))
+
+        # avoid size change during iteration by copying the cache
+        return list(itertools.chain.from_iterable(list(self.cache.values())))
+
+    def iterable_entries(self) -> Iterable[DNSRecord]:
+        """Returns an iterable of all entries.
+
+        This function is provided to avoid copying
+        the entries but is not threadsafe as the
+        contents of the cache can change during iteration.
+
+        Callers should trap RuntimeError and fallback
+        to calling entries.
+        """
+        return itertools.chain.from_iterable(self.cache.values())
 
 
 class Engine(threading.Thread):
@@ -1369,6 +1406,17 @@
             self.log_exception_warning('Error reading from socket %d', 
socket_.fileno())
             return
 
+        if self.data == data:
+            log.debug(
+                'Ignoring duplicate message received from %r:%r (socket %d) 
(%d bytes) as [%r]',
+                addr,
+                port,
+                socket_.fileno(),
+                len(data),
+                data,
+            )
+            return
+
         self.data = data
         msg = DNSIncoming(data)
         if msg.valid:
@@ -1422,15 +1470,29 @@
         self.name = "zeroconf-Reaper_%s" % (getattr(self, 'native_id', 
self.ident),)
 
     def run(self) -> None:
+        """Perodic removal of expired entries from the cache."""
         while True:
-            self.zc.wait(10 * 1000)
+            with self.zc.reaper_condition:
+                self.zc.reaper_condition.wait(10)
+
             if self.zc.done:
                 return
-            now = current_time_millis()
-            for record in self.zc.cache.entries():
-                if record.is_expired(now):
-                    self.zc.update_record(now, record)
-                    self.zc.cache.remove(record)
+            try:
+                # We try to iterate the cache without copying the whole
+                # cache as this can be quite an expensive operation.
+                self._cleanup_cache(self.zc.cache.iterable_entries())
+            except RuntimeError:
+                # If the cache changes during iteration, we fallback
+                # to making a copy before iteraiton.
+                self._cleanup_cache(self.zc.cache.entries())
+
+    def _cleanup_cache(self, entries: Iterable[DNSRecord]) -> None:
+        """Remove expired entries from the cache."""
+        now = current_time_millis()
+        for record in entries:
+            if record.is_expired(now):
+                self.zc.update_record(now, record)
+                self.zc.cache.remove(record)
 
 
 class Signal:
@@ -1501,7 +1563,7 @@
         assert handlers or listener, 'You need to specify at least one handler'
         self.types = set(type_ if isinstance(type_, list) else [type_])
         for check_type_ in self.types:
-            if not check_type_.endswith(service_type_name(check_type_, 
allow_underscores=True)):
+            if not check_type_.endswith(service_type_name(check_type_, 
strict=False)):
                 raise BadTypeInNameException
         threading.Thread.__init__(self)
         self.daemon = True
@@ -1733,7 +1795,7 @@
         # Accept both none, or one, but not both.
         if addresses is not None and parsed_addresses is not None:
             raise TypeError("addresses and parsed_addresses cannot be provided 
together")
-        if not type_.endswith(service_type_name(name, allow_underscores=True)):
+        if not type_.endswith(service_type_name(name, strict=False)):
             raise BadTypeInNameException
         self.type = type_
         self.name = name
@@ -2342,6 +2404,7 @@
         self.cache = DNSCache()
 
         self.condition = threading.Condition()
+        self.reaper_condition = threading.Condition()
 
         # Ensure we create the lock before
         # we add the listener as we could get
@@ -2373,6 +2436,11 @@
         with self.condition:
             self.condition.notify_all()
 
+    def notify_reaper(self) -> None:
+        """Notifies reaper"""
+        with self.reaper_condition:
+            self.reaper_condition.notify_all()
+
     def get_service_info(self, type_: str, name: str, timeout: int = 3000) -> 
Optional[ServiceInfo]:
         """Returns network's service information for a particular
         name and type, or None if no service matches by the timeout,
@@ -2878,6 +2946,7 @@
 
             # shutdown the rest
             self.notify_all()
+            self.notify_reaper()
             self.reaper.join()
             for s in self._respond_sockets:
                 s.close()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-zeroconf-0.28.3/zeroconf/test.py 
new/python-zeroconf-0.28.6/zeroconf/test.py
--- old/python-zeroconf-0.28.3/zeroconf/test.py 2020-08-31 12:57:18.000000000 
+0200
+++ new/python-zeroconf-0.28.6/zeroconf/test.py 2020-10-13 20:09:25.000000000 
+0200
@@ -12,6 +12,7 @@
 import threading
 import time
 import unittest
+import unittest.mock
 from threading import Event
 from typing import Dict, Optional  # noqa # used in type hints
 from typing import cast
@@ -656,14 +657,43 @@
         for name in bad_names_to_try:
             self.assertRaises(r.BadTypeInNameException, 
self.browser.get_service_info, name, 'x.' + name)
 
+    def test_bad_local_names_for_get_service_info(self):
+        bad_names_to_try = (
+            'homekitdev._nothttp._tcp.local.',
+            'homekitdev._http._udp.local.',
+        )
+        for name in bad_names_to_try:
+            self.assertRaises(
+                r.BadTypeInNameException, self.browser.get_service_info, 
'_http._tcp.local.', name
+            )
+
     def test_good_instance_names(self):
+        assert r.service_type_name('.._x._tcp.local.') == '_x._tcp.local.'
+        assert r.service_type_name('x.sub._http._tcp.local.') == 
'_http._tcp.local.'
+        assert (
+            
r.service_type_name('6d86f882b90facee9170ad3439d72a4d6ee9f511._zget._http._tcp.local.')
+            == '_http._tcp.local.'
+        )
+
+    def test_good_instance_names_without_protocol(self):
         good_names_to_try = (
-            '.._x._tcp.local.',
-            'x.sub._http._tcp.local.',
-            '6d86f882b90facee9170ad3439d72a4d6ee9f511._zget._http._tcp.local.',
+            "Rachio-C73233.local.",
+            'YeelightColorBulb-3AFD.local.',
+            'YeelightTunableBulb-7220.local.',
+            "AlexanderHomeAssistant 74651D.local.",
+            'iSmartGate-152.local.',
+            'MyQ-FGA.local.',
+            'lutron-02c4392a.local.',
+            'WICED-hap-3E2734.local.',
+            'MyHost.local.',
+            'MyHost.sub.local.',
         )
         for name in good_names_to_try:
-            r.service_type_name(name)
+            assert r.service_type_name(name, strict=False) == 'local.'
+
+        for name in good_names_to_try:
+            # Raises without strict=False
+            self.assertRaises(r.BadTypeInNameException, r.service_type_name, 
name)
 
     def test_bad_types(self):
         bad_names_to_try = (
@@ -686,17 +716,18 @@
 
     def test_good_service_names(self):
         good_names_to_try = (
-            '_x._tcp.local.',
-            '_x._udp.local.',
-            '_12345-67890-abc._udp.local.',
-            'x._sub._http._tcp.local.',
-            'a' * 63 + '._sub._http._tcp.local.',
-            'a' * 61 + u'â._sub._http._tcp.local.',
+            ('_x._tcp.local.', '_x._tcp.local.'),
+            ('_x._udp.local.', '_x._udp.local.'),
+            ('_12345-67890-abc._udp.local.', '_12345-67890-abc._udp.local.'),
+            ('x._sub._http._tcp.local.', '_http._tcp.local.'),
+            ('a' * 63 + '._sub._http._tcp.local.', '_http._tcp.local.'),
+            ('a' * 61 + u'â._sub._http._tcp.local.', '_http._tcp.local.'),
         )
-        for name in good_names_to_try:
-            r.service_type_name(name)
 
-        r.service_type_name('_one_two._tcp.local.', allow_underscores=True)
+        for name, result in good_names_to_try:
+            assert r.service_type_name(name) == result
+
+        assert r.service_type_name('_one_two._tcp.local.', strict=False) == 
'_one_two._tcp.local.'
 
     def test_invalid_addresses(self):
         type_ = "_test-srvc-type._tcp.local."
@@ -884,6 +915,54 @@
         assert 'a' not in cache.cache
 
 
+class TestReaper(unittest.TestCase):
+    def test_reaper(self):
+        zeroconf = Zeroconf(interfaces=['127.0.0.1'])
+        original_entries = zeroconf.cache.entries()
+        record_with_10s_ttl = r.DNSAddress('a', r._TYPE_SOA, r._CLASS_IN, 10, 
b'a')
+        record_with_1s_ttl = r.DNSAddress('a', r._TYPE_SOA, r._CLASS_IN, 1, 
b'b')
+        zeroconf.cache.add(record_with_10s_ttl)
+        zeroconf.cache.add(record_with_1s_ttl)
+        entries_with_cache = zeroconf.cache.entries()
+        time.sleep(1.05)
+        zeroconf.notify_reaper()
+        time.sleep(0.05)
+        entries = zeroconf.cache.entries()
+
+        try:
+            iterable_entries = list(zeroconf.cache.iterable_entries())
+        finally:
+            zeroconf.close()
+
+        assert entries != original_entries
+        assert entries_with_cache != original_entries
+        assert record_with_10s_ttl in entries
+        assert record_with_1s_ttl not in entries
+        assert record_with_10s_ttl in iterable_entries
+        assert record_with_1s_ttl not in iterable_entries
+
+    def test_reaper_with_dict_change_during_iteration(self):
+        zeroconf = Zeroconf(interfaces=['127.0.0.1'])
+        original_entries = zeroconf.cache.entries()
+        record_with_10s_ttl = r.DNSAddress('a', r._TYPE_SOA, r._CLASS_IN, 10, 
b'a')
+        record_with_1s_ttl = r.DNSAddress('a', r._TYPE_SOA, r._CLASS_IN, 1, 
b'b')
+        zeroconf.cache.add(record_with_10s_ttl)
+        zeroconf.cache.add(record_with_1s_ttl)
+        entries_with_cache = zeroconf.cache.entries()
+        with unittest.mock.patch("zeroconf.DNSCache.iterable_entries", 
side_effect=RuntimeError):
+            time.sleep(1.05)
+            zeroconf.notify_reaper()
+            time.sleep(0.05)
+
+        entries = zeroconf.cache.entries()
+        zeroconf.close()
+
+        assert entries != original_entries
+        assert entries_with_cache != original_entries
+        assert record_with_10s_ttl in entries
+        assert record_with_1s_ttl not in entries
+
+
 class ServiceTypesQuery(unittest.TestCase):
     def test_integration_with_listener(self):
 
_______________________________________________
openSUSE Commits mailing list -- commit@lists.opensuse.org
To unsubscribe, email commit-le...@lists.opensuse.org
List Netiquette: https://en.opensuse.org/openSUSE:Mailing_list_netiquette
List Archives: 
https://lists.opensuse.org/archives/list/commit@lists.opensuse.org

Reply via email to