From ebc2e88415ef7565a45564189fbed86036c17ba7 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Fri, 23 Aug 2024 12:05:25 -0400 Subject: [PATCH 1/8] Add MyPy type checking and a GitHub Action to automatically check this. --- .github/workflows/mypy.yml | 22 ++++++++++++++++++++++ mypy.ini | 26 ++++++++++++++++++++++++++ requirements-dev.txt | 4 ++++ 3 files changed, 52 insertions(+) create mode 100644 .github/workflows/mypy.yml create mode 100644 mypy.ini diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml new file mode 100644 index 00000000..485085f5 --- /dev/null +++ b/.github/workflows/mypy.yml @@ -0,0 +1,22 @@ +--- +name: Run mypy +on: + pull_request: + push: + branches: ["main", "v[0-9]*"] + tags: ["v[0-9]*"] + workflow_dispatch: +jobs: + mypy: + runs-on: ubuntu-22.04 + steps: + - name: apt update + run: sudo apt update + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip wheel + pip install -r requirements-dev.txt + - name: MyPy + run: mypy . diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..75784074 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,26 @@ +[mypy] +plugins = mypy_django_plugin.main +# TODO: Check all code in the future +check_untyped_defs = False +disallow_any_generics = False +disallow_incomplete_defs = False +disallow_subclassing_any = False +disallow_untyped_calls = False +disallow_untyped_defs = False +ignore_missing_imports = True +no_implicit_optional = True +no_implicit_reexport = True +strict = True +warn_return_any = False +# TODO: Remove this exception in the future +disable_error_code = var-annotated + +[mypy.plugins.django-stubs] +django_settings_module = "tests.test_settings.settings_5_0" + +# TODO: Type check tests in the future +[mypy-tests.*] +ignore_errors = True + +[mypy-docs.*] +ignore_errors = True diff --git a/requirements-dev.txt b/requirements-dev.txt index 753eb1c2..ed8928a2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,9 @@ # Requirements for developing (not just using) the package +django-stubs hatch +mypy pre-commit +requests tox<4 +types-requests From a31f6312d97ebd4a0e1b5622c621b5c113f5c17d Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Fri, 23 Aug 2024 12:06:10 -0400 Subject: [PATCH 2/8] Fix issues related to type hinting. --- anymail/backends/sendgrid.py | 3 ++- anymail/backends/unisender_go.py | 5 +++-- anymail/exceptions.py | 2 +- anymail/webhooks/base.py | 4 +++- tests/test_sparkpost_backend.py | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/anymail/backends/sendgrid.py b/anymail/backends/sendgrid.py index b19e884d..475c010a 100644 --- a/anymail/backends/sendgrid.py +++ b/anymail/backends/sendgrid.py @@ -1,12 +1,13 @@ import uuid import warnings +from collections.abc import Mapping 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 from .base_requests import AnymailRequestsBackend, RequestsPayload diff --git a/anymail/backends/unisender_go.py b/anymail/backends/unisender_go.py index c70a3a33..a1e187d6 100644 --- a/anymail/backends/unisender_go.py +++ b/anymail/backends/unisender_go.py @@ -135,8 +135,9 @@ def __init__( http_headers["Content-Type"] = "application/json" http_headers["Accept"] = "application/json" http_headers["X-API-KEY"] = backend.api_key + kwargs["headers"] = http_headers super().__init__( - message, defaults, backend, headers=http_headers, *args, **kwargs + message, defaults, backend, *args, **kwargs ) def get_api_endpoint(self) -> str: @@ -297,7 +298,7 @@ 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(timezone.utc).replace(tzinfo=None) # type:ignore[union-attr] send_at_formatted = send_at_utc.isoformat(sep=" ", timespec="seconds") assert len(send_at_formatted) == 19 except (AttributeError, TypeError): diff --git a/anymail/exceptions.py b/anymail/exceptions.py index 98dc9e07..4fe548c0 100644 --- a/anymail/exceptions.py +++ b/anymail/exceptions.py @@ -86,7 +86,7 @@ class AnymailAPIError(AnymailError): """Exception for unsuccessful response from ESP's API.""" -class AnymailRequestsAPIError(AnymailAPIError, HTTPError): +class AnymailRequestsAPIError(AnymailAPIError, HTTPError): # type: ignore[misc] """Exception for unsuccessful response from a requests API.""" def __init__(self, *args, **kwargs): diff --git a/anymail/webhooks/base.py b/anymail/webhooks/base.py index 5a2aefe8..8622c1a8 100644 --- a/anymail/webhooks/base.py +++ b/anymail/webhooks/base.py @@ -1,5 +1,7 @@ +import typing import warnings +from django.dispatch import Signal from django.http import HttpResponse from django.utils.crypto import constant_time_compare from django.utils.decorators import method_decorator @@ -30,7 +32,7 @@ def __init__(self, **kwargs): # Subclass implementation: # Where to send events: either ..signals.inbound or ..signals.tracking - signal = None + signal: typing.Optional[Signal] = None def validate_request(self, request): """Check validity of webhook post, or raise AnymailWebhookValidationFailure. diff --git a/tests/test_sparkpost_backend.py b/tests/test_sparkpost_backend.py index bfb44024..906539b5 100644 --- a/tests/test_sparkpost_backend.py +++ b/tests/test_sparkpost_backend.py @@ -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( "Subject", "Text Body", "from@example.com", ["to@example.com"] ) From dfe783647e6a3eeaa0dfc71c4729ecc4b4b8ce7c Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Fri, 23 Aug 2024 12:09:33 -0400 Subject: [PATCH 3/8] Add missing files to pass initial MyPy checks. --- anymail/types.py | 1 + docs/__init__.py | 0 2 files changed, 1 insertion(+) create mode 100644 anymail/types.py create mode 100644 docs/__init__.py diff --git a/anymail/types.py b/anymail/types.py new file mode 100644 index 00000000..3d4187f2 --- /dev/null +++ b/anymail/types.py @@ -0,0 +1 @@ +AnymailRecipientsType = list[dict[str, dict[str, str]]] diff --git a/docs/__init__.py b/docs/__init__.py new file mode 100644 index 00000000..e69de29b From bac6cca48a4c91527280d667b8b8bb0cdd0d8868 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Fri, 23 Aug 2024 12:14:44 -0400 Subject: [PATCH 4/8] Fix type hinting issues in webhooks/postal.py. --- anymail/webhooks/postal.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/anymail/webhooks/postal.py b/anymail/webhooks/postal.py index d94a6789..44a82818 100644 --- a/anymail/webhooks/postal.py +++ b/anymail/webhooks/postal.py @@ -35,11 +35,11 @@ missing_package="cryptography", install_extra="postal" ) ) - serialization = error - hashes = error + serialization = error # type: ignore[assignment] + hashes = error # type: ignore[assignment] default_backend = error - padding = error - InvalidSignature = object + padding = error # type: ignore[assignment] + InvalidSignature = object # type:ignore[assignment,misc] class PostalBaseWebhookView(AnymailBaseWebhookView): From ae58e393911ff44042f1f01a8016d70a45aa3a85 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Fri, 23 Aug 2024 15:32:31 -0400 Subject: [PATCH 5/8] Fix linting issues. --- anymail/backends/unisender_go.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/anymail/backends/unisender_go.py b/anymail/backends/unisender_go.py index a1e187d6..124cbef0 100644 --- a/anymail/backends/unisender_go.py +++ b/anymail/backends/unisender_go.py @@ -136,9 +136,7 @@ def __init__( http_headers["Accept"] = "application/json" http_headers["X-API-KEY"] = backend.api_key kwargs["headers"] = http_headers - super().__init__( - message, defaults, backend, *args, **kwargs - ) + super().__init__(message, defaults, backend, *args, **kwargs) def get_api_endpoint(self) -> str: return "email/send.json" @@ -298,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) # type:ignore[union-attr] + send_at_utc = send_at.astimezone( # type:ignore[union-attr] + timezone.utc + ).replace(tzinfo=None) send_at_formatted = send_at_utc.isoformat(sep=" ", timespec="seconds") assert len(send_at_formatted) == 19 except (AttributeError, TypeError): From 68732e6744518d45a60229908fb9efdecc5aafa8 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Tue, 27 Aug 2024 20:00:48 -0400 Subject: [PATCH 6/8] Update method of excluding docs directory and remove __init__.py file. --- docs/__init__.py | 0 mypy.ini | 4 +--- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 docs/__init__.py diff --git a/docs/__init__.py b/docs/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mypy.ini b/mypy.ini index 75784074..8583d6df 100644 --- a/mypy.ini +++ b/mypy.ini @@ -14,6 +14,7 @@ strict = True warn_return_any = False # TODO: Remove this exception in the future disable_error_code = var-annotated +exclude = docs [mypy.plugins.django-stubs] django_settings_module = "tests.test_settings.settings_5_0" @@ -21,6 +22,3 @@ django_settings_module = "tests.test_settings.settings_5_0" # TODO: Type check tests in the future [mypy-tests.*] ignore_errors = True - -[mypy-docs.*] -ignore_errors = True From 3748cdfd09a0ac45b9358c2b4c13199bd56fdeea Mon Sep 17 00:00:00 2001 From: YPCrumble Date: Thu, 12 Sep 2024 15:03:58 -0400 Subject: [PATCH 7/8] Update .github/workflows/mypy.yml Co-authored-by: Mike Edmunds --- .github/workflows/mypy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 485085f5..17e04fa0 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -14,6 +14,8 @@ jobs: run: sudo apt update - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: "3.12" - name: Install Python dependencies run: | python -m pip install --upgrade pip wheel From 97fe791d533bdc369ac4bf8bf52669eec028e591 Mon Sep 17 00:00:00 2001 From: YPCrumble Date: Thu, 12 Sep 2024 15:04:11 -0400 Subject: [PATCH 8/8] Update .github/workflows/mypy.yml Co-authored-by: Mike Edmunds --- .github/workflows/mypy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 17e04fa0..ab456c24 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -19,6 +19,6 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip wheel - pip install -r requirements-dev.txt + pip install -e '.[amazon-ses,resend,postal]' -r requirements-dev.txt - name: MyPy run: mypy .