Skip to content

Fix type hints, add mypy to CI, fix as_bytes DKIM bug#194

Merged
lavr merged 16 commits intomasterfrom
fix/type-hints-review-fixes
Mar 31, 2026
Merged

Fix type hints, add mypy to CI, fix as_bytes DKIM bug#194
lavr merged 16 commits intomasterfrom
fix/type-hints-review-fixes

Conversation

@lavr
Copy link
Copy Markdown
Owner

@lavr lavr commented Mar 31, 2026

Summary

  • Fix type annotations from review: py.typed now shipped in package, date callback type widened, SMTPResponse fields corrected (refused_recipients, esmtp_opts/rcpt_options as lists, status_text as bytes)
  • Add mypy to CI: configured in setup.cfg with per-module overrides for mixin pattern and vendored code; new tox -e typecheck env and GitHub Actions job
  • Reduce # type: ignore from 26 to 14: replaced suppressions with proper isinstance checks, asserts, and direct attribute access where possible
  • Fix as_bytes() DKIM bug: as_bytes() called sign_string() (expects str) instead of sign_bytes() (expects bytes), causing TypeError when DKIM signing was enabled
  • Add 10 new tests: BaseFile.get_data() with all data types, SMTPResponse attributes, DKIM as_bytes()

Test plan

  • mypy emails/ passes with 0 errors
  • 82 tests pass locally
  • CI passes

lavr added 3 commits March 31, 2026 17:31
1. Include py.typed in published package: add package_data in
   setup.py and include in MANIFEST.in (without this, PEP 561
   support was not actually working)

2. Widen date callback type from Callable[[], str] to
   Callable[..., str | datetime | float] to match existing
   behavior (callbacks can return datetime/float, not just str)

3. Fix SMTPResponse annotations: esmtp_opts and rcpt_options are
   lists (not str), and rename refused_recipient to
   refused_recipients to match actual usage in client.py
Configure mypy in setup.cfg with per-module overrides:
- emails.message: suppress mixin pattern false positives
- emails.packages: ignore vendored DKIM code
- emails.backend.smtp.client: private smtplib attrs
- emails.template, emails.django: optional deps

Add targeted type: ignore comments for known safe patterns.
Add tox typecheck environment and CI job.

mypy now passes clean on 26 source files.
The global ignore_missing_imports does not suppress import-untyped
for inline imports. Add explicit per-module override for requests.
@lavr lavr mentioned this pull request Mar 31, 2026
3 tasks
lavr added 5 commits March 31, 2026 17:55
The uri, filename, and data attributes are defined as properties
via get/set + property(). The type annotation in __init__ conflicted
with the property descriptor, causing mypy no-redef errors.
Removing the annotation lets mypy infer the type from the property.
….get_data

Use self._data directly (type known from setter) and isinstance
checks that mypy can narrow, removing type: ignore comments.
Replace suppressions with proper fixes where possible:
- Use isinstance checks instead of hasattr for type narrowing
- Use assert for None-safety where input guarantees non-None
- Access self._data directly instead of getattr
- Remove redundant to_unicode calls on already-str values
- Use explicit if/else instead of tricky and/or expressions
- Use dict[] instead of .get() where key is guaranteed present

Remaining 14 ignores are genuine mypy limitations: vendored dkim
API, private stdlib attrs, intentional method overrides, and
decorator typing.
New tests:
- BaseFile.get_data() with str, bytes, IO, and None
- SMTPResponse: defaults, set_status, success, refused_recipients
- Message.as_bytes() with DKIM signing

The as_bytes test uncovered a bug: as_bytes() called sign_string()
(expects str) instead of sign_bytes() (expects bytes), causing
TypeError when DKIM signing was enabled. Fixed.
smtplib.SMTP.mail/rcpt/data return (int, bytes), so status_text
and the text parameter of set_status() should be bytes, not str.
@lavr lavr changed the title Fix type hints issues from review Fix type hints, add mypy to CI, fix as_bytes DKIM bug Mar 31, 2026
lavr added 8 commits March 31, 2026 18:26
- 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.
Use direct isinstance check and bytes.decode() instead of
to_native/to_unicode. Clearer control flow, no type: ignore needed.
Dead code path: never called with allow_none_charset=True anywhere
in the codebase. It also violated its own return type (returned
bytes while promising str | None). Removing it simplifies the
function and eliminates the last type: ignore in to_unicode.
These were Python 2 compatibility helpers. On Python 3:
- to_native(s) where s is str → noop, removed
- to_native(s) where s is bytes → replaced with s.decode()
- to_unicode(s) where s is str → noop, removed
- to_unicode(value) for non-str → replaced with explicit
  bytes.decode() / str() branching

Functions kept in utils.py for now (still used by loader/).
All inputs are known str — use .encode() directly instead of the
Python 2 compatibility wrapper. signers.py no longer imports
anything from utils.
_send() can return None when client.sendmail() gets empty to_addrs.
Instead of asserting this away, widen the return type to
SMTPResponse | None and propagate through retry_on_disconnect
and sendmail.
@lavr lavr merged commit b1e0635 into master Mar 31, 2026
7 checks passed
@lavr lavr deleted the fix/type-hints-review-fixes branch March 31, 2026 15:57
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.

1 participant