Skip to content

Commit 97caa94

Browse files
authored
Merge pull request #537 from JohnVillalovos/jlvillal/mypy_setup
Add type-hints to `setup.py` and `imapclient/config.py`. Also use `argparse.Namespace` instead of `Bunch`
2 parents 332935d + 40a0b35 commit 97caa94

File tree

4 files changed

+50
-38
lines changed

4 files changed

+50
-38
lines changed

imapclient/config.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@
22
# Released subject to the New BSD License
33
# Please see http://en.wikipedia.org/wiki/BSD_licenses
44

5+
import argparse
56
import configparser
67
import json
8+
import os
79
import ssl
810
import urllib.parse
911
import urllib.request
10-
from os import environ, path
12+
from typing import Any, Callable, Dict, Optional, Tuple, TYPE_CHECKING, TypeVar
1113

1214
import imapclient
1315

1416

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

1820

19-
def get_config_defaults():
21+
def get_config_defaults() -> Dict[str, Any]:
2022
return {
2123
"username": getenv("username", None),
2224
"password": getenv("password", None),
@@ -35,7 +37,7 @@ def get_config_defaults():
3537
}
3638

3739

38-
def parse_config_file(filename):
40+
def parse_config_file(filename: str) -> argparse.Namespace:
3941
"""Parse INI files containing IMAP connection details.
4042
4143
Used by livetest.py and interact.py
@@ -50,12 +52,13 @@ def parse_config_file(filename):
5052

5153
conf.alternates = {}
5254
for section in parser.sections():
55+
# pylint: disable=no-member
5356
conf.alternates[section] = _read_config_section(parser, section)
5457

5558
return conf
5659

5760

58-
def get_string_config_defaults():
61+
def get_string_config_defaults() -> Dict[str, str]:
5962
out = {}
6063
for k, v in get_config_defaults().items():
6164
if v is True:
@@ -68,14 +71,19 @@ def get_string_config_defaults():
6871
return out
6972

7073

71-
def _read_config_section(parser, section):
72-
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:
7381
return parser.get(section, name)
7482

75-
def getboolean(name):
83+
def getboolean(name: str) -> bool:
7684
return parser.getboolean(section, name)
7785

78-
def get_allowing_none(name, typefunc):
86+
def get_allowing_none(name: str, typefunc: Callable[[str], T]) -> Optional[T]:
7987
try:
8088
v = parser.get(section, name)
8189
except configparser.NoOptionError:
@@ -84,17 +92,17 @@ def get_allowing_none(name, typefunc):
8492
return None
8593
return typefunc(v)
8694

87-
def getint(name):
95+
def getint(name: str) -> Optional[int]:
8896
return get_allowing_none(name, int)
8997

90-
def getfloat(name):
98+
def getfloat(name: str) -> Optional[float]:
9199
return get_allowing_none(name, float)
92100

93101
ssl_ca_file = get("ssl_ca_file")
94102
if ssl_ca_file:
95-
ssl_ca_file = path.expanduser(ssl_ca_file)
103+
ssl_ca_file = os.path.expanduser(ssl_ca_file)
96104

97-
return Bunch(
105+
return argparse.Namespace(
98106
host=get("host"),
99107
port=getint("port"),
100108
ssl=getboolean("ssl"),
@@ -120,7 +128,9 @@ def getfloat(name):
120128
}
121129

122130

123-
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:
124134
url = OAUTH2_REFRESH_URLS.get(hostname)
125135
if not url:
126136
raise ValueError("don't know where to refresh OAUTH2 token for %r" % hostname)
@@ -135,14 +145,19 @@ def refresh_oauth2_token(hostname, client_id, client_secret, refresh_token):
135145
url, urllib.parse.urlencode(post).encode("ascii")
136146
) as request:
137147
response = request.read()
138-
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
139152

140153

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

144157

145-
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:
146161
cache_key = (hostname, client_id, client_secret, refresh_token)
147162
token = _oauth2_cache.get(cache_key)
148163
if token:
@@ -153,7 +168,9 @@ def get_oauth2_token(hostname, client_id, client_secret, refresh_token):
153168
return token
154169

155170

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

159176
ssl_context = None
@@ -200,14 +217,3 @@ def create_client_from_config(conf, login=True):
200217
except: # noqa: E722
201218
client.shutdown()
202219
raise
203-
204-
205-
class Bunch(dict):
206-
def __getattr__(self, k):
207-
try:
208-
return self[k]
209-
except KeyError:
210-
raise AttributeError
211-
212-
def __setattr__(self, k, v):
213-
self[k] = v

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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,10 @@ 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",
5453
"livetest",
55-
"setup",
5654
]
5755
ignore_errors = true
5856

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
# Please see http://en.wikipedia.org/wiki/BSD_licenses
66

77
from os import path
8+
from typing import Dict
89

9-
from setuptools import setup
10+
from setuptools import setup # type: ignore[import]
1011

1112
# Read version info
1213
here = path.dirname(__file__)
1314
version_file = path.join(here, "imapclient", "version.py")
14-
info = {}
15+
info: Dict[str, str] = {}
1516
exec(open(version_file).read(), {}, info)
1617

1718
desc = """\

0 commit comments

Comments
 (0)