Repository: rthalley/dnspython

Issue:
httpx 0.28+ deprecationwarning  when passing str to verify for dns.query.https(), and mypy complaint when passing ssl.SSLContext
**Describe the bug**
Hello :)

With dnspython 2.7.0 and since httpx 0.28 I am getting this deprecation warning https://github.com/encode/httpx/blob/master/httpx/_config.py#L45-L50 when passing a string to the `verify` arg of `dns.query.https()`:

```deprecationwarning: `verify=<str>` is deprecated. Use `verify=ssl.create_default_context(cafile=...)` or `verify=ssl.create_default_context(capath=...)` instead```

Passing a `ssl.SSLContext()` instance instead of a string works well and also silences the warning, but then mypy complains because the signature of `dns.query.https()` only allows `bool` and `str` for the `verify` argument:

```
src/dns_exporter/collector.py:463: error: Argument "verify" to "https" has incompatible type "Union[SSLContext, bool]"; expected "Union[bool, str]"  [arg-type]
```

I propose a small patch to permit `ssl.SSLContext` as well as `bool` and `str` for the `verify` argument for `dns.query.https()`, see https://github.com/rthalley/dnspython/pull/1173

**To Reproduce**
I discovered this because my tox runs with all warnings as errors but I think `-Wd` can trigger the warnings with any https query:

```
(venv) user@privat-dev:~/devel/dns_exporter/src$ python -Wd
Python 3.10.13 (main, Nov 15 2023, 13:09:29) [GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import dns.name
>>> import dns.query
>>> import dns.message
>>> qname = dns.name.from_text('dns.google.')
>>> q = dns.message.make_query(qname, dns.rdatatype.A)
>>> dns.query.https(q, '8.8.8.8', verify="/etc/ssl/certs")
/home/user/devel/dns_exporter/venv/lib/python3.10/site-packages/httpx/_config.py:51: DeprecationWarning: `verify=<str>` is deprecated. Use `verify=ssl.create_default_context(cafile=...)` or `verify=ssl.create_default_context(capath=...)` instead.
  warnings.warn(message, DeprecationWarning)
<DNS message, ID 50759>
>>> 
(venv) user@privat-dev:~/devel/dns_exporter/src$ pip freeze | grep -E "dnspython|httpx"
dnspython==2.7.0
httpx==0.28.1
(venv) user@privat-dev:~/devel/dns_exporter/src$ 
```

**Context (please complete the following information):**
 - dnspython 2.7.0
 - httpx 0.28+
 - Python 3.10.13
 - OS: debian

Hints from issue comments:
@rthalley: Fixed, as #1173 has been merged.  Thanks!

Solution patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 837a23b..c2202c0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -54,7 +54,7 @@ jobs:
         python -m pip install cryptography trio idna httpx h2 aioquic mypy pytest ruff pyright
     - name: Typecheck
       run: |
-        python -m mypy --install-types --non-interactive --disallow-incomplete-defs dns
+        python -m mypy --disallow-incomplete-defs dns
         pyright dns
     - name: Lint
       run: |
diff --git a/dns/_asyncio_backend.py b/dns/_asyncio_backend.py
index 99656d4..303908c 100644
--- a/dns/_asyncio_backend.py
+++ b/dns/_asyncio_backend.py
@@ -131,7 +131,7 @@ if dns._features.have("doh"):
     import httpx
 
     _CoreAsyncNetworkBackend = httpcore.AsyncNetworkBackend
-    _CoreAnyIOStream = httpcore._backends.anyio.AnyIOStream
+    _CoreAnyIOStream = httpcore._backends.anyio.AnyIOStream  # pyright: ignore
 
     from dns.query import _compute_times, _expiration_for_this_attempt, _remaining
 
diff --git a/dns/_tls_util.py b/dns/_tls_util.py
new file mode 100644
index 0000000..79c421d
--- /dev/null
+++ b/dns/_tls_util.py
@@ -0,0 +1,19 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import os
+from typing import Optional, Tuple, Union
+
+
+def convert_verify_to_cafile_and_capath(
+    verify: Union[bool, str],
+) -> Tuple[Optional[str], Optional[str]]:
+    cafile: Optional[str] = None
+    capath: Optional[str] = None
+    if isinstance(verify, str):
+        if os.path.isfile(verify):
+            cafile = verify
+        elif os.path.isdir(verify):
+            capath = verify
+        else:
+            raise ValueError("invalid verify string")
+    return cafile, capath
diff --git a/dns/asyncquery.py b/dns/asyncquery.py
index 883e8af..3aae002 100644
--- a/dns/asyncquery.py
+++ b/dns/asyncquery.py
@@ -536,10 +536,10 @@ async def https(
     source_port: int = 0,  # pylint: disable=W0613
     one_rr_per_rrset: bool = False,
     ignore_trailing: bool = False,
-    client: Optional["httpx.AsyncClient"] = None,
+    client: Optional["httpx.AsyncClient|dns.quic.AsyncQuicConnection"] = None,
     path: str = "/dns-query",
     post: bool = True,
-    verify: Union[bool, str] = True,
+    verify: Union[bool, str, ssl.SSLContext] = True,
     bootstrap_address: Optional[str] = None,
     resolver: Optional["dns.asyncresolver.Resolver"] = None,  # pyright: ignore
     family: int = socket.AF_UNSPEC,
@@ -591,6 +591,11 @@ async def https(
                 parsed.hostname, family  # pyright: ignore
             )
             bootstrap_address = random.choice(list(answers.addresses()))
+        if client and not isinstance(
+            client, dns.quic.AsyncQuicConnection
+        ):  # pyright: ignore
+            raise ValueError("client parameter must be a dns.quic.AsyncQuicConnection.")
+        assert client is None or isinstance(client, dns.quic.AsyncQuicConnection)
         return await _http3(
             q,
             bootstrap_address,
@@ -603,13 +608,14 @@ async def https(
             ignore_trailing,
             verify=verify,
             post=post,
+            connection=client,
         )
 
     if not have_doh:
         raise NoDOH  # pragma: no cover
     # pylint: disable=possibly-used-before-assignment
     if client and not isinstance(client, httpx.AsyncClient):  # pyright: ignore
-        raise ValueError("session parameter must be an httpx.AsyncClient")
+        raise ValueError("client parameter must be an httpx.AsyncClient")
     # pylint: enable=possibly-used-before-assignment
 
     wire = q.to_wire()
@@ -702,35 +708,46 @@ async def _http3(
     where: str,
     url: str,
     timeout: Optional[float] = None,
-    port: int = 853,
+    port: int = 443,
     source: Optional[str] = None,
     source_port: int = 0,
     one_rr_per_rrset: bool = False,
     ignore_trailing: bool = False,
     verify: Union[bool, str] = True,
     backend: Optional[dns.asyncbackend.Backend] = None,
-    hostname: Optional[str] = None,
     post: bool = True,
+    connection: Optional[dns.quic.AsyncQuicConnection] = None,
 ) -> dns.message.Message:
     if not dns.quic.have_quic:
         raise NoDOH("DNS-over-HTTP3 is not available.")  # pragma: no cover
 
     url_parts = urllib.parse.urlparse(url)
     hostname = url_parts.hostname
+    assert hostname is not None
     if url_parts.port is not None:
         port = url_parts.port
 
     q.id = 0
     wire = q.to_wire()
-    (cfactory, mfactory) = dns.quic.factories_for_backend(backend)
+    the_connection: dns.quic.AsyncQuicConnection
+    if connection:
+        cfactory = dns.quic.null_factory
+        mfactory = dns.quic.null_factory
+    else:
+        (cfactory, mfactory) = dns.quic.factories_for_backend(backend)
 
     async with cfactory() as context:
         async with mfactory(
             context, verify_mode=verify, server_name=hostname, h3=True
         ) as the_manager:
-            the_connection = the_manager.connect(where, port, source, source_port)
+            if connection:
+                the_connection = connection
+            else:
+                the_connection = the_manager.connect(  # pyright: ignore
+                    where, port, source, source_port
+                )
             (start, expiration) = _compute_times(timeout)
-            stream = await the_connection.make_stream(timeout)
+            stream = await the_connection.make_stream(timeout)  # pyright: ignore
             async with stream:
                 # note that send_h3() does not need await
                 stream.send_h3(url, wire, post)
diff --git a/dns/query.py b/dns/query.py
index 6006a73..63733e5 100644
--- a/dns/query.py
+++ b/dns/query.py
@@ -32,6 +32,7 @@ import urllib.parse
 from typing import Any, Dict, Optional, Tuple, Union, cast
 
 import dns._features
+import dns._tls_util
 import dns.exception
 import dns.inet
 import dns.message
@@ -240,22 +241,22 @@ def _wait_for(fd, readable, writable, _, expiration):
 
     if readable and isinstance(fd, ssl.SSLSocket) and fd.pending() > 0:
         return True
-    sel = selectors.DefaultSelector()
-    events = 0
-    if readable:
-        events |= selectors.EVENT_READ
-    if writable:
-        events |= selectors.EVENT_WRITE
-    if events:
-        sel.register(fd, events)  # pyright: ignore
-    if expiration is None:
-        timeout = None
-    else:
-        timeout = expiration - time.time()
-        if timeout <= 0.0:
+    with selectors.DefaultSelector() as sel:
+        events = 0
+        if readable:
+            events |= selectors.EVENT_READ
+        if writable:
+            events |= selectors.EVENT_WRITE
+        if events:
+            sel.register(fd, events)  # pyright: ignore
+        if expiration is None:
+            timeout = None
+        else:
+            timeout = expiration - time.time()
+            if timeout <= 0.0:
+                raise dns.exception.Timeout
+        if not sel.select(timeout):
             raise dns.exception.Timeout
-    if not sel.select(timeout):
-        raise dns.exception.Timeout
 
 
 def _wait_for_readable(s, expiration):
@@ -401,7 +402,7 @@ def https(
     path: str = "/dns-query",
     post: bool = True,
     bootstrap_address: Optional[str] = None,
-    verify: Union[bool, str] = True,
+    verify: Union[bool, str, ssl.SSLContext] = True,
     resolver: Optional["dns.resolver.Resolver"] = None,  # pyright: ignore
     family: int = socket.AF_UNSPEC,
     http_version: HTTPVersion = HTTPVersion.DEFAULT,
@@ -490,6 +491,10 @@ def https(
             assert parsed.hostname is not None  # pyright: ignore
             answers = resolver.resolve_name(parsed.hostname, family)  # pyright: ignore
             bootstrap_address = random.choice(list(answers.addresses()))
+        if session and not isinstance(
+            session, dns.quic.SyncQuicConnection
+        ):  # pyright: ignore
+            raise ValueError("session parameter must be a dns.quic.SyncQuicConnection.")
         return _http3(
             q,
             bootstrap_address,
@@ -502,6 +507,7 @@ def https(
             ignore_trailing,
             verify=verify,
             post=post,
+            connection=session,
         )
 
     if not have_doh:
@@ -544,6 +550,7 @@ def https(
     with cm as session:
         # see https://tools.ietf.org/html/rfc8484#section-4.1.1 for DoH
         # GET and POST examples
+        assert session is not None
         if post:
             headers.update(
                 {
@@ -620,33 +627,45 @@ def _http3(
     where: str,
     url: str,
     timeout: Optional[float] = None,
-    port: int = 853,
+    port: int = 443,
     source: Optional[str] = None,
     source_port: int = 0,
     one_rr_per_rrset: bool = False,
     ignore_trailing: bool = False,
     verify: Union[bool, str] = True,
-    hostname: Optional[str] = None,
     post: bool = True,
+    connection: Optional[dns.quic.SyncQuicConnection] = None,
 ) -> dns.message.Message:
     if not dns.quic.have_quic:
         raise NoDOH("DNS-over-HTTP3 is not available.")  # pragma: no cover
 
     url_parts = urllib.parse.urlparse(url)
     hostname = url_parts.hostname
+    assert hostname is not None
     if url_parts.port is not None:
         port = url_parts.port
 
     q.id = 0
     wire = q.to_wire()
-    manager = dns.quic.SyncQuicManager(
-        verify_mode=verify, server_name=hostname, h3=True  # pyright: ignore
-    )
+    the_connection: dns.quic.SyncQuicConnection
+    the_manager: dns.quic.SyncQuicManager
+    if connection:
+        manager: contextlib.AbstractContextManager = contextlib.nullcontext(None)
+    else:
+        manager = dns.quic.SyncQuicManager(
+            verify_mode=verify, server_name=hostname, h3=True  # pyright: ignore
+        )
+        the_manager = manager  # for type checking happiness
 
     with manager:
-        connection = manager.connect(where, port, source, source_port)
+        if connection:
+            the_connection = connection
+        else:
+            the_connection = the_manager.connect(  # pyright: ignore
+                where, port, source, source_port
+            )
         (start, expiration) = _compute_times(timeout)
-        with connection.make_stream(timeout) as stream:
+        with the_connection.make_stream(timeout) as stream:  # pyright: ignore
             stream.send_h3(url, wire, post)
             wire = stream.receive(_remaining(expiration))
             _check_status(stream.headers(), where, wire)
@@ -1213,15 +1232,7 @@ def _tls_handshake(s, expiration):
 def _make_dot_ssl_context(
     server_hostname: Optional[str], verify: Union[bool, str]
 ) -> ssl.SSLContext:
-    cafile: Optional[str] = None
-    capath: Optional[str] = None
-    if isinstance(verify, str):
-        if os.path.isfile(verify):
-            cafile = verify
-        elif os.path.isdir(verify):
-            capath = verify
-        else:
-            raise ValueError("invalid verify string")
+    cafile, capath = dns._tls_util.convert_verify_to_cafile_and_capath(verify)
     ssl_context = ssl.create_default_context(cafile=cafile, capath=capath)
     ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
     if server_hostname is None:
@@ -1657,10 +1668,10 @@ def inbound_xfr(
     The default is 0.
 
     *udp_mode*, a ``dns.query.UDPMode``, determines how UDP is used
-    for IXFRs.  The default is ``dns.UDPMode.NEVER``, i.e. only use
-    TCP.  Other possibilities are ``dns.UDPMode.TRY_FIRST``, which
+    for IXFRs.  The default is ``dns.query.UDPMode.NEVER``, i.e. only use
+    TCP.  Other possibilities are ``dns.query.UDPMode.TRY_FIRST``, which
     means "try UDP but fallback to TCP if needed", and
-    ``dns.UDPMode.ONLY``, which means "try UDP and raise
+    ``dns.query.UDPMode.ONLY``, which means "try UDP and raise
     ``dns.xfr.UseTCP`` if it does not succeed.
 
     Raises on errors.
diff --git a/dns/quic/_common.py b/dns/quic/_common.py
index 930cf66..d21ceea 100644
--- a/dns/quic/_common.py
+++ b/dns/quic/_common.py
@@ -14,6 +14,7 @@ import aioquic.h3.events  # type: ignore
 import aioquic.quic.configuration  # type: ignore
 import aioquic.quic.connection  # type: ignore
 
+import dns._tls_util
 import dns.inet
 
 QUIC_MAX_DATAGRAM = 2048
@@ -245,7 +246,10 @@ class BaseQuicManager:
                 server_name=server_name,
             )
             if verify_path is not None:
-                conf.load_verify_locations(verify_path)
+                cafile, capath = dns._tls_util.convert_verify_to_cafile_and_capath(
+                    verify_path
+                )
+                conf.load_verify_locations(cafile=cafile, capath=capath)
         self._conf = conf
 
     def _connect(
diff --git a/dns/quic/_sync.py b/dns/quic/_sync.py
index d6e4128..18f9d05 100644
--- a/dns/quic/_sync.py
+++ b/dns/quic/_sync.py
@@ -133,25 +133,27 @@ class SyncQuicConnection(BaseQuicConnection):
 
     def _worker(self):
         try:
-            sel = selectors.DefaultSelector()
-            sel.register(self._socket, selectors.EVENT_READ, self._read)
-            sel.register(self._receive_wakeup, selectors.EVENT_READ, self._drain_wakeup)
-            while not self._done:
-                (expiration, interval) = self._get_timer_values(False)
-                items = sel.select(interval)
-                for key, _ in items:
-                    key.data()
-                with self._lock:
-                    self._handle_timer(expiration)
-                self._handle_events()
-                with self._lock:
-                    datagrams = self._connection.datagrams_to_send(time.time())
-                for datagram, _ in datagrams:
-                    try:
-                        self._socket.send(datagram)
-                    except BlockingIOError:
-                        # we let QUIC handle any lossage
-                        pass
+            with selectors.DefaultSelector() as sel:
+                sel.register(self._socket, selectors.EVENT_READ, self._read)
+                sel.register(
+                    self._receive_wakeup, selectors.EVENT_READ, self._drain_wakeup
+                )
+                while not self._done:
+                    (expiration, interval) = self._get_timer_values(False)
+                    items = sel.select(interval)
+                    for key, _ in items:
+                        key.data()
+                    with self._lock:
+                        self._handle_timer(expiration)
+                    self._handle_events()
+                    with self._lock:
+                        datagrams = self._connection.datagrams_to_send(time.time())
+                    for datagram, _ in datagrams:
+                        try:
+                            self._socket.send(datagram)
+                        except BlockingIOError:
+                            # we let QUIC handle any lossage
+                            pass
         except Exception:
             # Eat all exceptions as we have no way to pass them back to the
             # caller currently.  It might be nice to fix this in the future.
diff --git a/dns/rdata.py b/dns/rdata.py
index a305b8f..6e5a6f3 100644
--- a/dns/rdata.py
+++ b/dns/rdata.py
@@ -22,6 +22,7 @@ import binascii
 import inspect
 import io
 import itertools
+import ipaddress
 import random
 from importlib import import_module
 from typing import Any, Dict, Optional, Tuple, Union
@@ -551,6 +552,8 @@ class Rdata:
             return dns.ipv4.canonicalize(value)
         elif isinstance(value, bytes):
             return dns.ipv4.inet_ntoa(value)
+        elif isinstance(value, ipaddress.IPv4Address):
+            return dns.ipv4.inet_ntoa(value.packed)
         else:
             raise ValueError("not an IPv4 address")
 
@@ -560,6 +563,8 @@ class Rdata:
             return dns.ipv6.canonicalize(value)
         elif isinstance(value, bytes):
             return dns.ipv6.inet_ntoa(value)
+        elif isinstance(value, ipaddress.IPv6Address):
+            return dns.ipv6.inet_ntoa(value.packed)
         else:
             raise ValueError("not an IPv6 address")
 
@@ -752,6 +757,8 @@ def from_text(
     """
     if isinstance(tok, str):
         tok = dns.tokenizer.Tokenizer(tok, idna_codec=idna_codec)
+    if not isinstance(tok, dns.tokenizer.Tokenizer):
+        raise ValueError("tok must be a string or a Tokenizer")
     rdclass = dns.rdataclass.RdataClass.make(rdclass)
     rdtype = dns.rdatatype.RdataType.make(rdtype)
     cls = get_rdata_class(rdclass, rdtype)
diff --git a/dns/rdatatype.py b/dns/rdatatype.py
index aa9e561..f2d0eef 100644
--- a/dns/rdatatype.py
+++ b/dns/rdatatype.py
@@ -86,6 +86,7 @@ class RdataType(dns.enum.IntEnum):
     ZONEMD = 63
     SVCB = 64
     HTTPS = 65
+    DSYNC = 66
     SPF = 99
     UNSPEC = 103
     NID = 104
@@ -332,5 +333,6 @@ RESINFO = RdataType.RESINFO
 WALLET = RdataType.WALLET
 TA = RdataType.TA
 DLV = RdataType.DLV
+DSYNC = RdataType.DSYNC
 
 ### END generated RdataType constants
diff --git a/dns/rdtypes/ANY/DSYNC.py b/dns/rdtypes/ANY/DSYNC.py
new file mode 100644
index 0000000..42e4476
--- /dev/null
+++ b/dns/rdtypes/ANY/DSYNC.py
@@ -0,0 +1,74 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import struct
+
+import dns.enum
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+import dns.rdtypes.util
+
+
+class UnknownScheme(dns.exception.DNSException):
+    """Unknown DSYNC scheme"""
+
+
+class Scheme(dns.enum.IntEnum):
+    """DSYNC SCHEME"""
+
+    NOTIFY = 1
+
+    @classmethod
+    def _maximum(cls):
+        return 255
+
+    @classmethod
+    def _unknown_exception_class(cls):
+        return UnknownScheme
+
+
+@dns.immutable.immutable
+class DSYNC(dns.rdata.Rdata):
+    """DSYNC record"""
+
+    # see: draft-ietf-dnsop-generalized-notify
+
+    __slots__ = ["rrtype", "scheme", "port", "target"]
+
+    def __init__(self, rdclass, rdtype, rrtype, scheme, port, target):
+        super().__init__(rdclass, rdtype)
+        self.rrtype = self._as_rdatatype(rrtype)
+        self.scheme = Scheme.make(scheme)
+        self.port = self._as_uint16(port)
+        self.target = self._as_name(target)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        target = self.target.choose_relativity(origin, relativize)
+        return "%s %s %d %s" % (
+            dns.rdatatype.to_text(self.rrtype),
+            Scheme.to_text(self.scheme),
+            self.port,
+            target,
+        )
+
+    @classmethod
+    def from_text(
+        cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+    ):
+        rrtype = dns.rdatatype.from_text(tok.get_string())
+        scheme = Scheme.make(tok.get_string())
+        port = tok.get_uint16()
+        target = tok.get_name(origin, relativize, relativize_to)
+        return cls(rdclass, rdtype, rrtype, scheme, port, target)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        three_ints = struct.pack("!HBH", self.rrtype, self.scheme, self.port)
+        file.write(three_ints)
+        self.target.to_wire(file, None, origin, False)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        (rrtype, scheme, port) = parser.get_struct("!HBH")
+        target = parser.get_name(origin)
+        return cls(rdclass, rdtype, rrtype, scheme, port, target)
diff --git a/dns/rdtypes/ANY/__init__.py b/dns/rdtypes/ANY/__init__.py
index 647b215..cc39f86 100644
--- a/dns/rdtypes/ANY/__init__.py
+++ b/dns/rdtypes/ANY/__init__.py
@@ -31,6 +31,7 @@ __all__ = [
     "DNAME",
     "DNSKEY",
     "DS",
+    "DSYNC",
     "EUI48",
     "EUI64",
     "GPOS",
diff --git a/dns/resolver.py b/dns/resolver.py
index 1e23c0c..47e2556 100644
--- a/dns/resolver.py
+++ b/dns/resolver.py
@@ -318,6 +318,10 @@ class Answers(dict):
     """A dict of DNS stub resolver answers, indexed by type."""
 
 
+class EmptyHostAnswers(dns.exception.DNSException):
+    """The HostAnswers has no addresses"""
+
+
 class HostAnswers(Answers):
     """A dict of DNS stub resolver answers to a host name lookup, indexed by
     type.
@@ -364,6 +368,8 @@ class HostAnswers(Answers):
     # Returns the canonical name from this result.
     def canonical_name(self) -> dns.name.Name:
         answer = self.get(dns.rdatatype.AAAA, self.get(dns.rdatatype.A))
+        if answer is None:
+            raise EmptyHostAnswers
         return answer.canonical_name
 
 
@@ -1935,6 +1941,7 @@ def _getnameinfo(sockaddr, flags=0):
         pname = "udp"
     else:
         pname = "tcp"
+    assert isinstance(addr, str)
     qname = dns.reversename.from_address(addr)
     if flags & socket.NI_NUMERICHOST == 0:
         try:
@@ -2014,6 +2021,7 @@ def _gethostbyaddr(ip):
     bin_ip = dns.inet.inet_pton(family, ip)
     for item in tuples:
         addr = item[4][0]
+        assert isinstance(addr, str)
         bin_addr = dns.inet.inet_pton(family, addr)
         if bin_ip == bin_addr:
             addresses.append(addr)
diff --git a/examples/xfr.py b/examples/xfr.py
index 1c8175e..8f21e90 100755
--- a/examples/xfr.py
+++ b/examples/xfr.py
@@ -4,9 +4,13 @@ import dns.query
 import dns.resolver
 import dns.zone
 
+# Note that running this doesn't currently work because
+# dnspython.org's nameservers do not permit AXFR
+
 soa_answer = dns.resolver.resolve("dnspython.org", "SOA")
 master_answer = dns.resolver.resolve(soa_answer[0].mname, "A")
 
-z = dns.zone.from_xfr(dns.query.xfr(master_answer[0].address, "dnspython.org"))
+z = dns.zone.Zone("dnspython.org")
+dns.query.inbound_xfr(master_answer[0].address, z)
 for n in sorted(z.nodes.keys()):
     print(z[n].to_text(n))


Test patch:
diff --git a/tests/test_rdata.py b/tests/test_rdata.py
index c1d3416..6691fa7 100644
--- a/tests/test_rdata.py
+++ b/tests/test_rdata.py
@@ -17,6 +17,7 @@
 # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
 import io
+import ipaddress
 import operator
 import pickle
 import struct
@@ -29,6 +30,8 @@ import dns.rdataclass
 import dns.rdataset
 import dns.rdatatype
 import dns.rdtypes.ANY.RRSIG
+import dns.rdtypes.IN.A
+import dns.rdtypes.IN.AAAA
 import dns.rdtypes.IN.APL
 import dns.rdtypes.util
 import dns.tokenizer
@@ -65,6 +68,7 @@ class RdataTestCase(unittest.TestCase):
 
     def test_class_registration(self):
         CTXT = 64003
+
         class CTXTImp(dns.rdtypes.txtbase.TXTBase):
             """Test TXT-like record"""
 
@@ -896,6 +900,28 @@ class RdataTestCase(unittest.TestCase):
         self.assertEqual(rdata.next_name(), expected_rel)
         self.assertEqual(rdata.next_name(origin), expected_abs)
 
+    def test_a_rdata_made_multiple_ways(self):
+        r1 = dns.rdtypes.IN.A.A(dns.rdataclass.IN, dns.rdatatype.A, "1.2.3.4")
+        r2 = dns.rdtypes.IN.A.A(dns.rdataclass.IN, dns.rdatatype.A, b"\x01\x02\x03\x04")
+        r3 = dns.rdtypes.IN.A.A(
+            dns.rdataclass.IN, dns.rdatatype.A, ipaddress.ip_address("1.2.3.4")
+        )
+        self.assertEqual(r1, r2)
+        self.assertEqual(r1, r3)
+
+    def test_aaaa_rdata_made_multiple_ways(self):
+        r1 = dns.rdtypes.IN.AAAA.AAAA(dns.rdataclass.IN, dns.rdatatype.AAAA, "::1")
+        r2 = dns.rdtypes.IN.AAAA.AAAA(
+            dns.rdataclass.IN,
+            dns.rdatatype.AAAA,
+            b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01",
+        )
+        r3 = dns.rdtypes.IN.AAAA.AAAA(
+            dns.rdataclass.IN, dns.rdatatype.AAAA, ipaddress.ip_address("::1")
+        )
+        self.assertEqual(r1, r2)
+        self.assertEqual(r1, r3)
+
 
 class UtilTestCase(unittest.TestCase):
     def test_Gateway_bad_type0(self):
@@ -1005,6 +1031,15 @@ class UtilTestCase(unittest.TestCase):
             dns.rdatatype.RdataType.make("NSAP-PTR"), dns.rdatatype.NSAP_PTR
         )
 
+    def test_bad_DSYNC_text(self):
+        bad_dsync = [
+            "DSYNC CDS NOTIFY 5300 notify-endpoint.parent.net."
+            "DSYNC CDS 256 5300 notify-endpoint.parent.net."
+        ]
+        for text in bad_dsync:
+            with self.assertRaises(dns.exception.SyntaxError):
+                dns.rdata.from_text("in", "dsync", text)
+
 
 Rdata = dns.rdata.Rdata