Skip to content

Commit 5c4b29c

Browse files
Strict typing (#138)
Co-authored-by: J. Nick Koston <[email protected]>
1 parent 928b8a6 commit 5c4b29c

File tree

7 files changed

+166
-87
lines changed

7 files changed

+166
-87
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
run: |
3131
pip install .
3232
- name: Run mypy
33-
run: python mypy_run.py
33+
run: mypy
3434
- name: Prepare twine checker
3535
run: |
3636
pip install -U build twine wheel

.mypy.ini

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[mypy]
2+
files = aiodns, tests
3+
check_untyped_defs = True
4+
follow_imports_for_stubs = True
5+
disallow_any_decorated = True
6+
disallow_any_generics = True
7+
disallow_any_unimported = True
8+
disallow_incomplete_defs = True
9+
disallow_subclassing_any = True
10+
disallow_untyped_calls = True
11+
disallow_untyped_decorators = True
12+
disallow_untyped_defs = True
13+
# TODO(PY312): explicit-override
14+
enable_error_code = deprecated, ignore-without-code, possibly-undefined, redundant-expr, redundant-self, truthy-bool, truthy-iterable, unused-awaitable
15+
extra_checks = True
16+
follow_untyped_imports = True
17+
implicit_reexport = False
18+
no_implicit_optional = True
19+
pretty = True
20+
show_column_numbers = True
21+
show_error_codes = True
22+
show_error_code_links = True
23+
strict_equality = True
24+
warn_incomplete_stub = True
25+
warn_redundant_casts = True
26+
warn_return_any = True
27+
warn_unreachable = True
28+
warn_unused_ignores = True

aiodns/__init__.py

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
21
import asyncio
32
import functools
4-
import pycares
53
import socket
64
import sys
5+
from collections.abc import Iterable, Sequence
6+
from typing import Any, Literal, Optional, TypeVar, Union, overload
77

8+
import pycares
89
from typing import (
910
Any,
1011
Callable,
@@ -22,6 +23,8 @@
2223

2324
__all__ = ('DNSResolver', 'error')
2425

26+
_T = TypeVar("_T")
27+
2528
WINDOWS_SELECTOR_ERR_MSG = (
2629
"aiodns needs a SelectorEventLoop on Windows. See more: "
2730
"https://github.com/aio-libs/aiodns#note-for-windows-users"
@@ -55,7 +58,7 @@
5558
class DNSResolver:
5659
def __init__(self, nameservers: Optional[Sequence[str]] = None,
5760
loop: Optional[asyncio.AbstractEventLoop] = None,
58-
**kwargs: Any) -> None:
61+
**kwargs: Any) -> None: # TODO(PY311): Use Unpack for kwargs.
5962
self.loop = loop or asyncio.get_event_loop()
6063
assert self.loop is not None
6164
kwargs.pop('sock_state_cb', None)
@@ -80,31 +83,33 @@ def __init__(self, nameservers: Optional[Sequence[str]] = None,
8083
**kwargs)
8184
if nameservers:
8285
self.nameservers = nameservers
83-
self._read_fds = set() # type: Set[int]
84-
self._write_fds = set() # type: Set[int]
85-
self._timer = None # type: Optional[asyncio.TimerHandle]
86+
self._read_fds: set[int] = set()
87+
self._write_fds: set[int] = set()
88+
self._timer: Optional[asyncio.TimerHandle] = None
8689

8790
@property
8891
def nameservers(self) -> Sequence[str]:
8992
return self._channel.servers
9093

9194
@nameservers.setter
92-
def nameservers(self, value: Sequence[str]) -> None:
93-
self._channel.servers = value if isinstance(value, list) else list(value)
95+
def nameservers(self, value: Iterable[Union[str, bytes]]) -> None:
96+
# Remove type ignore after mypy 1.16.0
97+
# https://github.com/python/mypy/issues/12892
98+
self._channel.servers = value # type: ignore[assignment]
9499

95100
@staticmethod
96-
def _callback(fut: asyncio.Future, result: Any, errorno: int) -> None:
101+
def _callback(fut: asyncio.Future[_T], result: _T, errorno: Optional[int]) -> None:
97102
if fut.cancelled():
98103
return
99104
if errorno is not None:
100105
fut.set_exception(error.DNSError(errorno, pycares.errno.strerror(errorno)))
101106
else:
102107
fut.set_result(result)
103108

104-
def _get_future_callback(self) -> Tuple["asyncio.Future[Any]", Callable[[Any, int], None]]:
109+
def _get_future_callback(self) -> Tuple["asyncio.Future[_T]", Callable[[_T, int], None]]:
105110
"""Return a future and a callback to set the result of the future."""
106-
cb: Callable[[Any, int], None]
107-
future: "asyncio.Future[Any]" = self.loop.create_future()
111+
cb: Callable[[_T, int], None]
112+
future: "asyncio.Future[_T]" = self.loop.create_future()
108113
if self._event_thread:
109114
cb = functools.partial( # type: ignore[assignment]
110115
self.loop.call_soon_threadsafe,
@@ -115,7 +120,41 @@ def _get_future_callback(self) -> Tuple["asyncio.Future[Any]", Callable[[Any, in
115120
cb = functools.partial(self._callback, future)
116121
return future, cb
117122

118-
def query(self, host: str, qtype: str, qclass: Optional[str]=None) -> asyncio.Future:
123+
@overload
124+
def query(self, host: str, qtype: Literal["A"], qclass: Optional[str] = ...) -> asyncio.Future[list[pycares.ares_query_a_result]]:
125+
...
126+
@overload
127+
def query(self, host: str, qtype: Literal["AAAA"], qclass: Optional[str] = ...) -> asyncio.Future[list[pycares.ares_query_aaaa_result]]:
128+
...
129+
@overload
130+
def query(self, host: str, qtype: Literal["CAA"], qclass: Optional[str] = ...) -> asyncio.Future[list[pycares.ares_query_caa_result]]:
131+
...
132+
@overload
133+
def query(self, host: str, qtype: Literal["CNAME"], qclass: Optional[str] = ...) -> asyncio.Future[list[pycares.ares_query_cname_result]]:
134+
...
135+
@overload
136+
def query(self, host: str, qtype: Literal["MX"], qclass: Optional[str] = ...) -> asyncio.Future[list[pycares.ares_query_mx_result]]:
137+
...
138+
@overload
139+
def query(self, host: str, qtype: Literal["NAPTR"], qclass: Optional[str] = ...) -> asyncio.Future[list[pycares.ares_query_naptr_result]]:
140+
...
141+
@overload
142+
def query(self, host: str, qtype: Literal["NS"], qclass: Optional[str] = ...) -> asyncio.Future[list[pycares.ares_query_ns_result]]:
143+
...
144+
@overload
145+
def query(self, host: str, qtype: Literal["PTR"], qclass: Optional[str] = ...) -> asyncio.Future[list[pycares.ares_query_ptr_result]]:
146+
...
147+
@overload
148+
def query(self, host: str, qtype: Literal["SOA"], qclass: Optional[str] = ...) -> asyncio.Future[list[pycares.ares_query_soa_result]]:
149+
...
150+
@overload
151+
def query(self, host: str, qtype: Literal["SRV"], qclass: Optional[str] = ...) -> asyncio.Future[list[pycares.ares_query_srv_result]]:
152+
...
153+
@overload
154+
def query(self, host: str, qtype: Literal["TXT"], qclass: Optional[str] = ...) -> asyncio.Future[list[pycares.ares_query_txt_result]]:
155+
...
156+
157+
def query(self, host: str, qtype: str, qclass: Optional[str]=None) -> asyncio.Future[list[Any]]:
119158
try:
120159
qtype = query_type_map[qtype]
121160
except KeyError:
@@ -126,30 +165,35 @@ def query(self, host: str, qtype: str, qclass: Optional[str]=None) -> asyncio.Fu
126165
except KeyError:
127166
raise ValueError('invalid query class: {}'.format(qclass))
128167

168+
fut: asyncio.Future[list[Any]]
129169
fut, cb = self._get_future_callback()
130170
self._channel.query(host, qtype, cb, query_class=qclass)
131171
return fut
132172

133-
def gethostbyname(self, host: str, family: socket.AddressFamily) -> asyncio.Future:
173+
def gethostbyname(self, host: str, family: socket.AddressFamily) -> asyncio.Future[pycares.ares_host_result]:
174+
fut: asyncio.Future[pycares.ares_host_result]
134175
fut, cb = self._get_future_callback()
135176
self._channel.gethostbyname(host, family, cb)
136177
return fut
137178

138-
def getaddrinfo(self, host: str, family: socket.AddressFamily = socket.AF_UNSPEC, port: Optional[int] = None, proto: int = 0, type: int = 0, flags: int = 0) -> asyncio.Future:
179+
def getaddrinfo(self, host: str, family: socket.AddressFamily = socket.AF_UNSPEC, port: Optional[int] = None, proto: int = 0, type: int = 0, flags: int = 0) -> asyncio.Future[pycares.ares_addrinfo_result]:
180+
fut: asyncio.Future[pycares.ares_addrinfo_result]
139181
fut, cb = self._get_future_callback()
140182
self._channel.getaddrinfo(host, port, cb, family=family, type=type, proto=proto, flags=flags)
141183
return fut
142184

143-
def getnameinfo(self, sockaddr: Union[Tuple[str, int], Tuple[str, int, int, int]], flags: int = 0) -> asyncio.Future:
185+
def getnameinfo(self, sockaddr: Union[tuple[str, int], tuple[str, int, int, int]], flags: int = 0) -> asyncio.Future[pycares.ares_nameinfo_result]:
186+
fut: asyncio.Future[pycares.ares_nameinfo_result]
144187
fut, cb = self._get_future_callback()
145188
self._channel.getnameinfo(sockaddr, flags, cb)
146189
return fut
147190

148-
def gethostbyaddr(self, name: str) -> asyncio.Future:
191+
def gethostbyaddr(self, name: str) -> asyncio.Future[pycares.ares_host_result]:
192+
fut: asyncio.Future[pycares.ares_host_result]
149193
fut, cb = self._get_future_callback()
150194
self._channel.gethostbyaddr(name, cb)
151195
return fut
152-
196+
153197
def cancel(self) -> None:
154198
self._channel.cancel()
155199

@@ -177,7 +221,7 @@ def _sock_state_cb(self, fd: int, readable: bool, writable: bool) -> None:
177221
self._timer.cancel()
178222
self._timer = None
179223

180-
def _handle_event(self, fd: int, event: Any) -> None:
224+
def _handle_event(self, fd: int, event: int) -> None:
181225
read_fd = pycares.ARES_SOCKET_BAD
182226
write_fd = pycares.ARES_SOCKET_BAD
183227
if event == READ:
@@ -193,7 +237,7 @@ def _timer_cb(self) -> None:
193237
else:
194238
self._timer = None
195239

196-
def _start_timer(self):
240+
def _start_timer(self) -> None:
197241
timeout = self._timeout
198242
if timeout is None or timeout < 0 or timeout > 1:
199243
timeout = 1

aiodns/error.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
1-
2-
import pycares
3-
4-
for code, name in pycares.errno.errorcode.items():
5-
globals()[name] = code
1+
from pycares.errno import (
2+
ARES_SUCCESS as ARES_SUCCESS,
3+
ARES_ENODATA as ARES_ENODATA,
4+
ARES_EFORMERR as ARES_EFORMERR,
5+
ARES_ESERVFAIL as ARES_ESERVFAIL,
6+
ARES_ENOTFOUND as ARES_ENOTFOUND,
7+
ARES_ENOTIMP as ARES_ENOTIMP,
8+
ARES_EREFUSED as ARES_EREFUSED,
9+
ARES_EBADQUERY as ARES_EBADQUERY,
10+
ARES_EBADNAME as ARES_EBADNAME,
11+
ARES_EBADFAMILY as ARES_EBADFAMILY,
12+
ARES_EBADRESP as ARES_EBADRESP,
13+
ARES_ECONNREFUSED as ARES_ECONNREFUSED,
14+
ARES_ETIMEOUT as ARES_ETIMEOUT,
15+
ARES_EOF as ARES_EOF,
16+
ARES_EFILE as ARES_EFILE,
17+
ARES_ENOMEM as ARES_ENOMEM,
18+
ARES_EDESTRUCTION as ARES_EDESTRUCTION,
19+
ARES_EBADSTR as ARES_EBADSTR,
20+
ARES_EBADFLAGS as ARES_EBADFLAGS,
21+
ARES_ENONAME as ARES_ENONAME,
22+
ARES_EBADHINTS as ARES_EBADHINTS,
23+
ARES_ENOTINITIALIZED as ARES_ENOTINITIALIZED,
24+
ARES_ELOADIPHLPAPI as ARES_ELOADIPHLPAPI,
25+
ARES_EADDRGETNETWORKPARAMS as ARES_EADDRGETNETWORKPARAMS,
26+
ARES_ECANCELLED as ARES_ECANCELLED,
27+
ARES_ESERVICE as ARES_ESERVICE
28+
)
629

730

831
class DNSError(Exception):
932
pass
10-

mypy.ini

Lines changed: 0 additions & 2 deletions
This file was deleted.

mypy_run.py

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)