Skip to content

Add pycares 5.0 support with backward-compatible result types#218

Merged
bdraco merged 54 commits into
masterfrom
pycares_5
Jan 8, 2026
Merged

Add pycares 5.0 support with backward-compatible result types#218
bdraco merged 54 commits into
masterfrom
pycares_5

Conversation

@bdraco
Copy link
Copy Markdown
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
Copy Markdown

codecov Bot commented Dec 11, 2025

Codecov Report

❌ Patch coverage is 98.30918% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.36%. Comparing base (1067970) to head (9980ea4).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
tests/test_aiodns.py 97.50% 7 Missing and 1 partial ⚠️
aiodns/__init__.py 94.87% 4 Missing ⚠️
aiodns/compat.py 98.43% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #218      +/-   ##
==========================================
+ Coverage   97.69%   98.36%   +0.66%     
==========================================
  Files           3        5       +2     
  Lines         564     1345     +781     
  Branches       38       70      +32     
==========================================
+ Hits          551     1323     +772     
- Misses          7       16       +9     
  Partials        6        6              

☔ 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.

@saghul
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Contributor

saghul commented Dec 11, 2025

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

@Dreamsorcerer
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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:
        ...

@saghul
Copy link
Copy Markdown
Contributor

saghul commented Jan 8, 2026

Happy new year everyone!

My thoughts on the matter. Since people are going to need to migrate eventually, IMHO the best path forward is a new major version with breaking changes, which depends on pycares >=5,<6.

People that want to wait can pin to aiodns 3.

@bdraco
Copy link
Copy Markdown
Member Author

bdraco commented Jan 8, 2026

The challenge for us is that Home Assistant has 18+ libraries that depend on aiodns. A clean break means all of them have to update simultaneously before we can upgrade, and we can't get pycares 5.x until that happens (blocking any security fixes, performance improvements, etc). Realistically that takes 6-12 months of coordination, and some libraries inevitably get abandoned along the way and need replacing.

With backward compat we can upgrade aiodns/pycares immediately and let libraries migrate at their own pace over time. No blocking dependency chain.

If the preference is a clean break, we'd likely need to fork or find an alternative solution for Home Assistant, which isn't ideal for either project.

@saghul
Copy link
Copy Markdown
Contributor

saghul commented Jan 8, 2026

I see. Fair enough. Let's go with your proposal then.

@bdraco
Copy link
Copy Markdown
Member Author

bdraco commented Jan 8, 2026

Thank you

@bdraco
Copy link
Copy Markdown
Member Author

bdraco commented Jan 8, 2026

Now I need to retest everywhere in HA

@bdraco
Copy link
Copy Markdown
Member Author

bdraco commented Jan 8, 2026

didn't find anything else. tested everything I could think of.

@bdraco bdraco merged commit 2a65ab2 into master Jan 8, 2026
24 checks passed
@bdraco bdraco deleted the pycares_5 branch January 8, 2026 21:41
enoch85 added a commit to enoch85/EffektGuard that referenced this pull request Jan 8, 2026
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