Skip to content

Conversation

@bdraco
Copy link
Member

@bdraco bdraco commented Dec 11, 2025

What do these changes do?

Add support for pycares 5.0 while maintaining full backward compatibility with existing code.

Key changes:

  • New aiodns/compat.py module with frozen dataclasses matching pycares 4.x field names
  • New query_dns() method returning native pycares 5.x DNSResult (with access to answer/authority/additional sections)
  • query() method deprecated (emits DeprecationWarning) but continues to work with pycares 4.x compatible results
  • Uses pycares 5.x event_thread by default (falls back to sock_state_cb on error)
  • gethostbyname() now uses getaddrinfo() internally (pycares 5 removed gethostbyname)
  • nameservers property strips port suffix added by pycares 5.x for backward compatibility
  • Exports result types: AresQueryAResult, AresQueryAAAAResult, AresQueryCNAMEResult, AresQueryMXResult, AresQueryNSResult, AresQueryTXTResult, AresQuerySOAResult, AresQuerySRVResult, AresQueryNAPTRResult, AresQueryCAAResult, AresQueryPTRResult, AresHostResult

Migration path

# Old API (deprecated but still works)
result = await resolver.query('example.com', 'MX')
for record in result:
    print(record.host, record.priority, record.ttl)

# New API (recommended)
result = await resolver.query_dns('example.com', 'MX')
for record in result.answer:
    print(record.data.exchange, record.data.priority, record.ttl)

Future migration to aiodns 5.x

The temporary query_dns() naming allows gradual migration without breaking changes:

Version query() query_dns()
4.x Deprecated, returns compat types New API, returns pycares 5.x types
5.x New API, returns pycares 5.x types Alias to query() for back compat

In aiodns 5.x, query() will become the primary API returning native pycares 5.x types, and query_dns() will remain as an alias for backward compatibility. This allows downstream projects to migrate at their own pace.

Field mappings (pycares 5.x → aiodns compat)

Record pycares 5.x aiodns compat
A/AAAA data.addr host
MX data.exchange host
NS data.nsdname host
TXT data.data text
SOA mname/rname/expire/minimum nsname/hostmaster/expires/minttl
SRV data.target host
CAA data.tag property
PTR data.dname name

Are there changes in behavior for the user?

Backward compatible - existing code continues to work unchanged (with deprecation warning on query()).

Version bumped to 4.0.0 due to:

  • Result types from query() are now aiodns dataclasses instead of pycares types
  • Dropped Python 3.9 support (minimum is now Python 3.10)
  • Dropped PyPy support (pycares 5.0 doesn't build on PyPy)

The query() type change is unlikely to affect anyone since checking isinstance(result, pycares.ares_query_*_result) was uncommon. Field access patterns remain identical to pycares 4.x.

Users can migrate to query_dns() at their own pace while staying on aiodns 4.x, avoiding coordinated dependency upgrades.

Related issue number

Fixes #214

Checklist

  • I think the code is well written
  • Unit tests for the changes exist
  • Documentation reflects the changes

@codecov
Copy link

codecov bot commented Dec 11, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.05%. Comparing base (1067970) to head (2858193).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #218      +/-   ##
==========================================
+ Coverage   97.69%   99.05%   +1.35%     
==========================================
  Files           3        5       +2     
  Lines         564     1266     +702     
  Branches       38       69      +31     
==========================================
+ Hits          551     1254     +703     
  Misses          7        7              
+ Partials        6        5       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bdraco
Copy link
Member Author

bdraco commented Dec 11, 2025

Tested with HomeAssistant all good

@saghul
Copy link
Contributor

saghul commented Dec 11, 2025

If we are bumping the version to 4 I see no point in retaining backwards compatibility.

We lose important data like the TTL for all query types.

@bdraco
Copy link
Member Author

bdraco commented Dec 11, 2025

If we are bumping the version to 4 I see no point in retaining backwards compatibility.

We lose important data like the TTL for all query types.

TLDR: We should change it so they get TTL, etc it but after a bridge release to make it easier for downstream to coordinate all the needed changes which should hopefully shave of a few months (or years if other libs are slow) to the migration process (HA has libs using pycares/aiodns that it does not control are will be a the mercy of the maintainers of them to get them updated).

I think we should keep the 4.x API compatible while adding pycares 5.x support. Since we didn't have a hard pin on the pycares major version, users could end up with pycares 5.x installed alongside code expecting the old aiodns API.

Having a bridge release (aiodns 4.x with pycares 5.x support but the same API) ensures nobody gets stuck.

Home Assistant is a concrete example: we have several dependencies that use the aiodns query API. With this approach, we can release aiodns 5.x with breaking changes whenever it's ready, and Home Assistant can stay on 4.x until all downstream deps are updated. This avoids a giant coordinated merge and complex dependency bump - each dep can migrate independently on their own timeline.

To give an idea of the scope of the problem, Home Assistant has 18 deps that use aiodns that will need to be updated, and some of them are subdeps. All of that will need to be coordinated. When we had the update for pydantic 2.x from 1.x it took nearly two years to coordinate all the downstreams. While this will be a little bit better, it will still be VERY painful.

@bdraco
Copy link
Member Author

bdraco commented Dec 11, 2025

Thinking about it a bit more, we could introduce a new api for modern query with a new name, deprecate query. Than we give downgrade months or years to migrate. Than we don't end up in a place where we have multiple years before downstream could use a 5.x version

@saghul
Copy link
Contributor

saghul commented Dec 11, 2025

If we are bumping the version to 4 I see no point in retaining backwards compatibility.

We lose important data like the TTL for all query types.

TLDR: We should change it so they get TTL, etc it but after a bridge release to make it easier for downstream to coordinate all the needed changes which should hopefully shave of a few months (or years if other libs are slow) to the migration process (HA has libs using pycares/aiodns that it does not control are will be a the mercy of the maintainers of them to get them updated).

I think we should keep the 4.x API compatible while adding pycares 5.x support. Since we didn't have a hard pin on the pycares major version, users could end up with pycares 5.x installed alongside code expecting the old aiodns API.

Having a bridge release (aiodns 4.x with pycares 5.x support but the same API) ensures nobody gets stuck.

Home Assistant is a concrete example: we have several dependencies that use the aiodns query API. With this approach, we can release aiodns 5.x with breaking changes whenever it's ready, and Home Assistant can stay on 4.x until all downstream deps are updated. This avoids a giant coordinated merge and complex dependency bump - each dep can migrate independently on their own timeline.

To give an idea of the scope of the problem, Home Assistant has 18 deps that use aiodns that will need to be updated, and some of them are subdeps. All of that will need to be coordinated. When we had the update for pydantic 2.x from 1.x it took nearly two years to coordinate all the downstreams. While this will be a little bit better, it will still be VERY painful.

Fair enough.

@saghul
Copy link
Contributor

saghul commented Dec 11, 2025

Thinking about it a bit more, we could introduce a new api for modern query with a new name, deprecate query. Than we give downgrade months or years to migrate. Than we don't end up in a place where we have multiple years before downstream could use a 5.x version

I like this less since "query" is the obvious API to use...

@bdraco
Copy link
Member Author

bdraco commented Dec 11, 2025

Thinking about it a bit more, we could introduce a new api for modern query with a new name, deprecate query. Than we give downgrade months or years to migrate. Than we don't end up in a place where we have multiple years before downstream could use a 5.x version

I like this less since "query" is the obvious API to use...

I added query_dns which uses the new API and kept query backwards compat with a deprecation warning to use query_dns.... While the bridge release solves the pycares/aiodns update coordination, it doesn't solve the hard break in the aiodns API and I couldn't come up with a way that avoids having to update all the aiodns deps that use .query at once without adding a new API.

If you have a better idea, I'm more than happy to execute on it.

@bdraco
Copy link
Member Author

bdraco commented Dec 11, 2025

With this approach, I think in 5.x we could drop the old query method, rename query_dns to query, have an alias for back compat. The temporary naming seems like a minor inconvenience compared to the coordination nightmare of breaking changes.

@bdraco bdraco marked this pull request as ready for review December 11, 2025 19:39
@bdraco
Copy link
Member Author

bdraco commented Dec 11, 2025

I updated the PR summary and docs to reflect that approach.

Version query() query_dns()
4.x Deprecated, returns compat types New API, returns pycares 5.x types
5.x New API, returns pycares 5.x types Alias to query() for back compat

@bdraco
Copy link
Member Author

bdraco commented Dec 11, 2025

I'm happy with this now. I've tested it many places downstream. It solves all the current issues at hand.

@saghul
Copy link
Contributor

saghul commented Dec 11, 2025

Yeah, if we want to support both APIs for a while this makes sense.

What feels a bit weird is that when we drop query, there will only be query_dns, which is odd.

How about having a dedicated function per query type?

query_a, query_soa, query_mx ... this way the query_xxx are the new ones and the old query() remains the old one. When we remove it, it's obvious why the others are called the way they are.

Thoughts?

return AresQueryNSResult(host=ns_data.nsdname, ttl=ttl)
if record_type == pycares.QUERY_TYPE_TXT:
txt_data = cast(pycares.TXTRecordData, record.data)
return AresQueryTXTResult(text=txt_data.data, ttl=ttl)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that pycares4 returned str and the new one does bytes.

Migrating from aiodns 3.x
=========================

aiodns 4.x introduces a new ``query_dns()`` method that returns native pycares 5.x result types.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a link to the pycares docs where the result types are described would help?

__all__ = (
'AresHostResult',
'AresQueryAAAAResult',
# Compatibility types for pycares 4.x API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird comment location? do we want to re-export the pycares return types?

raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG)
except ModuleNotFoundError as ex:
raise RuntimeError(WINDOWS_SELECTOR_ERR_MSG) from ex
# Use weak reference to avoid preventing garbage collection
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary?

self._channel.gethostbyname(host, family, cb)
) -> asyncio.Future[AresHostResult]:
"""
Resolve hostname to addresses.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a deprecation warning here and remove it in the next mayor version.

@bdraco
Copy link
Member Author

bdraco commented Dec 11, 2025

Yeah, if we want to support both APIs for a while this makes sense.

What feels a bit weird is that when we drop query, there will only be query_dns, which is odd.

How about having a dedicated function per query type?

query_a, query_soa, query_mx ... this way the query_xxx are the new ones and the old query() remains the old one. When we remove it, it's obvious why the others are called the way they are.

Thoughts?

The plan for 5.x is to rename query_dns() back to query() (with query_dns() kept as an alias for back compat). So we'd end up with the natural query() name as the primary API.

Type specific methods like query_a(), query_mx() could be a bit awkward for downstream users who build DNS tools where the record type is user-selectable:

With query_dns it works like this:

query_type = user_input  # "A", "MX", "TXT", etc.
result = await resolver.query_dns(host, query_type)

and than its back to query for 5.x

With type specific we get something a bit awkward like:

method = getattr(resolver, f"query_{query_type.lower()}")
result = await method(host)

That said, type-specific methods could be added later as convenience wrappers as well?

@saghul
Copy link
Contributor

saghul commented Dec 11, 2025

The plan for 5.x is to rename query_dns() back to query() (with query_dns() kept as an alias for back compat). So we'd end up with the natural query() name as the primary API.

That means people need to update their code twice if they want to use the new API. I find that more incovenient than the alternative.

Type specific methods like query_a(), query_mx() could be a bit awkward for downstream users who build DNS tools where the record type is user-selectable:

It's a simple mapping, I don't think it would be a big deal.

Having to change the code twice to use the new stuff sounds worse IMHO.

@bdraco
Copy link
Member Author

bdraco commented Dec 11, 2025

Fair point on the double migration.

Though with query_dns(), users only need to change once, query_dns() remains as a permanent alias in 5.x, so the second change to query() is optional. They can keep using query_dns() forever if they prefer.

But I see the appeal of type-specific methods being a cleaner one-time migration. I'm okay either way. What do you think is the better path forward?

@saghul
Copy link
Contributor

saghul commented Dec 11, 2025

Let's sleep on it :-) Maybe others want to weigh in too?

@Dreamsorcerer
Copy link
Member

With type specific we get something a bit awkward like:

method = getattr(resolver, f"query_{query_type.lower()}")
result = await method(host)

I would have to agree that would be a bit awkward, I'd probably want to avoid that. It's also very easy to lose type safety if you're trying to access these functions dynamically like that. Convenience methods in addition to the general query function (like in aiohttp) would be fine though.

It doesn't sound like there's a perfect solution available, but I'd be inclined to go with bdraco's proposal currently, unless any better ideas appear.

One other idea I can think of (I'm not really for or against it though) is tweaking the function signature in some way. For example, if the new version used an enum instead of strings, then we can use the same function and just switch the implementation depending on whether the argument is str or enum (which could be cleanly implemented with functools.singledispatchmethod decorator).

@Dreamsorcerer
Copy link
Member

Also, it looks like the new version has lost the overloads we had for each query type. It looks like we'll need to rework the typing in pycares 5 to make that work again.

@Dreamsorcerer
Copy link
Member

pycares should probably have similar overloads for the callback here:
https://github.com/saghul/pycares/blob/3e517e429602be4ab6fac5ea430ef89f229c34af/src/pycares/__init__.py#L728

That way it can verify the callback expects to receive the specific type it will return, like:

    @overload
    def query(self, name: str, query_type: QUERY_TYPE_A, *, query_class: int = ..., callback: Callable[[DNSAResult, int], None]) -> None:
        ...
    @overload
    def query(self, name: str, query_type: QUERY_TYPE_MX, *, query_class: int = ..., callback: Callable[[DNSMXResult, int], None]) -> None:
        ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incompatible with Pycares v5.0.0

4 participants