]> git.ipfire.org Git - thirdparty/dnspython.git/commitdiff
switch from httpx to httpx2
authorBob Halley <halley@dnspython.org>
Tue, 16 Jun 2026 23:29:12 +0000 (16:29 -0700)
committerBob Halley <halley@dnspython.org>
Tue, 16 Jun 2026 23:29:12 +0000 (16:29 -0700)
14 files changed:
.github/workflows/ci.yml
dns/_asyncio_backend.py
dns/_features.py
dns/_trio_backend.py
dns/asyncquery.py
dns/query.py
doc/conf.py
doc/installation.rst
doc/whatsnew.rst
examples/doh-json.py
examples/doh.py
pyproject.toml
tests/test_async.py
tests/test_doh.py

index 845c552c1c10ab7c92b35997d491f50ef70bc2cf..198ebfcde867c31d090a82cde709eb38f21de1f1 100644 (file)
@@ -51,7 +51,7 @@ jobs:
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
-        python -m pip install cryptography trio idna httpx h2 aioquic pytest ruff pyright ty
+        python -m pip install cryptography trio idna httpx2 h2 aioquic pytest ruff pyright ty
     - name: Typecheck
       run: |
         pyright dns
index 831631828ba6c15d4bbc25931a1b9af403e0631b..0b8a6b9b6ec9c9a831f3129de6511fe9f691eb77 100644 (file)
@@ -115,12 +115,12 @@ class _StreamSocket(dns._asyncbackend.StreamSocket):
 
 if dns._features.have("doh"):
     import anyio
-    import httpcore
-    import httpcore._backends.anyio
-    import httpx
+    import httpcore2
+    import httpcore2._backends.anyio
+    import httpx2
 
-    _CoreAsyncNetworkBackend = httpcore.AsyncNetworkBackend
-    _CoreAnyIOStream = httpcore._backends.anyio.AnyIOStream  # pyright: ignore
+    _CoreAsyncNetworkBackend = httpcore2.AsyncNetworkBackend
+    _CoreAnyIOStream = httpcore2._backends.anyio.AnyIOStream  # pyright: ignore
 
     from dns.query import _compute_times, _expiration_for_this_attempt, _remaining
 
@@ -133,7 +133,7 @@ if dns._features.have("doh"):
             self._family = family
             if local_port != 0:
                 raise NotImplementedError(
-                    "the asyncio transport for HTTPX cannot set the local port"
+                    "the asyncio transport for httpx2 cannot set the local port"
                 )
 
         async def connect_tcp(
@@ -167,7 +167,7 @@ if dns._features.have("doh"):
                     return _CoreAnyIOStream(stream)
                 except Exception:
                     pass
-            raise httpcore.ConnectError
+            raise httpcore2.ConnectError
 
         async def connect_unix_socket(
             self, path, timeout=None, socket_options=None
@@ -177,7 +177,7 @@ if dns._features.have("doh"):
         async def sleep(self, seconds):  # pylint: disable=signature-differs
             await anyio.sleep(seconds)
 
-    class _HTTPTransport(httpx.AsyncHTTPTransport):
+    class _HTTPTransport(httpx2.AsyncHTTPTransport):
         def __init__(
             self,
             *args,
index c2b3286237514dbcb02a8b60cbd3e6397cde12f3..697cbf4a31d9e452aa2c8839abf7f7ecef7d6a8a 100644 (file)
@@ -85,7 +85,7 @@ def force(feature: str, enabled: bool) -> None:
 _requirements: dict[str, list[str]] = {
     ### BEGIN generated requirements
     "dnssec": ["cryptography>=45"],
-    "doh": ["httpcore>=1.0.0", "httpx>=0.28.0", "h2>=4.2.0"],
+    "doh": ["httpcore2>=2.4", "httpx2>=2.4", "h2>=4.3.0"],
     "doq": ["aioquic>=1.2.0"],
     "idna": ["idna>=3.10"],
     "trio": ["trio>=0.30"],
index 686d37ad4e8074208ce435313b39ca744d449843..a547c0e70c381b430be6b8b191401d8123bf23f4 100644 (file)
@@ -103,12 +103,12 @@ class StreamSocket(dns._asyncbackend.StreamSocket):
 
 
 if dns._features.have("doh"):
-    import httpcore
-    import httpcore._backends.trio
-    import httpx
+    import httpcore2
+    import httpcore2._backends.trio
+    import httpx2
 
-    _CoreAsyncNetworkBackend = httpcore.AsyncNetworkBackend
-    _CoreTrioStream = httpcore._backends.trio.TrioStream
+    _CoreAsyncNetworkBackend = httpcore2.AsyncNetworkBackend
+    _CoreTrioStream = httpcore2._backends.trio.TrioStream
 
     from dns.query import _compute_times, _expiration_for_this_attempt, _remaining
 
@@ -155,7 +155,7 @@ if dns._features.have("doh"):
                     return _CoreTrioStream(sock.stream)
                 except Exception:
                     continue
-            raise httpcore.ConnectError
+            raise httpcore2.ConnectError
 
         async def connect_unix_socket(
             self, path, timeout=None, socket_options=None
@@ -165,7 +165,7 @@ if dns._features.have("doh"):
         async def sleep(self, seconds):  # pylint: disable=signature-differs
             await trio.sleep(seconds)
 
-    class _HTTPTransport(httpx.AsyncHTTPTransport):
+    class _HTTPTransport(httpx2.AsyncHTTPTransport):
         def __init__(
             self,
             *args,
index e6ab2e3d0daef600d93a07e28e9486a9b8944e49..d1731a2c3edd6ba1a9a635c8bb1247c20d5642fd 100644 (file)
@@ -57,7 +57,7 @@ except ImportError:
     import dns._no_ssl as ssl  # pyright: ignore
 
 if have_doh:
-    import httpx
+    import httpx2
 
 # for brevity
 _lltuple = dns.inet.low_level_address_tuple
@@ -540,7 +540,7 @@ async def https(
     source_port: int = 0,  # pylint: disable=W0613
     one_rr_per_rrset: bool = False,
     ignore_trailing: bool = False,
-    client: "httpx.AsyncClient|dns.quic.AsyncQuicConnection | None" = None,
+    client: "httpx2.AsyncClient|dns.quic.AsyncQuicConnection | None" = None,
     path: str = "/dns-query",
     post: bool = True,
     verify: bool | str | ssl.SSLContext = True,
@@ -553,8 +553,8 @@ async def https(
 
     :param client: If provided, the client to use for the query. Unlike the
         other dnspython async functions, a backend cannot be provided here
-        because httpx always auto-detects the async backend.
-    :type client: ``httpx.AsyncClient`` or ``None``
+        because httpx2 always auto-detects the async backend.
+    :type client: ``httpx2.AsyncClient`` or ``None``
 
     See :py:func:`dns.query.https()` for the documentation of the other
     parameters, exceptions, and return type of this method.
@@ -617,8 +617,8 @@ async def https(
     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("client parameter must be an httpx.AsyncClient")
+    if client and not isinstance(client, httpx2.AsyncClient):  # pyright: ignore
+        raise ValueError("client parameter must be an httpx2.AsyncClient")
     # pylint: enable=possibly-used-before-assignment
 
     wire = q.to_wire()
@@ -650,7 +650,7 @@ async def https(
             family=family,
         )
 
-        cm = httpx.AsyncClient(  # pyright: ignore
+        cm = httpx2.AsyncClient(  # pyright: ignore
             http1=h1, http2=h2, verify=verify, transport=transport  # type: ignore
         )
 
@@ -675,7 +675,7 @@ async def https(
             )
         else:
             wire = base64.urlsafe_b64encode(wire).rstrip(b"=")
-            twire = wire.decode()  # httpx does a repr() if we give it bytes
+            twire = wire.decode()  # httpx2 does a repr() if we give it bytes
             response = await backend.wait_for(
                 the_client.get(  # pyright: ignore
                     url,
index ac982c94e7645778623aff972f82b095b304525a..46f0f13fc8d97634589969c9f14905bb21ea8d0c 100644 (file)
@@ -66,13 +66,13 @@ def _expiration_for_this_attempt(timeout, expiration):
     return min(time.time() + timeout, expiration)
 
 
-_have_httpx = dns._features.have("doh")
-if _have_httpx:
-    import httpcore._backends.sync
-    import httpx
+_have_httpx2 = dns._features.have("doh")
+if _have_httpx2:
+    import httpcore2._backends.sync
+    import httpx2
 
-    _CoreNetworkBackend = httpcore.NetworkBackend
-    _CoreSyncStream = httpcore._backends.sync.SyncStream
+    _CoreNetworkBackend = httpcore2.NetworkBackend
+    _CoreSyncStream = httpcore2._backends.sync.SyncStream
 
     class _NetworkBackend(_CoreNetworkBackend):
         def __init__(self, resolver, local_port, bootstrap_address, family):
@@ -121,14 +121,14 @@ if _have_httpx:
                     return _CoreSyncStream(sock)
                 except Exception:
                     pass
-            raise httpcore.ConnectError
+            raise httpcore2.ConnectError
 
         def connect_unix_socket(
             self, path, timeout=None, socket_options=None
         ):  # pylint: disable=signature-differs
             raise NotImplementedError
 
-    class _HTTPTransport(httpx.HTTPTransport):  # pyright: ignore
+    class _HTTPTransport(httpx2.HTTPTransport):  # pyright: ignore
         def __init__(
             self,
             *args,
@@ -166,7 +166,7 @@ else:
             raise NotImplementedError
 
 
-have_doh = _have_httpx
+have_doh = _have_httpx2
 
 
 def default_socket_factory(
@@ -193,7 +193,7 @@ class BadResponse(dns.exception.FormError):
 
 
 class NoDOH(dns.exception.DNSException):
-    """DNS over HTTPS (DOH) was requested but the httpx module is not
+    """DNS over HTTPS (DOH) was requested but the httpx2 module is not
     available."""
 
 
@@ -471,7 +471,7 @@ def https(
     :type ignore_trailing: bool
     :param session: If provided, the client session to use to send the
         queries.
-    :type session: ``httpx.Client`` or ``None``
+    :type session: ``httpx2.Client`` or ``None``
     :param path: If *where* is an IP address, *path* is used to construct the
         query URL.
     :type path: str
@@ -486,7 +486,7 @@ def https(
     :param resolver: Resolver to use for hostname resolution in URLs.  If
         ``None``, a new resolver with default configuration is used (not the
         default resolver, to avoid a DoH chicken-and-egg problem).  Only
-        effective when using httpx.
+        effective when using httpx2.
     :type resolver: :py:class:`dns.resolver.Resolver` or ``None``
     :param family: Address family.  ``socket.AF_UNSPEC`` (the default)
         retrieves both A and AAAA records.
@@ -544,8 +544,8 @@ def https(
 
     if not have_doh:
         raise NoDOH  # pragma: no cover
-    if session and not isinstance(session, httpx.Client):  # pyright: ignore
-        raise ValueError("session parameter must be an httpx.Client")
+    if session and not isinstance(session, httpx2.Client):  # pyright: ignore
+        raise ValueError("session parameter must be an httpx2.Client")
 
     wire = q.to_wire()
     headers = {"accept": "application/dns-message"}
@@ -576,7 +576,7 @@ def https(
             family=family,  # pyright: ignore
         )
 
-        cm = httpx.Client(  # pyright: ignore
+        cm = httpx2.Client(  # pyright: ignore
             http1=h1, http2=h2, verify=verify, transport=transport  # type: ignore
         )
     with cm as session:
@@ -599,7 +599,7 @@ def https(
             )
         else:
             wire = base64.urlsafe_b64encode(wire).rstrip(b"=")
-            twire = wire.decode()  # httpx does a repr() if we give it bytes
+            twire = wire.decode()  # httpx2 does a repr() if we give it bytes
             response = session.get(
                 url,
                 headers=headers,
index 075550d3ceae3973fb1c2ef9d0396d43fedfc8e7..921e09df50c1ffa6c88f11d27386907884cab30f 100644 (file)
@@ -42,7 +42,7 @@ extensions = [
 
 intersphinx_mapping = {
     "python": ("https://docs.python.org/3", None),
-    "cryptography": ("https://cryptography.io/en/latest/", None)
+    "cryptography": ("https://cryptography.io/en/latest/", None),
 }
 
 nitpick_ignore = [
@@ -68,7 +68,7 @@ nitpick_ignore = [
     # socket module class (from dns.query sock parameters)
     ("py:class", "socket"),
     # External libraries not in intersphinx
-    ("py:class", "httpx.AsyncClient"),
+    ("py:class", "httpx2.AsyncClient"),
     ("py:class", "dns.quic.AsyncQuicConnection"),
 ]
 
index 35b46ae2edfbe120dffd306500653c37b5f77a2a..8c218f5e96979b7870fc6d00eba728539598ac28 100644 (file)
@@ -45,7 +45,7 @@ Optional Modules
 
 The following modules are optional, but recommended for full functionality.
 
-If ``httpx`` is installed, then DNS-over-HTTPS will be available.
+If ``httpx2`` is installed, then DNS-over-HTTPS will be available.
 
 If ``cryptography`` is installed, then dnspython will be
 able to do low-level DNSSEC signature generation and validation.
index 3ac56c1b5ae4ffeb7ac1a309f6d315fd7077b182..7deac03fe97cd954dfd8d20645df75781b2597c5 100644 (file)
@@ -72,7 +72,7 @@ TBD
   currently dns.message.CopyMode.QUESTION for all opcodes.
 
 * If an IP address is used as the hostname in a URL, the https query code now passes
-  the sni_hostname to httpx as this is required to get httpx to validate the certificate
+  the sni_hostname to httpx2 as this is required to get httpx2 to validate the certificate
   and check for an IP subject alternative name.
 
 * The minimum supported aioquic version is now 1.0.0.
@@ -101,7 +101,7 @@ TBD
 
 * Dnspython now looks for version metadata for optional packages and will not
   use them if they are too old.  This prevents possible exceptions when a
-  feature like DoH is not desired in dnspython, but an old httpx is installed
+  feature like DoH is not desired in dnspython, but an old httpx2 is installed
   along with dnspython for some other purpose.
 
 * The DoHNameserver class now allows GET to be used instead of the default POST,
@@ -197,8 +197,8 @@ TBD
 
 * The DNS-over-HTTPS bootstrap address no longer causes URL rewriting.
 
-* DNS-over-HTTPS now only uses httpx; support for requests has been dropped.  A source
-  port may now be supplied when using httpx.
+* DNS-over-HTTPS now only uses httpx2; support for requests has been dropped.  A source
+  port may now be supplied when using httpx2.
 
 * DNSSEC zone signing with NSEC records is now supported. Thank you
   very much (again!) Jakob Schlyter!
@@ -292,7 +292,7 @@ This release has no new features, but fixes the following issues:
   an error trace like the NoNameservers exception.  This class is a subclass of
   dns.exception.Timeout for backwards compatibility.
 
-* DNS-over-HTTPS will try to use HTTP/2 if the httpx and h2 packages
+* DNS-over-HTTPS will try to use HTTP/2 if the httpx2 and h2 packages
   are installed.
 
 * DNS-over-HTTPS is now supported for asynchronous queries and resolutions.
index bad85bb9b08f4c45ddc332a0cffec5c87f7eeced..3c57ee1a8149511d73d370d6af7a1487d4ea3bff 100755 (executable)
@@ -3,7 +3,7 @@
 import copy
 import json
 
-import httpx
+import httpx2
 
 import dns.flags
 import dns.message
@@ -93,7 +93,7 @@ def from_doh_simple(simple, add_qr=False):
 a = dns.resolver.resolve("www.dnspython.org", "a")
 p = to_doh_simple(a.response)
 print(json.dumps(p, indent=4))
-response = httpx.get(
+response = httpx2.get(
     "https://dns.google/resolve?",
     verify=True,
     params={"name": "www.dnspython.org", "type": 1},
index 2fd44ff3b02c55bed1fb6d51e28ab4c564aff6a9..8adf9a03842952d6f70150dbc6f65a9b4e143fc8 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/env python3
 #
 # This is an example of sending DNS queries over HTTPS (DoH) with dnspython.
-import httpx
+import httpx2
 
 import dns.message
 import dns.query
@@ -11,7 +11,7 @@ import dns.rdatatype
 def main():
     where = "https://dns.google/dns-query"
     qname = "example.com."
-    with httpx.Client() as client:
+    with httpx2.Client() as client:
         q = dns.message.make_query(qname, dns.rdatatype.A)
         r = dns.query.https(q, where, session=client)
         for answer in r.answer:
index d0685beea01e6ed319b738f4a666330cf0646ae0..e68bd003d7ec6ee28e935c12c3240bbc0659d95f 100644 (file)
@@ -43,7 +43,7 @@ dev = [
     "ty>=0.0.14",
 ]
 dnssec = ["cryptography>=46"]
-doh = ["httpcore>=1.0.0", "httpx>=0.28.0", "h2>=4.3.0"]
+doh = ["httpcore22>=2.4", "httpx2>=2.4", "h2>=4.3.0"]
 doq = ["aioquic>=1.3.0"]
 idna = ["idna>=3.11"]
 trio = ["trio>=0.30"]
index 0ec72fe32ec06c927f53429c3f68cf8c362fd2f2..24767a935b8e7a9758e5d25490141d13151a747f 100644 (file)
@@ -554,7 +554,7 @@ class AsyncTests(unittest.TestCase):
 
         self.assertRaises(dns.exception.Timeout, run)
 
-    @unittest.skipIf(not dns.query._have_httpx, "httpx not available")
+    @unittest.skipIf(not dns.query._have_httpx2, "httpx2 not available")
     @tests.util.retry_on_timeout
     def testDOHGetRequest(self):
         async def run():
@@ -567,7 +567,7 @@ class AsyncTests(unittest.TestCase):
 
         self.async_run(run)
 
-    @unittest.skipIf(not dns.query._have_httpx, "httpx not available")
+    @unittest.skipIf(not dns.query._have_httpx2, "httpx2 not available")
     @tests.util.retry_on_timeout
     def testDOHPostRequest(self):
         async def run():
@@ -633,7 +633,7 @@ class AsyncTests(unittest.TestCase):
 
         self.async_run(run)
 
-    @unittest.skipIf(not dns.query._have_httpx, "httpx not available")
+    @unittest.skipIf(not dns.query._have_httpx2, "httpx2 not available")
     @tests.util.retry_on_timeout
     def testResolverDOH(self):
         async def run():
index 2743dee4a94632682c668d8bece7e86360d90ca0..b4433b6ff099971e7f36e16b83259e7522759552 100644 (file)
@@ -34,8 +34,8 @@ import dns.quic
 import dns.rdatatype
 import dns.resolver
 
-if dns.query._have_httpx:
-    import httpx
+if dns.query._have_httpx2:
+    import httpx2
 
 import tests.util
 
@@ -80,12 +80,12 @@ KNOWN_PAD_AWARE_DOH_RESOLVER_URLS = [
 
 
 @unittest.skipUnless(
-    dns.query._have_httpx and tests.util.is_internet_reachable() and _have_ssl,
-    "Python httpx cannot be imported; no DNS over HTTPS (DOH)",
+    dns.query._have_httpx2 and tests.util.is_internet_reachable() and _have_ssl,
+    "Python httpx2 cannot be imported; no DNS over HTTPS (DOH)",
 )
-class DNSOverHTTPSTestCaseHttpx(unittest.TestCase):
+class DNSOverHTTPSTestCasehttpx2(unittest.TestCase):
     def setUp(self):
-        self.session = httpx.Client(http1=True, http2=True, verify=True)
+        self.session = httpx2.Client(http1=True, http2=True, verify=True)
 
     def tearDown(self):
         self.session.close()
@@ -152,7 +152,7 @@ class DNSOverHTTPSTestCaseHttpx(unittest.TestCase):
     #         q = dns.message.make_query("example.com.", dns.rdatatype.A)
     #         # make sure CleanBrowsing's IP address will fail TLS certificate
     #         # check.
-    #         with self.assertRaises(httpx.ConnectError):
+    #         with self.assertRaises(httpx2.ConnectError):
     #             dns.query.https(q, invalid_tls_url, session=self.session, timeout=4)
     #         # And if we don't mangle the URL, it should work.
     #         r = dns.query.https(