From dd26fd31088e7d101726480404e68ab2dd5a8ce4 Mon Sep 17 00:00:00 2001 From: medmunds Date: Mon, 16 Apr 2018 15:41:00 -0700 Subject: [PATCH] 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 --- anymail/exceptions.py | 18 ++++++++++++++++-- anymail/webhooks/amazon_ses.py | 19 ++++++++++++------- tests/test_utils.py | 15 ++++++++++++++- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/anymail/exceptions.py b/anymail/exceptions.py index ceab253..789638d 100644 --- a/anymail/exceptions.py +++ b/anymail/exceptions.py @@ -177,9 +177,9 @@ class AnymailImproperlyInstalled(AnymailConfigurationError, ImportError): """Exception for Anymail missing package dependencies""" def __init__(self, missing_package, backend=""): - message = "The %s package is required to use this backend, but isn't installed.\n" \ + message = "The %s package is required to use this ESP, but isn't installed.\n" \ "(Be sure to use `pip install django-anymail[%s]` " \ - "with your desired backends)" % (missing_package, backend) + "with your desired ESPs.)" % (missing_package, backend) super(AnymailImproperlyInstalled, self).__init__(message) @@ -195,3 +195,17 @@ class AnymailInsecureWebhookWarning(AnymailWarning): class AnymailDeprecationWarning(AnymailWarning, DeprecationWarning): """Warning for deprecated Anymail features""" + + +# Helpers + +class _LazyError(object): + """An object that sits inert unless/until used, then raises an error""" + def __init__(self, error): + self._error = error + + def __call__(self, *args, **kwargs): + raise self._error + + def __getattr__(self, item): + raise self._error diff --git a/anymail/webhooks/amazon_ses.py b/anymail/webhooks/amazon_ses.py index 62bdc65..7cdeae2 100644 --- a/anymail/webhooks/amazon_ses.py +++ b/anymail/webhooks/amazon_ses.py @@ -6,18 +6,23 @@ from django.http import HttpResponse from django.utils.dateparse import parse_datetime from .base import AnymailBaseWebhookView -from ..backends.amazon_ses import _get_anymail_boto3_params from ..exceptions import ( - AnymailAPIError, AnymailConfigurationError, AnymailImproperlyInstalled, AnymailWebhookValidationFailure) + AnymailAPIError, AnymailConfigurationError, AnymailImproperlyInstalled, AnymailWebhookValidationFailure, + _LazyError) from ..inbound import AnymailInboundMessage from ..signals import AnymailInboundEvent, AnymailTrackingEvent, EventType, RejectReason, inbound, tracking from ..utils import combine, get_anymail_setting, getfirst try: import boto3 - import botocore.exceptions + from botocore.exceptions import ClientError + from ..backends.amazon_ses import _get_anymail_boto3_params except ImportError: - raise AnymailImproperlyInstalled(missing_package='boto3', backend='amazon_ses') + # This module gets imported by anymail.urls, so don't complain about boto3 missing + # unless one of the Amazon SES webhook views is actually used and needs it + boto3 = _LazyError(AnymailImproperlyInstalled(missing_package='boto3', backend='amazon_ses')) + ClientError = object + _get_anymail_boto3_params = _LazyError(AnymailImproperlyInstalled(missing_package='boto3', backend='amazon_ses')) class AmazonSESBaseWebhookView(AnymailBaseWebhookView): @@ -296,7 +301,7 @@ class AmazonSESInboundWebhookView(AmazonSESBaseWebhookView): s3.download_fileobj(bucket_name, object_key, content) content.seek(0) message = AnymailInboundMessage.parse_raw_mime_file(content) - except botocore.exceptions.ClientError as err: + except ClientError as err: # improve the botocore error message raise AnymailBotoClientAPIError( "Anymail AmazonSESInboundWebhookView couldn't download S3 object '{bucket_name}:{object_key}'" @@ -334,11 +339,11 @@ class AmazonSESInboundWebhookView(AmazonSESBaseWebhookView): )] -class AnymailBotoClientAPIError(AnymailAPIError, botocore.exceptions.ClientError): +class AnymailBotoClientAPIError(AnymailAPIError, ClientError): """An AnymailAPIError that is also a Boto ClientError""" def __init__(self, *args, **kwargs): raised_from = kwargs.pop('raised_from') - assert isinstance(raised_from, botocore.exceptions.ClientError) + assert isinstance(raised_from, ClientError) assert len(kwargs) == 0 # can't support other kwargs # init self as boto ClientError (which doesn't cooperatively subclass): super(AnymailBotoClientAPIError, self).__init__( diff --git a/tests/test_utils.py b/tests/test_utils.py index f6800a4..a7d4a51 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,7 +18,7 @@ try: except ImportError: string_concat = None -from anymail.exceptions import AnymailInvalidAddress +from anymail.exceptions import AnymailInvalidAddress, _LazyError from anymail.utils import ( parse_address_list, parse_single_address, EmailAddress, is_lazy, force_non_lazy, force_non_lazy_dict, force_non_lazy_list, @@ -355,3 +355,16 @@ class ParseRFC2822DateTests(SimpleTestCase): self.assertIsNone(parse_rfc2822date("Tue, 24 Oct")) self.assertIsNone(parse_rfc2822date("Lug, 24 Nod 2017 10:11:35 +0000")) self.assertIsNone(parse_rfc2822date("Tue, 99 Oct 9999 99:99:99 +9999")) + + +class LazyErrorTests(SimpleTestCase): + def test_attr(self): + lazy = _LazyError(ValueError("lazy failure")) # creating doesn't cause error + lazy.some_prop = "foo" # setattr doesn't cause error + with self.assertRaisesMessage(ValueError, "lazy failure"): + self.unused = lazy.anything # getattr *does* cause error + + def test_call(self): + lazy = _LazyError(ValueError("lazy failure")) # creating doesn't cause error + with self.assertRaisesMessage(ValueError, "lazy failure"): + self.unused = lazy() # call *does* cause error