From 60e3086fc04cb9d9158321b1a78a88e1781000af Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 2 Mar 2011 21:35:25 -0500 Subject: [PATCH] - ``pyramid_mailer.includeme`` function added for ``config.include('pyramid_mailer')`` support - ``pyramid_mailer.testing`` module added for ``config.include('pyramid_mailer.testing')`` support. - ``pyramid_mailer.get_mailer`` API added (see docs). - ``pyramid_mailer.interfaces`` module readded (with marker IMailer interface for ZCA registration). - ``setup.cfg`` added with coverage parameters to allow for ``setup.py nosetests --with-coverage``. --- .hgignore | 2 +- CHANGES.txt | 16 +++ docs/index.rst | 199 ++++++++++++++++++++++++----------- pyramid_mailer/__init__.py | 19 ++++ pyramid_mailer/interfaces.py | 5 + pyramid_mailer/mailer.py | 6 +- pyramid_mailer/testing.py | 6 ++ pyramid_mailer/tests.py | 79 +++++++++++++- 8 files changed, 264 insertions(+), 68 deletions(-) create mode 100644 CHANGES.txt create mode 100644 pyramid_mailer/interfaces.py create mode 100644 pyramid_mailer/testing.py diff --git a/.hgignore b/.hgignore index a6f817d..83213de 100644 --- a/.hgignore +++ b/.hgignore @@ -12,7 +12,7 @@ syntax: glob *.orig *.cfg *.tox - +env26/ *~ diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..9bcd533 --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,16 @@ +Next release +------------ + +- ``pyramid_mailer.includeme`` function added for + ``config.include('pyramid_mailer')`` support + +- ``pyramid_mailer.testing`` module added for + ``config.include('pyramid_mailer.testing')`` support. + +- ``pyramid_mailer.get_mailer`` API added (see docs). + +- ``pyramid_mailer.interfaces`` module readded (with marker IMailer interface + for ZCA registration). + +- ``setup.cfg`` added with coverage parameters to allow for ``setup.py + nosetests --with-coverage``. diff --git a/docs/index.rst b/docs/index.rst index ab4a3c6..0dac816 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,66 +1,70 @@ pyramid_mailer ================== -**pyramid_mailer** is a package for the Pyramid framework to take the pain out of sending emails. It has the following features: +**pyramid_mailer** is a package for the Pyramid framework to take the pain + out of sending emails. It has the following features: -1. A wrapper around the low-level email functionality of standard Python. This includes handling multipart emails with both text and - HTML content, and file attachments. +1. A wrapper around the low-level email functionality of standard + Python. This includes handling multipart emails with both text and HTML + content, and file attachments. -2. The option of directly sending an email or adding it to the queue in your maildir. +2. The option of directly sending an email or adding it to the queue in your +maildir. -3. Wrapping email sending in the transaction manager. If you have a view that sends a customer an email for example, and there is an - error in that view (for example, a database error) then this ensures that the email is not sent. +3. Wrapping email sending in the transaction manager. If you have a view that + sends a customer an email for example, and there is an error in that view + (for example, a database error) then this ensures that the email is not + sent. -4. A ``DummyMailer`` class to help with writing unit tests, or other situations where you want to avoid emails being sent accidentally - from a non-production install. +4. A ``DummyMailer`` class to help with writing unit tests, or other + situations where you want to avoid emails being sent accidentally from a + non-production install. -**pyramid_mailer** uses the `repoze_sendmail`_ package for general email sending, queuing and transaction management, and the `Lamson`_ -library for low-level multipart message encoding and wrapping. You do not have to install or run a Lamson mail service. +**pyramid_mailer** uses the `repoze_sendmail`_ package for general email +sending, queuing and transaction management, and the `Lamson`_ library for +low-level multipart message encoding and wrapping. You do not have to install +or run a Lamson mail service. Installation ------------ -Install using **pip install pyramid_mailer** or **easy_install pyramid_mailer**. +Install using **pip install pyramid_mailer** or **easy_install +pyramid_mailer**. -If installing from source, untar/unzip, cd into the directory and do **python setup.py install**. +If installing from source, untar/unzip, cd into the directory and do **python +setup.py install**. -The source repository is on `Bitbucket`_. Please report any bugs, issues or queries there. +The source repository is on `Bitbucket`_. Please report any bugs, issues or +queries there. Installing on Windows --------------------- -Some Windows users have reported issues installing `Lamson`_ due to some dependencies that do not work on Windows. +Some Windows users have reported issues installing `Lamson`_ due to some +dependencies that do not work on Windows. -The best way to install on Windows is to install the individual packages using the `no dependencies` option:: +The best way to install on Windows is to install the individual packages +using the `no dependencies` option:: easy_install -N lamson chardet repoze.sendmail pyramid_mailer -Getting started ---------------- +Getting Started (The Easier Way) +-------------------------------- -To get started create an instance of :class:`pyramid_mailer.mailer.Mailer`:: +In your application's configuration stanza (where you create a Pyramid +"Configurator"), use the ``config.include`` method:: - from pyramid_mailer.mailer import Mailer - - mailer = Mailer() + config.include('pyramid_mailer') -The ``Mailer`` class can take a number of optional settings, detailed in :ref:`configuration`. It's a good idea to create a single ``Mailer`` instance for your application, and add it to your registry in your configuration setup:: +Thereafter in view code, use the ``pyramid_mailer.get_mailer`` API to obtain +the configured mailer:: - config = Configurator(settings=settings) - config.registry['mailer'] = Mailer.from_settings(settings) + from pyramid_mailer import get_mailer + mailer = get_mailer(request) -or alternatively:: - - from pyramid_mailer import mailer_factory_from_settings - config.registry['mailer'] = mailer_factory_from_settings(settings) - -You can then access your mailer in a view:: - - def my_view(request): - mailer = request.registry['mailer'] - -To send a message, you must first create a :class:`pyramid_mailer.message.Message` instance:: +To send a message, you must first create a +:class:`pyramid_mailer.message.Message` instance:: from pyramid_mailer.message import Message @@ -69,7 +73,8 @@ To send a message, you must first create a :class:`pyramid_mailer.message.Messag recipients=["arthur.dent@gmail.com"], body="hello, arthur") -The ``Message`` is then passed to the ``Mailer`` instance. You can either send the message right away:: +The ``Message`` is then passed to the ``Mailer`` instance. You can either +send the message right away:: mailer.send(message) @@ -77,22 +82,69 @@ or add it to your mail queue (a maildir on disk):: mailer.send_to_queue(message) - -Usually you provide the ``sender`` to your ``Message`` instance. Often however a site might just use a single from address. If that is the case you can provide the ``default_sender`` to your ``Mailer`` and this will be used in throughout your application as the default if the ``sender`` is not otherwise provided. +Usually you provide the ``sender`` to your ``Message`` instance. Often +however a site might just use a single from address. If that is the case you +can provide the ``default_sender`` to your ``Mailer`` and this will be used +in throughout your application as the default if the ``sender`` is not +otherwise provided. -If you don't want to use transactions, you can side-step them by using **send_immediately**:: +If you don't want to use transactions, you can side-step them by using +**send_immediately**:: mailer.send_immediately(message, fail_silently=False) -This will send the email immediately, outwith the transaction, so if it fails you have to deal with it manually. The ``fail_silently`` flag will swallow any connection errors silently - if it's not important whether the email gets sent. +This will send the email immediately, outwith the transaction, so if it fails +you have to deal with it manually. The ``fail_silently`` flag will swallow +any connection errors silently - if it's not important whether the email gets +sent. + +Getting Started (The Harder Way) +-------------------------------- + +To get started the harder way (without using ``config.include``), create an +instance of :class:`pyramid_mailer.mailer.Mailer`:: + + from pyramid_mailer.mailer import Mailer + + mailer = Mailer() + +The ``Mailer`` class can take a number of optional settings, detailed in +:ref:`configuration`. It's a good idea to create a single ``Mailer`` instance +for your application, and add it to your registry in your configuration +setup:: + + config = Configurator(settings=settings) + config.registry['mailer'] = Mailer.from_settings(settings) + +or alternatively:: + + from pyramid_mailer import mailer_factory_from_settings + config.registry['mailer'] = mailer_factory_from_settings(settings) + +You can then access your mailer in a view:: + + def my_view(request): + mailer = request.registry['mailer'] + +Note that the ``pyramid_mailer.get_mailer()`` API will not work if you +construct and seat your own mailer in this way. .. _configuration: Configuration ------------- -If you create your ``Mailer`` instance using :meth:`pyramid_mailer.mailer.Mailer.from_settings`, you can pass the settings from your .ini file or other source. By default, the prefix is assumed to be `mail.` although you can use another prefix if you wish. +If you create your ``Mailer`` instance using +:meth:`pyramid_mailer.mailer.Mailer.from_settings` or +``config.include('pyramid_mailer')``, you can pass the settings from your +.ini file or other source. By default, the prefix is assumed to be `mail.`. +If you use the ``config.include`` mechanism, to set another prefix, use the +``pyramid_mailer.prefix`` key in the config file, +e.g. ``pyramid_mailer.prefix = foo.``. If you use the +:meth:`pyramid_mailer.Mailer.Mailer.from_settings` or +:func:`pyramid_mailer.mailer_factory_from_settings` API, these accept a +prefix directly. ========================= =============== ===================== Setting Default Description @@ -110,12 +162,18 @@ Setting Default Description **mail.debug** **False** SMTP debug level ========================= =============== ===================== -**Note:** SSL will only work with **pyramid_mailer** if you are using Python **2.6** or higher, as it uses the SSL additions to the ``smtplib`` package. While it may be possible to work around this if you have to use Python 2.5 or lower, **pyramid_mailer** does not support this out of the box. +**Note:** SSL will only work with **pyramid_mailer** if you are using Python + **2.6** or higher, as it uses the SSL additions to the ``smtplib`` + package. While it may be possible to work around this if you have to use + Python 2.5 or lower, **pyramid_mailer** does not support this out of the + box. Transactions ------------ -If you are using transaction management with your Pyramid application then **pyramid_mailer** will only send the emails (or add them to the mail queue) when the transactions are committed. +If you are using transaction management with your Pyramid application then +**pyramid_mailer** will only send the emails (or add them to the mail queue) +when the transactions are committed. For example:: @@ -147,7 +205,8 @@ committed, and mail will be sent. Attachments ----------- -Attachments are added using the :class:`pyramid_mailer.message.Attachment` class:: +Attachments are added using the :class:`pyramid_mailer.message.Attachment` +class:: from pyramid_mailer.message import Attachment from pyramid_mailer.message import Message @@ -159,7 +218,8 @@ Attachments are added using the :class:`pyramid_mailer.message.Attachment` class message.attach(attachment) -You can pass the data either as a string or file object, so the above code could be rewritten:: +You can pass the data either as a string or file object, so the above code +could be rewritten:: from pyramid_mailer.message import Attachment from pyramid_mailer.message import Message @@ -176,28 +236,38 @@ You can pass the data either as a string or file object, so the above code could Unit tests ---------- -When running unit tests you probably don't want to actually send any emails inadvertently. However it's still useful to keep track of what emails would be sent in your tests. +When running unit tests you probably don't want to actually send any emails +inadvertently. However it's still useful to keep track of what emails would +be sent in your tests. -Another case is if your site is in development and you want to avoid accidental sending of any emails to customers. +Another case is if your site is in development and you want to avoid +accidental sending of any emails to customers. -In either case, the :class:`pyramid_mailer.mailer.DummyMailer` can be used:: +In either case, ``config.include('pyramid_mailer.testing')`` can be used to +make the current mailer an instance of the +:class:`pyramid_mailer.mailer.DummyMailer`:: + + from pyramid import testing class TestViews(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + self.config.include('pyramid_mailer.testing') + + def tearDown(self): + testing.tearDown() def test_some_view(self): - - from pyramid.config import Configurator from pyramid.testing import DummyRequest - from pyramid_mailer.mailer import DummyMailer - - config = Configurator() - mailer = DummyMailer() - config.registry['mailer'] = mailer - request = DummyRequest() + mailer = get_mailer(request) response = some_view(request) -The ``DummyMailer`` instance keeps track of emails "sent" in two properties: `queue` for emails send via :meth:`pyramid_mailer.mailer.Mailer.send_to_queue` and `outbox` for emails sent via :meth:`pyramid_mailer.mailer.Mailer.send`. Each stores the individual ``Message`` instances:: +The ``DummyMailer`` instance keeps track of emails "sent" in two properties: +`queue` for emails send via +:meth:`pyramid_mailer.mailer.Mailer.send_to_queue` and `outbox` for emails +sent via :meth:`pyramid_mailer.mailer.Mailer.send`. Each stores the +individual ``Message`` instances:: self.assertEqual(len(mailer.outbox) == 1) self.assertEqual(mailer.outbox[0].subject == "hello world") @@ -210,12 +280,13 @@ Queue When you send mail to a queue via :meth:`pyramid_mailer.Mailer.send_to_queue`, the mail will be placed into a -``maildir`` directory specified by the ``queue_path`` parameter or setting to :class:`pyramid_mailer.mailer.Mailer`. A -separate process will need to be launched to monitor this maildir and take -actions based on its state. Such a program comes as part of -`repoze_sendmail`_ (a dependency of the ``pyramid_mailer`` package). It is -known as ``qp``. ``qp`` will be installed into your Python (or virtualenv) -``bin`` or ``Scripts`` directory when you install ``repoze_sendmail``. +``maildir`` directory specified by the ``queue_path`` parameter or setting to +:class:`pyramid_mailer.mailer.Mailer`. A separate process will need to be +launched to monitor this maildir and take actions based on its state. Such a +program comes as part of `repoze_sendmail`_ (a dependency of the +``pyramid_mailer`` package). It is known as ``qp``. ``qp`` will be +installed into your Python (or virtualenv) ``bin`` or ``Scripts`` directory +when you install ``repoze_sendmail``. You'll need to arrange for ``qp`` to be a long-running process that monitors the maildir state.:: @@ -235,6 +306,8 @@ API .. autofunction:: mailer_factory_from_settings +.. autofunction:: get_mailer + .. module:: pyramid_mailer.mailer .. autoclass:: Mailer diff --git a/pyramid_mailer/__init__.py b/pyramid_mailer/__init__.py index 386f82a..f926ca9 100644 --- a/pyramid_mailer/__init__.py +++ b/pyramid_mailer/__init__.py @@ -1,4 +1,5 @@ from pyramid_mailer.mailer import Mailer +from pyramid_mailer.interfaces import IMailer def mailer_factory_from_settings(settings, prefix='mail.'): """ @@ -8,3 +9,21 @@ def mailer_factory_from_settings(settings, prefix='mail.'): :versionadded: 0.2.2 """ return Mailer.from_settings(settings, prefix) + +def includeme(config): + settings = config.settings + prefix = settings.get('pyramid_mailer.prefix', 'mail.') + mailer = mailer_factory_from_settings(settings, prefix=prefix) + config.registry.registerUtility(mailer, IMailer) + +def get_mailer(request): + """Obtain a mailer previously registered via + ``config.include('pyramid_mailer')`` or + ``config.include('pyramid_mailer.testing')``. + + :versionadded: 0.2.3 + """ + registry = getattr(request, 'registry', None) + if registry is None: + registry = request + return registry.getUtility(IMailer) diff --git a/pyramid_mailer/interfaces.py b/pyramid_mailer/interfaces.py new file mode 100644 index 0000000..ea0bbb3 --- /dev/null +++ b/pyramid_mailer/interfaces.py @@ -0,0 +1,5 @@ +from zope.interface import Interface + +class IMailer(Interface): + pass + diff --git a/pyramid_mailer/mailer.py b/pyramid_mailer/mailer.py index 050bc42..733a0fe 100644 --- a/pyramid_mailer/mailer.py +++ b/pyramid_mailer/mailer.py @@ -57,7 +57,7 @@ class SMTP_SSLMailer(SMTPMailer): # support disabled if pre-2.6 smtp = smtplib.SMTP_SSL ssl_support = True - except AttributeError: + except AttributeError: # pragma: no cover smtp = smtplib.SMTP ssl_support = False @@ -69,7 +69,7 @@ def __init__(self, *args, **kwargs): def smtp_factory(self): - if self.ssl_support is False: + if self.ssl_support is False: # pragma: no cover return super(SMTP_SSLMailer, self).smtp_factory() connection = self.smtp(self.hostname, str(self.port), @@ -158,7 +158,7 @@ def from_settings(cls, settings, prefix='mail.'): kwarg_names = [prefix + k for k in ( 'host', 'port', 'username', 'password', 'tls', 'ssl', 'keyfile', - 'certfile', 'queue_path', 'debug')] + 'certfile', 'queue_path', 'debug', 'default_sender')] size = len(prefix) diff --git a/pyramid_mailer/testing.py b/pyramid_mailer/testing.py new file mode 100644 index 0000000..7b11043 --- /dev/null +++ b/pyramid_mailer/testing.py @@ -0,0 +1,6 @@ +from pyramid_mailer.interfaces import IMailer +from pyramid_mailer.mailer import DummyMailer + +def includeme(config): + mailer = DummyMailer() + config.registry.registerUtility(mailer, IMailer) diff --git a/pyramid_mailer/tests.py b/pyramid_mailer/tests.py index 4caaf48..13e0d95 100644 --- a/pyramid_mailer/tests.py +++ b/pyramid_mailer/tests.py @@ -3,6 +3,8 @@ import unittest +from pyramid import testing + class TestAttachment(unittest.TestCase): def test_data_from_string(self): @@ -345,7 +347,7 @@ def test_send_immediately(self): from pyramid_mailer.mailer import Mailer from pyramid_mailer.message import Message - mailer = Mailer() + mailer = Mailer(host='localhost', port='28322') msg = Message(subject="testing", sender="sender@example.com", @@ -522,3 +524,78 @@ def test_from_settings(self): self.assert_(mailer.queue_delivery.queuePath == '/tmp') self.assert_(mailer.direct_delivery.mailer.debug_smtp == 1) +class Test_get_mailer(unittest.TestCase): + def _callFUT(self, arg): + from pyramid_mailer import get_mailer + return get_mailer(arg) + + def test_arg_is_registry(self): + mailer = object() + registry = DummyRegistry(mailer) + result = self._callFUT(registry) + self.assertEqual(result, mailer) + + def test_arg_is_request(self): + class Dummy(object): + pass + mailer = object() + registry = DummyRegistry(mailer) + request = Dummy() + request.registry = registry + result = self._callFUT(request) + self.assertEqual(result, mailer) + +class Test_includeme(unittest.TestCase): + def _callFUT(self, config): + from pyramid_mailer import includeme + includeme(config) + + def test_it_default_prefix(self): + from pyramid_mailer.interfaces import IMailer + registry = DummyRegistry() + settings = {'mail.default_sender':'sender'} + config = DummyConfig(registry, settings) + self._callFUT(config) + self.assertEqual(registry.registered[IMailer].default_sender, 'sender') + + def test_it_specified_prefix(self): + from pyramid_mailer.interfaces import IMailer + registry = DummyRegistry() + settings = {'pyramid_mailer.prefix':'foo.', + 'foo.default_sender':'sender'} + config = DummyConfig(registry, settings) + self._callFUT(config) + self.assertEqual(registry.registered[IMailer].default_sender, 'sender') + +class Test_testing_includeme(unittest.TestCase): + def _callFUT(self, config): + from pyramid_mailer.testing import includeme + includeme(config) + + def test_it(self): + from pyramid_mailer.interfaces import IMailer + from pyramid_mailer.mailer import DummyMailer + registry = DummyRegistry() + config = DummyConfig(registry, {}) + self._callFUT(config) + self.assertEqual(registry.registered[IMailer].__class__, DummyMailer) + +class DummyConfig(object): + def __init__(self, registry, settings): + self.registry = registry + self.settings = settings + +class DummyRegistry(object): + def __init__(self, result=None): + self.result = result + self.registered = {} + + def getUtility(self, iface): + return self.result + + def registerUtility(self, impl, iface): + self.registered[iface] = impl + + + +