diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml new file mode 100644 index 00000000..ab456c24 --- /dev/null +++ b/.github/workflows/mypy.yml @@ -0,0 +1,24 @@ +--- +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 + with: + python-version: "3.12" + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip wheel + pip install -e '.[amazon-ses,resend,postal]' -r requirements-dev.txt + - name: MyPy + run: mypy . 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..124cbef0 100644 --- a/anymail/backends/unisender_go.py +++ b/anymail/backends/unisender_go.py @@ -135,9 +135,8 @@ def __init__( http_headers["Content-Type"] = "application/json" http_headers["Accept"] = "application/json" http_headers["X-API-KEY"] = backend.api_key - super().__init__( - message, defaults, backend, headers=http_headers, *args, **kwargs - ) + kwargs["headers"] = http_headers + super().__init__(message, defaults, backend, *args, **kwargs) def get_api_endpoint(self) -> str: return "email/send.json" @@ -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] + timezone.utc + ).replace(tzinfo=None) 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/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/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/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): diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..8583d6df --- /dev/null +++ b/mypy.ini @@ -0,0 +1,24 @@ +[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 +exclude = docs + +[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 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 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"] )