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

Add params type checking for mail helper methods #720

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ install:
- pip install pypandoc
- pip install coverage
- pip install codecov
- pip install wrapt
Copy link
Contributor

Choose a reason for hiding this comment

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

Why add a new dependency instead of using built-in decorators?

# - sudo apt-get install -y pandoc
addons:
apt_packages:
Expand Down
2 changes: 1 addition & 1 deletion docker-test/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ fi

cd sendgrid-python
python3.6 setup.py install
pip install pyyaml six werkzeug flask python-http-client pytest
pip install pyyaml six werkzeug flask python-http-client pytest wrapt
exec $SHELL
3 changes: 2 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ RUN python2.7 get-pip.py && \
pip install tox && \
rm get-pip.py

#install pyyaml, six, werkzeug
#install pyyaml, six, werkzeug, flask, wrapt
RUN python3.6 -m pip install pyyaml
RUN python3.6 -m pip install six
RUN python3.6 -m pip install werkzeug
RUN python3.6 -m pip install flask
RUN python3.6 -m pip install wrapt

# set up default sendgrid env
WORKDIR /root/sources
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ Flask==0.10.1
PyYAML==3.11
python-http-client==2.2.1
six==1.10.0
pytest==3.7.1
pytest==3.7.1
wrapt==1.10.11
1 change: 1 addition & 0 deletions sendgrid/helpers/mail/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .custom_arg import CustomArg
from .email import Email
from .exceptions import SendGridException, APIKeyIncludedException
from .decorators import accepts
from .footer_settings import FooterSettings
from .ganalytics import Ganalytics
from .header import Header
Expand Down
4 changes: 4 additions & 0 deletions sendgrid/helpers/mail/asm.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from .decorators import accepts


class ASM(object):
"""An object specifying unsubscribe behavior."""

@accepts(int, list)
def __init__(self, group_id=None, groups_to_display=None):
"""Create an ASM with the given group_id and groups_to_display.

Expand Down
5 changes: 5 additions & 0 deletions sendgrid/helpers/mail/bcc_settings.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from .email import Email
from .decorators import accepts


class BCCSettings(object):
"""Settings object for automatic BCC.

This allows you to have a blind carbon copy automatically sent to the
specified email address for every email that is sent.
"""

@accepts(bool, Email)
def __init__(self, enable=None, email=None):
"""Create a BCCSettings.

Expand Down
4 changes: 4 additions & 0 deletions sendgrid/helpers/mail/bypass_list_management.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from .decorators import accepts


class BypassListManagement(object):
"""Setting for Bypass List Management

Expand All @@ -7,6 +10,7 @@ class BypassListManagement(object):
receives your email.
"""

@accepts(bool)
def __init__(self, enable=None):
"""Create a BypassListManagement.

Expand Down
4 changes: 4 additions & 0 deletions sendgrid/helpers/mail/category.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from .decorators import accepts


class Category(object):
"""A category name for this message."""

@accepts(str)
def __init__(self, name=None):
"""Create a Category.

Expand Down
4 changes: 4 additions & 0 deletions sendgrid/helpers/mail/click_tracking.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from .decorators import accepts


class ClickTracking(object):
"""Allows you to track whether a recipient clicked a link in your email."""

@accepts(bool, bool)
def __init__(self, enable=None, enable_text=None):
"""Create a ClickTracking to track clicked links in your email.

Expand Down
2 changes: 2 additions & 0 deletions sendgrid/helpers/mail/content.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .decorators import accepts
from .validators import ValidateAPIKey

class Content(object):
Expand All @@ -6,6 +7,7 @@ class Content(object):
You must specify at least one mime type in the Contents of your email.
"""

@accepts(str, str)
def __init__(self, type_=None, value=None):
"""Create a Content with the specified MIME type and value.

Expand Down
37 changes: 37 additions & 0 deletions sendgrid/helpers/mail/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Contains decorators used in helper."""

import inspect
import six
import wrapt


def accepts(*types, **kwtypes):
"""Decorator for verifying the type of method arguments."""
@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
args_names = inspect.getargspec(wrapped)[0]
if instance is not None:
args_names = args_names[1:]

for (arg_val, expected_type, arg_name) in zip(args, types, args_names):
if expected_type is str:
expected_type = six.string_types

if not isinstance(arg_val, expected_type):
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm worried that this is going to cause more breaking changes than is functionally necessary. For example, with this PR adding strict type checking broke a functional implementation.

raise TypeError(
"Argument '{0}' should be of type {1}, got: {2}"
.format(arg_name, expected_type, type(arg_val))
)

for kwarg in kwargs:
if kwarg in kwtypes:
arg_name = kwtypes[kwarg]
actual_value = kwargs[kwarg]
if not isinstance(actual_value, arg_name):
raise TypeError(
"Argument '{}' should be of type {}, got: {}"
.format(kwarg, arg_name, type(actual_value))
)

return wrapped(*args, **kwargs)
return wrapper
2 changes: 2 additions & 0 deletions sendgrid/helpers/mail/email.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .decorators import accepts
try:
import rfc822
except ImportError:
Expand All @@ -7,6 +8,7 @@
class Email(object):
"""An email address with an optional name."""

@accepts(str, str)
def __init__(self, email=None, name=None):
"""Create an Email with the given address and name.

Expand Down
4 changes: 4 additions & 0 deletions sendgrid/helpers/mail/footer_settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from .decorators import accepts


class FooterSettings(object):
"""The default footer that you would like included on every email."""

@accepts(bool, str, str)
def __init__(self, enable=None, text=None, html=None):
"""Create a default footer.

Expand Down
5 changes: 5 additions & 0 deletions sendgrid/helpers/mail/ganalytics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from .decorators import accepts


class Ganalytics(object):
"""Allows you to enable tracking provided by Google Analytics."""

@accepts(bool, str, str, str, str, str)
def __init__(self,
enable=None,
utm_source=None,
Expand Down Expand Up @@ -37,6 +41,7 @@ def __init__(self,
self.__set_field("utm_content", utm_content)
self.__set_field("utm_campaign", utm_campaign)

@accepts(str, object)
def __set_field(self, field, value):
""" Sets a field to the provided value if value is not None

Expand Down
3 changes: 3 additions & 0 deletions sendgrid/helpers/mail/header.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from .decorators import accepts


class Header(object):
"""A header to specify specific handling instructions for your email.

Expand Down
3 changes: 3 additions & 0 deletions sendgrid/helpers/mail/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
from .personalization import Personalization
from .header import Header
from .email import Email
from .content import Content
from .decorators import accepts


class Mail(object):
"""A request to be sent with the SendGrid v3 Mail Send API (v3/mail/send).

Use get() to get the request body.
"""
@accepts(Email, str, Email, Content)
def __init__(self,
from_email=None,
subject=None,
Expand Down
4 changes: 4 additions & 0 deletions sendgrid/helpers/mail/open_tracking.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from .decorators import accepts


class OpenTracking(object):
"""
Allows you to track whether the email was opened or not, by including a
single pixel image in the body of the content. When the pixel is loaded,
we log that the email was opened.
"""

@accepts(bool, str)
def __init__(self, enable=None, substitution_tag=None):
"""Create an OpenTracking to track when your email is opened.

Expand Down
13 changes: 13 additions & 0 deletions sendgrid/helpers/mail/personalization.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
from .decorators import accepts
from .email import Email
from .header import Header
from .substitution import Substitution
from .custom_arg import CustomArg


class Personalization(object):
"""A Personalization defines who should receive an individual message and
how that message should be handled.
Expand Down Expand Up @@ -31,6 +38,7 @@ def tos(self):
def tos(self, value):
self._tos = value

@accepts(Email)
def add_to(self, email):
"""Add a single recipient to this Personalization.

Expand All @@ -50,6 +58,7 @@ def ccs(self):
def ccs(self, value):
self._ccs = value

@accepts(Email)
def add_cc(self, email):
"""Add a single recipient to receive a copy of this email.

Expand All @@ -70,6 +79,7 @@ def bccs(self):
def bccs(self, value):
self._bccs = value

@accepts(Email)
def add_bcc(self, email):
"""Add a single recipient to receive a blind carbon copy of this email.

Expand Down Expand Up @@ -104,6 +114,7 @@ def headers(self):
def headers(self, value):
self._headers = value

@accepts(Header)
def add_header(self, header):
"""Add a single Header to this Personalization.

Expand All @@ -123,6 +134,7 @@ def substitutions(self):
def substitutions(self, value):
self._substitutions = value

@accepts(Substitution)
def add_substitution(self, substitution):
"""Add a new Substitution to this Personalization.

Expand All @@ -142,6 +154,7 @@ def custom_args(self):
def custom_args(self, value):
self._custom_args = value

@accepts(CustomArg)
def add_custom_arg(self, custom_arg):
"""Add a CustomArg to this Personalization.

Expand Down
4 changes: 4 additions & 0 deletions sendgrid/helpers/mail/sandbox_mode.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from .decorators import accepts


class SandBoxMode(object):
"""Setting for sandbox mode.

This allows you to send a test email to ensure that your request body is
valid and formatted correctly.
"""
@accepts(bool)
def __init__(self, enable=None):
"""Create an enabled or disabled SandBoxMode.

Expand Down
4 changes: 4 additions & 0 deletions sendgrid/helpers/mail/spam_check.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from .decorators import accepts


class SpamCheck(object):
"""This allows you to test the content of your email for spam."""

@accepts(bool, int, str)
def __init__(self, enable=None, threshold=None, post_to_url=None):
"""Create a SpamCheck to test the content of your email for spam.

Expand Down
4 changes: 4 additions & 0 deletions sendgrid/helpers/mail/subscription_tracking.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from .decorators import accepts


class SubscriptionTracking(object):
"""Allows you to insert a subscription management link at the bottom of the
text and html bodies of your email. If you would like to specify the
location of the link within your email, you may use the substitution_tag.
"""

@accepts(bool, str, str, str)
def __init__(self, enable=None, text=None, html=None, substitution_tag=None):
"""Create a SubscriptionTracking to customize subscription management.

Expand Down
4 changes: 4 additions & 0 deletions sendgrid/helpers/mail/substitution.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from .decorators import accepts


class Substitution(object):
"""A string substitution to be applied to the text and HTML contents of
the body of your email, as well as in the Subject and Reply-To parameters.
"""

@accepts(str, str)
def __init__(self, key=None, value=None):
"""Create a Substitution with the given key and value.

Expand Down
13 changes: 13 additions & 0 deletions test/test_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,3 +584,16 @@ def test_dynamic_template_data(self):
}
}
self.assertDictEqual(p.get(), expected)

def test_type_checking_decorator(self):
with self.assertRaises(TypeError):
Email(123)

with self.assertRaises(TypeError):
Content("text/plain", 123.45)

with self.assertRaises(TypeError):
email = Email("[email protected]")
subject = "Testing"
not_email = Personalization()
Mail(email, subject, not_email)