From: Bob Halley Date: Tue, 16 Jun 2026 23:29:12 +0000 (-0700) Subject: switch from httpx to httpx2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=a06c144616e058d65e8dbfe3394c5f7b277fd768;p=thirdparty%2Fdnspython.git switch from httpx to httpx2 --- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 845c552c..198ebfcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/dns/_asyncio_backend.py b/dns/_asyncio_backend.py index 83163182..0b8a6b9b 100644 --- a/dns/_asyncio_backend.py +++ b/dns/_asyncio_backend.py @@ -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, diff --git a/dns/_features.py b/dns/_features.py index c2b32862..697cbf4a 100644 --- a/dns/_features.py +++ b/dns/_features.py @@ -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"], diff --git a/dns/_trio_backend.py b/dns/_trio_backend.py index 686d37ad..a547c0e7 100644 --- a/dns/_trio_backend.py +++ b/dns/_trio_backend.py @@ -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, diff --git a/dns/asyncquery.py b/dns/asyncquery.py index e6ab2e3d..d1731a2c 100644 --- a/dns/asyncquery.py +++ b/dns/asyncquery.py @@ -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, diff --git a/dns/query.py b/dns/query.py index ac982c94..46f0f13f 100644 --- a/dns/query.py +++ b/dns/query.py @@ -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, diff --git a/doc/conf.py b/doc/conf.py index 075550d3..921e09df 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -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"), ] diff --git a/doc/installation.rst b/doc/installation.rst index 35b46ae2..8c218f5e 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -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. diff --git a/doc/whatsnew.rst b/doc/whatsnew.rst index 3ac56c1b..7deac03f 100644 --- a/doc/whatsnew.rst +++ b/doc/whatsnew.rst @@ -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. diff --git a/examples/doh-json.py b/examples/doh-json.py index bad85bb9..3c57ee1a 100755 --- a/examples/doh-json.py +++ b/examples/doh-json.py @@ -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}, diff --git a/examples/doh.py b/examples/doh.py index 2fd44ff3..8adf9a03 100755 --- a/examples/doh.py +++ b/examples/doh.py @@ -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: diff --git a/pyproject.toml b/pyproject.toml index d0685bee..e68bd003 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/tests/test_async.py b/tests/test_async.py index 0ec72fe3..24767a93 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -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(): diff --git a/tests/test_doh.py b/tests/test_doh.py index 2743dee4..b4433b6f 100644 --- a/tests/test_doh.py +++ b/tests/test_doh.py @@ -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(