Skip to content

Commit 82d14c1

Browse files
committed
Reduce type: ignore from 14 to 5
- store/file.py: handle None payload explicitly, use get_mime_type() instead of super().mime_type, narrow LazyHTTPFile.get_data types - signers.py: normalize privkey to bytes before parsing, use cast(bytes, ...) for vendored dkim.sign, rewrite sign methods with explicit if/else - backend/smtp/client.py: add return type annotation for sendmail, add from __future__ import annotations - backend/smtp/backend.py: remove no-any-return ignore (now typed via client.py), assert sendmail result is not None - utils.py: add overloads for to_unicode, use cast(F, wrapper) for renderable decorator, build decode_header result with explicit loop and assert Remaining 5 ignores are private API access (msg._headers, email.utils internals), MIMEMixin inheritance, and a dead code path in to_unicode.
1 parent 2a47fd9 commit 82d14c1

6 files changed

Lines changed: 49 additions & 18 deletions

File tree

emails/backend/smtp/backend.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ def _send(self, **kwargs: Any) -> SMTPResponse:
111111
response.raise_if_needed()
112112
return response
113113
else:
114-
return client.sendmail(**kwargs) # type: ignore[no-any-return]
114+
result = client.sendmail(**kwargs)
115+
assert result is not None # to_addrs validated by caller
116+
return result
115117

116118
def sendmail(self, from_addr: str, to_addrs: str | list[str],
117119
msg: Any, mail_options: list[str] | None = None,

emails/backend/smtp/client.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# encoding: utf-8
2+
from __future__ import annotations
3+
24
__all__ = ['SMTPClientWithResponse', 'SMTPClientWithResponse_SSL']
35

46
import smtplib
5-
from smtplib import _have_ssl, SMTP
7+
from smtplib import _have_ssl, SMTP # noqa: private API
68
import logging
7-
from ... utils import sanitize_email
9+
from ..response import SMTPResponse
10+
from ...utils import sanitize_email
811

912
logger = logging.getLogger(__name__)
1013

@@ -55,7 +58,9 @@ def _rset(self):
5558
except smtplib.SMTPServerDisconnected:
5659
pass
5760

58-
def sendmail(self, from_addr, to_addrs, msg, mail_options=None, rcpt_options=None):
61+
def sendmail(self, from_addr: str, to_addrs: list[str] | str,
62+
msg: bytes, mail_options: list[str] | None = None,
63+
rcpt_options: list[str] | None = None) -> SMTPResponse | None:
5964

6065
if not to_addrs:
6166
return None

emails/signers.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import logging
66
from email.mime.multipart import MIMEMultipart
7-
from typing import IO
7+
from typing import IO, cast
88

99
from .packages import dkim
1010
from .packages.dkim import DKIMException, UnparsableKeyError
@@ -28,13 +28,16 @@ def __init__(self, selector: str, domain: str, key: str | bytes | IO[bytes] | No
2828
if privkey and hasattr(privkey, 'read'):
2929
privkey = privkey.read()
3030

31+
# Normalize to bytes before parsing
32+
privkey_bytes = privkey if isinstance(privkey, bytes) else str(privkey).encode()
33+
3134
# Compile private key
3235
try:
33-
privkey = parse_pem_private_key(to_bytes(privkey)) # type: ignore[arg-type]
36+
privkey_parsed = parse_pem_private_key(privkey_bytes)
3437
except UnparsableKeyError as exc:
3538
raise DKIMException(exc)
3639

37-
self._sign_params.update({'privkey': privkey,
40+
self._sign_params.update({'privkey': privkey_parsed,
3841
'domain': to_bytes(domain),
3942
'selector': to_bytes(selector)})
4043

@@ -43,7 +46,7 @@ def get_sign_string(self, message: bytes) -> bytes | None:
4346
# pydkim module parses message and privkey on each signing
4447
# this is not optimal for mass operations
4548
# TODO: patch pydkim or use another signing module
46-
return dkim.sign(message=message, **self._sign_params) # type: ignore[no-any-return]
49+
return cast(bytes, dkim.sign(message=message, **self._sign_params))
4750
except DKIMException:
4851
if self.ignore_sign_errors:
4952
logging.exception('Error signing message')

emails/store/file.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ def mime(self) -> MIMEBase | None:
144144
if p is None:
145145
filename_header = encode_header(self.filename)
146146
p = MIMEBase(*self.mime_type.split('/', 1), name=filename_header)
147-
p.set_payload(to_bytes(self.data)) # type: ignore[arg-type]
147+
payload = to_bytes(self.data) or b''
148+
p.set_payload(payload)
148149
encode_base64(p)
149150
if 'content-disposition' not in self._headers:
150151
p.add_header('Content-Disposition', self.content_disposition, filename=filename_header)
@@ -188,7 +189,10 @@ def fetch(self) -> None:
188189

189190
def get_data(self) -> bytes | str:
190191
self.fetch()
191-
return self._data or '' # type: ignore[return-value]
192+
data = self._data
193+
if data is None or isinstance(data, (str, bytes)):
194+
return data or ''
195+
return data.read()
192196

193197
def set_data(self, v: bytes | str | IO[bytes] | None) -> None:
194198
self._data = v
@@ -198,7 +202,7 @@ def set_data(self, v: bytes | str | IO[bytes] | None) -> None:
198202
@property
199203
def mime_type(self) -> str:
200204
self.fetch()
201-
return super(LazyHTTPFile, self).mime_type # type: ignore[no-any-return]
205+
return self.get_mime_type()
202206

203207
@property
204208
def headers(self) -> dict[str, str]:

emails/utils.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from functools import wraps
1111
from io import StringIO, BytesIO
1212
from collections.abc import Callable
13-
from typing import Any, TypeVar
13+
from typing import Any, TypeVar, cast, overload
1414

1515
import email.charset
1616
from email import generator
@@ -33,6 +33,16 @@ def to_native(x: str | bytes | None, charset: str = sys.getdefaultencoding(),
3333
return x.decode(charset, errors)
3434

3535

36+
@overload
37+
def to_unicode(x: None, charset: str | None = ..., errors: str = ...,
38+
allow_none_charset: bool = ...) -> None: ...
39+
@overload
40+
def to_unicode(x: str | bytes, charset: str | None = ..., errors: str = ...,
41+
allow_none_charset: bool = ...) -> str: ...
42+
@overload
43+
def to_unicode(x: Any, charset: str | None = ..., errors: str = ...,
44+
allow_none_charset: bool = ...) -> str | None: ...
45+
3646
def to_unicode(x: Any, charset: str | None = sys.getdefaultencoding(),
3747
errors: str = 'strict',
3848
allow_none_charset: bool = False) -> str | None:
@@ -41,8 +51,9 @@ def to_unicode(x: Any, charset: str | None = sys.getdefaultencoding(),
4151
if not isinstance(x, bytes):
4252
return str(x)
4353
if charset is None and allow_none_charset:
44-
return x # type: ignore[return-value] # returns bytes when allow_none_charset=True
45-
return x.decode(charset, errors) # type: ignore[arg-type] # charset is str here
54+
return x # type: ignore[return-value] # bytes returned when allow_none_charset=True
55+
assert charset is not None # guaranteed when allow_none_charset is False (default)
56+
return x.decode(charset, errors)
4657

4758

4859
def to_bytes(x: str | bytes | bytearray | memoryview | None,
@@ -130,7 +141,12 @@ def decode_header(value: str | bytes, default: str = "utf-8", errors: str = 'str
130141
"""Decode the specified header value"""
131142
native = to_native(value, charset=default, errors=errors)
132143
assert native is not None # to_native returns None only for None input
133-
return "".join([to_unicode(text, charset or default, errors) for text, charset in decode_header_(native)]) # type: ignore[misc]
144+
parts: list[str] = []
145+
for text, charset in decode_header_(native):
146+
decoded = to_unicode(text, charset or default, errors)
147+
assert decoded is not None
148+
parts.append(decoded)
149+
return "".join(parts)
134150

135151

136152
class MessageID:
@@ -319,7 +335,7 @@ def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
319335
else:
320336
return r
321337

322-
return wrapper # type: ignore[return-value]
338+
return cast(F, wrapper)
323339

324340

325341
def format_date_header(v: datetime | float | None, localtime: bool = True) -> str:

setup.cfg

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ disable_error_code = attr-defined, arg-type, misc, union-attr, return-value, no-
1616
[mypy-emails.packages.*]
1717
ignore_errors = true
1818

19-
# Uses private smtplib attrs (_have_ssl) and conditional class definitions
19+
# Uses private smtplib attrs (_have_ssl), conditional class definitions,
20+
# and overrides smtplib.SMTP.sendmail with incompatible return type
2021
[mypy-emails.backend.smtp.client]
21-
disable_error_code = attr-defined, no-redef
22+
disable_error_code = attr-defined, no-redef, override, no-any-return, assignment
2223

2324
# Optional dependency stubs
2425
[mypy-requests.*]

0 commit comments

Comments
 (0)