Skip to content

Commit 40a0b35

Browse files
chore: add type-hints to imapclient/config.py
1 parent 7d1a91e commit 40a0b35

File tree

3 files changed

+42
-20
lines changed

3 files changed

+42
-20
lines changed

imapclient/config.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99
import ssl
1010
import urllib.parse
1111
import urllib.request
12+
from typing import Any, Callable, Dict, Optional, Tuple, TYPE_CHECKING, TypeVar
1213

1314
import imapclient
1415

1516

16-
def getenv(name, default):
17+
def getenv(name: str, default: Optional[str]) -> Optional[str]:
1718
return os.environ.get("imapclient_" + name, default)
1819

1920

20-
def get_config_defaults():
21+
def get_config_defaults() -> Dict[str, Any]:
2122
return {
2223
"username": getenv("username", None),
2324
"password": getenv("password", None),
@@ -36,7 +37,7 @@ def get_config_defaults():
3637
}
3738

3839

39-
def parse_config_file(filename):
40+
def parse_config_file(filename: str) -> argparse.Namespace:
4041
"""Parse INI files containing IMAP connection details.
4142
4243
Used by livetest.py and interact.py
@@ -51,12 +52,13 @@ def parse_config_file(filename):
5152

5253
conf.alternates = {}
5354
for section in parser.sections():
55+
# pylint: disable=no-member
5456
conf.alternates[section] = _read_config_section(parser, section)
5557

5658
return conf
5759

5860

59-
def get_string_config_defaults():
61+
def get_string_config_defaults() -> Dict[str, str]:
6062
out = {}
6163
for k, v in get_config_defaults().items():
6264
if v is True:
@@ -69,14 +71,19 @@ def get_string_config_defaults():
6971
return out
7072

7173

72-
def _read_config_section(parser, section):
73-
def get(name):
74+
T = TypeVar("T")
75+
76+
77+
def _read_config_section(
78+
parser: configparser.ConfigParser, section: str
79+
) -> argparse.Namespace:
80+
def get(name: str) -> str:
7481
return parser.get(section, name)
7582

76-
def getboolean(name):
83+
def getboolean(name: str) -> bool:
7784
return parser.getboolean(section, name)
7885

79-
def get_allowing_none(name, typefunc):
86+
def get_allowing_none(name: str, typefunc: Callable[[str], T]) -> Optional[T]:
8087
try:
8188
v = parser.get(section, name)
8289
except configparser.NoOptionError:
@@ -85,10 +92,10 @@ def get_allowing_none(name, typefunc):
8592
return None
8693
return typefunc(v)
8794

88-
def getint(name):
95+
def getint(name: str) -> Optional[int]:
8996
return get_allowing_none(name, int)
9097

91-
def getfloat(name):
98+
def getfloat(name: str) -> Optional[float]:
9299
return get_allowing_none(name, float)
93100

94101
ssl_ca_file = get("ssl_ca_file")
@@ -121,7 +128,9 @@ def getfloat(name):
121128
}
122129

123130

124-
def refresh_oauth2_token(hostname, client_id, client_secret, refresh_token):
131+
def refresh_oauth2_token(
132+
hostname: str, client_id: str, client_secret: str, refresh_token: str
133+
) -> str:
125134
url = OAUTH2_REFRESH_URLS.get(hostname)
126135
if not url:
127136
raise ValueError("don't know where to refresh OAUTH2 token for %r" % hostname)
@@ -136,14 +145,19 @@ def refresh_oauth2_token(hostname, client_id, client_secret, refresh_token):
136145
url, urllib.parse.urlencode(post).encode("ascii")
137146
) as request:
138147
response = request.read()
139-
return json.loads(response.decode("ascii"))["access_token"]
148+
result = json.loads(response.decode("ascii"))["access_token"]
149+
if TYPE_CHECKING:
150+
assert isinstance(result, str)
151+
return result
140152

141153

142154
# Tokens are expensive to refresh so use the same one for the duration of the process.
143-
_oauth2_cache = {}
155+
_oauth2_cache: Dict[Tuple[str, str, str, str], str] = {}
144156

145157

146-
def get_oauth2_token(hostname, client_id, client_secret, refresh_token):
158+
def get_oauth2_token(
159+
hostname: str, client_id: str, client_secret: str, refresh_token: str
160+
) -> str:
147161
cache_key = (hostname, client_id, client_secret, refresh_token)
148162
token = _oauth2_cache.get(cache_key)
149163
if token:
@@ -154,7 +168,9 @@ def get_oauth2_token(hostname, client_id, client_secret, refresh_token):
154168
return token
155169

156170

157-
def create_client_from_config(conf, login=True):
171+
def create_client_from_config(
172+
conf: argparse.Namespace, login: bool = True
173+
) -> imapclient.IMAPClient:
158174
assert conf.host, "missing host"
159175

160176
ssl_context = None

imapclient/imapclient.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import re
1010
import select
1111
import socket
12+
import ssl as ssl_lib
1213
import sys
1314
import warnings
1415
from datetime import date, datetime
@@ -247,7 +248,7 @@ def __init__(
247248
use_uid: bool = True,
248249
ssl: bool = True,
249250
stream: bool = False,
250-
ssl_context: Optional[str] = None,
251+
ssl_context: Optional[ssl_lib.SSLContext] = None,
251252
timeout: Optional[float] = None,
252253
):
253254
if stream:
@@ -386,7 +387,7 @@ def starttls(self, ssl_context=None):
386387
self._imap.file = self._imap.sock.makefile("rb")
387388
return data[0]
388389

389-
def login(self, username, password):
390+
def login(self, username: str, password: str):
390391
"""Login using *username* and *password*, returning the
391392
server response.
392393
"""
@@ -403,7 +404,13 @@ def login(self, username, password):
403404
logger.debug("Logged in as %s", username)
404405
return rv
405406

406-
def oauth2_login(self, user, access_token, mech="XOAUTH2", vendor=None):
407+
def oauth2_login(
408+
self,
409+
user: str,
410+
access_token: str,
411+
mech: str = "XOAUTH2",
412+
vendor: Optional[str] = None,
413+
):
407414
"""Authenticate using the OAUTH2 or XOAUTH2 methods.
408415
409416
Gmail and Yahoo both support the 'XOAUTH2' mechanism, but Yahoo requires
@@ -517,7 +524,7 @@ def logout(self):
517524
logger.debug("Logged out, connection closed")
518525
return data[0]
519526

520-
def shutdown(self):
527+
def shutdown(self) -> None:
521528
"""Close the connection to the IMAP server (without logging out)
522529
523530
In most cases, :py:meth:`.logout` should be used instead of

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ warn_unused_ignores = true
4747
# Overrides for currently untyped modules
4848
[[tool.mypy.overrides]]
4949
module = [
50-
"imapclient.config",
5150
"imapclient.imapclient",
5251
"imapclient.interact",
5352
"interact",

0 commit comments

Comments
 (0)