Fix type hints, add mypy to CI, fix as_bytes DKIM bug#194
Merged
Conversation
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.
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.
- 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
py.typednow shipped in package,datecallback type widened,SMTPResponsefields corrected (refused_recipients,esmtp_opts/rcpt_optionsas lists,status_textasbytes)setup.cfgwith per-module overrides for mixin pattern and vendored code; newtox -e typecheckenv and GitHub Actions job# type: ignorefrom 26 to 14: replaced suppressions with proper isinstance checks, asserts, and direct attribute access where possibleas_bytes()DKIM bug:as_bytes()calledsign_string()(expectsstr) instead ofsign_bytes()(expectsbytes), causingTypeErrorwhen DKIM signing was enabledBaseFile.get_data()with all data types,SMTPResponseattributes, DKIMas_bytes()Test plan
mypy emails/passes with 0 errors