Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/implement mypy type checking #394

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

YPCrumble
Copy link

This PR adds initial MyPy type checks per discussion in #393.

@medmunds
Copy link
Contributor

Thanks for this! I want to poke around with it a little, and then will probably have some questions.

The separate GH workflow for mypy is fine for getting things going, but we'll eventually want to move running mypy into tox.ini to simplify local runs. (I can do that later if you're not familiar with tox.)

Is there a reason docs needs to be turned into a module? (We deliberately don't support importing from docs.)

@YPCrumble
Copy link
Author

Sounds good @medmunds. No need to make docs a module, I adjusted how we're excluding it and removed the __init__.py file.

Copy link
Contributor

@medmunds medmunds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been playing around with these changes and mypy, and really like where this is going. Thanks again for getting it started.

I have a handful of comments and questions, inline in the code. (GitHub sometimes collapses review comments; search this page for "hidden conversations" to be sure.)

.github/workflows/mypy.yml Show resolved Hide resolved
.github/workflows/mypy.yml Outdated Show resolved Hide resolved
from email.utils import quote as rfc822_quote

from requests.structures import CaseInsensitiveDict

from ..exceptions import AnymailConfigurationError, AnymailWarning
from ..message import AnymailRecipientStatus
from ..utils import BASIC_NUMERIC_TYPES, Mapping, get_anymail_setting, update_deep
from ..utils import BASIC_NUMERIC_TYPES, get_anymail_setting, update_deep
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 it already found a bug!

@@ -297,7 +296,9 @@ def set_send_at(self, send_at: datetime | str) -> None:
# "Date and time in the format “YYYY-MM-DD hh:mm:ss” in the UTC time zone."
# If send_at is a datetime, it's guaranteed to be aware, but maybe not UTC.
# Convert to UTC, then strip tzinfo to avoid isoformat "+00:00" at end.
send_at_utc = send_at.astimezone(timezone.utc).replace(tzinfo=None)
send_at_utc = send_at.astimezone( # type:ignore[union-attr]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option here would be to replace the try/catch with if isinstance(send_at, datetime).

Question: Would you suggest first adding the type:ignore comments without changing code, and then going back to resolve each of them in later commits that change the code?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's an easy fix I'd recommend doing it now - I just didn't see the fix offhand and lean towards keeping MyPy from slowing things down vs. trying to figure out a type hint I can't grok right away, especially if I know the code is fine.

@@ -0,0 +1 @@
AnymailRecipientsType = list[dict[str, dict[str, str]]]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? (It doesn't seem to be referenced anywhere.)

disallow_subclassing_any = False
disallow_untyped_calls = False
disallow_untyped_defs = False
ignore_missing_imports = True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my experimenting, I was able to switch ignore_missing_imports back to False by:

  1. pip install -e '.[amazon-ses,resend,postal]' in the workflow, to pick up all the optional dependencies (see earlier comment)
  2. Adding boto3-stubs[s3,ses,sns] to requirements-dev.txt to pick up boto3 typing (see later comment)
  3. Adding some type:ignore comments on the _LazyImportErrors in webhooks/amazon_ses.py and webhooks/resend.py (like the ones you already added to webhooks/postal.py).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds great!

pre-commit
requests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requests should not be needed here, since it's already a package dependency. (But you need to pip install -e . to pick up package dependencies, per earlier comment.)

However, I did need to add boto3-stubs[s3,ses,sns] since boto3 isn't typed.

Question: How do library packages typically handle dependencies only needed for typing? Would it be helpful for django-anymail to declare a "typing" extra (pip install django-anymail[typing]) that adds the dependencies on django-stubs and boto3-stubs? Or do libraries usually assume that users will figure out on their own what typing stubs are required?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, for me this was just for my local environment. I generally expect to pip install -r requirements-dev.txt and my local machine would have the necessary packages to run a command like mypy ....that's what led me to make this change.

@@ -49,7 +49,7 @@ class SparkPostBackendMockAPITestCase(RequestsBackendMockAPITestCase):
def setUp(self):
super().setUp()
# Simple message useful for many tests
self.message = mail.EmailMultiAlternatives(
self.message = AnymailMessage(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is no longer required (since you disabled mypy on tests).

But it raises a couple of questions: A lot of the tests use Anymail's added attributes directly on Django's EmailMessage or EmailMultiAlternatives, because this is a documented Anymail feature. (Anymail has always allowed duck typing for its added attributes.)

  • Is there some special way we should be testing this works with mypy? (Other than just a test on EmailMessage or EmailMultiAlternatives that sets all of Anymail's attributes with type:ignore comments?)
  • Is there some special way Anymail should be exposing this capability for library users? (Like an AnymailMessageProtocol?)

YPCrumble and others added 2 commits September 12, 2024 15:03
Co-authored-by: Mike Edmunds <medmunds@gmail.com>
Co-authored-by: Mike Edmunds <medmunds@gmail.com>
@YPCrumble
Copy link
Author

Thank you for the review @medmunds !!! I wanted to respond to your thoughtful comments and also let you know I'm a little busy over the coming months until the new year so won't be able to get back to this...feel free to take over or I will come back to it later!

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.

2 participants