Skip to content

Commit dd26fd3

Browse files
committed
Don't require boto3 if Amazon SES webhooks aren't actually used
Delay raising AnymailImproperlyInstalled from webhooks.amazon_ses until an SES webhook view is instantiated. Allows anymail.urls to import webhooks.amazon_ses without error. Fixes #103
1 parent e85c4a9 commit dd26fd3

File tree

3 files changed

+42
-10
lines changed

3 files changed

+42
-10
lines changed

anymail/exceptions.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,9 @@ class AnymailImproperlyInstalled(AnymailConfigurationError, ImportError):
177177
"""Exception for Anymail missing package dependencies"""
178178

179179
def __init__(self, missing_package, backend="<backend>"):
180-
message = "The %s package is required to use this backend, but isn't installed.\n" \
180+
message = "The %s package is required to use this ESP, but isn't installed.\n" \
181181
"(Be sure to use `pip install django-anymail[%s]` " \
182-
"with your desired backends)" % (missing_package, backend)
182+
"with your desired ESPs.)" % (missing_package, backend)
183183
super(AnymailImproperlyInstalled, self).__init__(message)
184184

185185

@@ -195,3 +195,17 @@ class AnymailInsecureWebhookWarning(AnymailWarning):
195195

196196
class AnymailDeprecationWarning(AnymailWarning, DeprecationWarning):
197197
"""Warning for deprecated Anymail features"""
198+
199+
200+
# Helpers
201+
202+
class _LazyError(object):
203+
"""An object that sits inert unless/until used, then raises an error"""
204+
def __init__(self, error):
205+
self._error = error
206+
207+
def __call__(self, *args, **kwargs):
208+
raise self._error
209+
210+
def __getattr__(self, item):
211+
raise self._error

anymail/webhooks/amazon_ses.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,23 @@
66
from django.utils.dateparse import parse_datetime
77

88
from .base import AnymailBaseWebhookView
9-
from ..backends.amazon_ses import _get_anymail_boto3_params
109
from ..exceptions import (
11-
AnymailAPIError, AnymailConfigurationError, AnymailImproperlyInstalled, AnymailWebhookValidationFailure)
10+
AnymailAPIError, AnymailConfigurationError, AnymailImproperlyInstalled, AnymailWebhookValidationFailure,
11+
_LazyError)
1212
from ..inbound import AnymailInboundMessage
1313
from ..signals import AnymailInboundEvent, AnymailTrackingEvent, EventType, RejectReason, inbound, tracking
1414
from ..utils import combine, get_anymail_setting, getfirst
1515

1616
try:
1717
import boto3
18-
import botocore.exceptions
18+
from botocore.exceptions import ClientError
19+
from ..backends.amazon_ses import _get_anymail_boto3_params
1920
except ImportError:
20-
raise AnymailImproperlyInstalled(missing_package='boto3', backend='amazon_ses')
21+
# This module gets imported by anymail.urls, so don't complain about boto3 missing
22+
# unless one of the Amazon SES webhook views is actually used and needs it
23+
boto3 = _LazyError(AnymailImproperlyInstalled(missing_package='boto3', backend='amazon_ses'))
24+
ClientError = object
25+
_get_anymail_boto3_params = _LazyError(AnymailImproperlyInstalled(missing_package='boto3', backend='amazon_ses'))
2126

2227

2328
class AmazonSESBaseWebhookView(AnymailBaseWebhookView):
@@ -296,7 +301,7 @@ def esp_to_anymail_events(self, ses_event, sns_message):
296301
s3.download_fileobj(bucket_name, object_key, content)
297302
content.seek(0)
298303
message = AnymailInboundMessage.parse_raw_mime_file(content)
299-
except botocore.exceptions.ClientError as err:
304+
except ClientError as err:
300305
# improve the botocore error message
301306
raise AnymailBotoClientAPIError(
302307
"Anymail AmazonSESInboundWebhookView couldn't download S3 object '{bucket_name}:{object_key}'"
@@ -334,11 +339,11 @@ def esp_to_anymail_events(self, ses_event, sns_message):
334339
)]
335340

336341

337-
class AnymailBotoClientAPIError(AnymailAPIError, botocore.exceptions.ClientError):
342+
class AnymailBotoClientAPIError(AnymailAPIError, ClientError):
338343
"""An AnymailAPIError that is also a Boto ClientError"""
339344
def __init__(self, *args, **kwargs):
340345
raised_from = kwargs.pop('raised_from')
341-
assert isinstance(raised_from, botocore.exceptions.ClientError)
346+
assert isinstance(raised_from, ClientError)
342347
assert len(kwargs) == 0 # can't support other kwargs
343348
# init self as boto ClientError (which doesn't cooperatively subclass):
344349
super(AnymailBotoClientAPIError, self).__init__(

tests/test_utils.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
except ImportError:
1919
string_concat = None
2020

21-
from anymail.exceptions import AnymailInvalidAddress
21+
from anymail.exceptions import AnymailInvalidAddress, _LazyError
2222
from anymail.utils import (
2323
parse_address_list, parse_single_address, EmailAddress,
2424
is_lazy, force_non_lazy, force_non_lazy_dict, force_non_lazy_list,
@@ -355,3 +355,16 @@ def test_unparseable_dates(self):
355355
self.assertIsNone(parse_rfc2822date("Tue, 24 Oct"))
356356
self.assertIsNone(parse_rfc2822date("Lug, 24 Nod 2017 10:11:35 +0000"))
357357
self.assertIsNone(parse_rfc2822date("Tue, 99 Oct 9999 99:99:99 +9999"))
358+
359+
360+
class LazyErrorTests(SimpleTestCase):
361+
def test_attr(self):
362+
lazy = _LazyError(ValueError("lazy failure")) # creating doesn't cause error
363+
lazy.some_prop = "foo" # setattr doesn't cause error
364+
with self.assertRaisesMessage(ValueError, "lazy failure"):
365+
self.unused = lazy.anything # getattr *does* cause error
366+
367+
def test_call(self):
368+
lazy = _LazyError(ValueError("lazy failure")) # creating doesn't cause error
369+
with self.assertRaisesMessage(ValueError, "lazy failure"):
370+
self.unused = lazy() # call *does* cause error

0 commit comments

Comments
 (0)