← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:services-mail-print-function into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:services-mail-print-function into launchpad:master.

Commit message:
Port lp.services.mail to print_function

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/387928

I've ported some doctests to unicode_literals as well in the process.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:services-mail-print-function into launchpad:master.
diff --git a/lib/lp/services/mail/doc/emailauthentication.txt b/lib/lp/services/mail/doc/emailauthentication.txt
index 2a0b37f..e875e8c 100644
--- a/lib/lp/services/mail/doc/emailauthentication.txt
+++ b/lib/lp/services/mail/doc/emailauthentication.txt
@@ -123,7 +123,7 @@ message.
     GPGVerificationError: (7, 8, u'Bad signature')
 
     >>> getUtility(IGPGHandler).getVerifiedSignature(
-    ...     msg.signedContent.replace('\n', '\r\n'), msg.signature)
+    ...     msg.signedContent.replace(b'\n', b'\r\n'), msg.signature)
     <...PymeSignature...>
 
 authenticateEmail() doesn't have any problems verifying the signature:
@@ -135,7 +135,7 @@ authenticateEmail() doesn't have any problems verifying the signature:
     ...     msg.parsed_string = msg.as_string()
     ...     principal = authenticateEmail(msg, accept_any_timestamp)
     ...     authenticated_person = IPerson(principal)
-    ...     print authenticated_person.preferredemail.email
+    ...     print(authenticated_person.preferredemail.email)
     test@xxxxxxxxxxxxx
     test@xxxxxxxxxxxxx
 
@@ -147,19 +147,19 @@ starts failing, Python is probably fixed, so the manual boundary parsing
 hack can be removed.
 
     >>> msg = read_test_message('signed_folded_header.txt')
-    >>> print msg.parsed_string #doctest: -NORMALIZE_WHITESPACE
+    >>> print(msg.parsed_string) #doctest: -NORMALIZE_WHITESPACE
     Date:...
     ...
     Content-Type: multipart/mixed;
      boundary="--------------------EuxKj2iCbKjpUGkD"
     ...
 
-    >>> print msg.get_payload(i=0).as_string() #doctest: -NORMALIZE_WHITESPACE
+    >>> print(msg.get_payload(i=0).as_string()) #doctest: -NORMALIZE_WHITESPACE
     Content-Type: multipart/mixed; boundary="--------------------EuxKj2iCbKjpUGkD"
     ...
 
     >>> principal = authenticateEmail(msg, accept_any_timestamp)
-    >>> print IPerson(principal).displayname
+    >>> print(IPerson(principal).displayname)
     Sample Person
 
 
@@ -184,7 +184,7 @@ An unsigned email:
     >>> IWeaklyAuthenticatedPrincipal.providedBy(principal)
     True
 
-    >>> print launchbag.user.displayname
+    >>> print(launchbag.user.displayname)
     Foo Bar
     >>> launchbag.login
     'foo.bar@xxxxxxxxxxxxx'
@@ -197,7 +197,7 @@ authenticated user:
     >>> IWeaklyAuthenticatedPrincipal.providedBy(principal)
     True
 
-    >>> print launchbag.user.displayname
+    >>> print(launchbag.user.displayname)
     Sample Person
     >>> launchbag.login
     'testing@xxxxxxxxxxxxx'
@@ -211,7 +211,7 @@ principal.
     >>> IWeaklyAuthenticatedPrincipal.providedBy(principal)
     False
 
-    >>> print launchbag.user.displayname
+    >>> print(launchbag.user.displayname)
     Sample Person
     >>> launchbag.login
     'test@xxxxxxxxxxxxx'
diff --git a/lib/lp/services/mail/doc/mailbox.txt b/lib/lp/services/mail/doc/mailbox.txt
index 7761772..4da62d6 100644
--- a/lib/lp/services/mail/doc/mailbox.txt
+++ b/lib/lp/services/mail/doc/mailbox.txt
@@ -76,7 +76,7 @@ before:
     >>> id, raw_mail = mails[0]
     >>> import email
     >>> mail = email.message_from_string(raw_mail)
-    >>> print mail['Message-ID']
+    >>> print(mail['Message-ID'])
     <test1>
 
 When we're done using the mail box we have to close it:
@@ -91,7 +91,7 @@ Since we didn't delete the mail, it's still in there:
     2
     >>> id, raw_mail = mails[0]
     >>> mail = email.message_from_string(raw_mail)
-    >>> print mail['Message-ID']
+    >>> print(mail['Message-ID'])
     <test1>
 
 Let's delete all mails in the mail box:
diff --git a/lib/lp/services/mail/doc/notification-recipient-set.txt b/lib/lp/services/mail/doc/notification-recipient-set.txt
index 94e97be..7dea670 100644
--- a/lib/lp/services/mail/doc/notification-recipient-set.txt
+++ b/lib/lp/services/mail/doc/notification-recipient-set.txt
@@ -55,7 +55,7 @@ name.
 It's also possible to iterate over the recipients:
 
     >>> for person in recipients:
-    ...     print person.displayname
+    ...     print(person.displayname)
     Celso Providelo
     Sample Person
 
@@ -98,13 +98,17 @@ Obtaining the rationale
 You can obtain the rationale, header tuple by using the getReason()
 method:
 
-    >>> recipients.getReason(cprov)
-    ('You are notified for no reason.', 'Why not')
+    >>> def print_reason(reason):
+    ...     rationale, header = reason
+    ...     print('%s (%s)' % (header, rationale))
+
+    >>> print_reason(recipients.getReason(cprov))
+    Why not (You are notified for no reason.)
 
 You can also ask the reason associated with an email address:
 
-    >>> recipients.getReason('test@xxxxxxxxxxxxx')
-    ('You are notified because...', 'Unknown')
+    >>> print_reason(recipients.getReason('test@xxxxxxxxxxxxx'))
+    Unknown (You are notified because...)
 
 Requesting the reason for somebody that is not a recipient raises a
 UnknownRecipientError:
@@ -114,7 +118,7 @@ UnknownRecipientError:
       ...
     UnknownRecipientError: ...
 
-    >>> recipients.getReason('no-priv@xxxxxxxxxxxxx')
+    >>> recipients.getReason(six.ensure_str('no-priv@xxxxxxxxxxxxx'))
     Traceback (most recent call last):
       ...
     UnknownRecipientError: 'no-priv@xxxxxxxxxxxxx'
@@ -135,7 +139,7 @@ person:
 
     >>> ubuntu_team = person_set.getByName('ubuntu-team')
     >>> ignored = login_person(ubuntu_team.teamowner)
-    >>> print ubuntu_team.preferredemail.email
+    >>> print(ubuntu_team.preferredemail.email)
     support@xxxxxxxxxx
 
     >>> recipients.add(ubuntu_team, 'You are notified for fun.', 'Fun')
@@ -158,7 +162,7 @@ addresses are added to the recipients list, and this recursively.
 
     >>> recipients = NotificationRecipientSet()
     >>> ubuntu_gnome_team = person_set.getByName('name18')
-    >>> print ubuntu_gnome_team.preferredemail
+    >>> print(ubuntu_gnome_team.preferredemail)
     None
 
     >>> recipients.add(
@@ -182,7 +186,7 @@ be notified for they're a member of Warty Security Team, itself a member of
 Ubuntu Gnome Team:
 
     >>> warty_security_team = person_set.getByName('name20')
-    >>> print warty_security_team.displayname
+    >>> print(warty_security_team.displayname)
     Warty Security Team
 
     >>> sample_person.inTeam(warty_security_team)
@@ -202,11 +206,11 @@ Ubuntu Gnome Team:
 
 Their email will have the same rationale than the team:
 
-    >>> recipients.getReason(ubuntu_gnome_team)
-    ('Notified because a member of the team', 'Team')
+    >>> print_reason(recipients.getReason(ubuntu_gnome_team))
+    Team (Notified because a member of the team)
 
-    >>> recipients.getReason('test@xxxxxxxxxxxxx')
-    ('Notified because a member of the team', 'Team')
+    >>> print_reason(recipients.getReason('test@xxxxxxxxxxxxx'))
+    Team (Notified because a member of the team)
 
 
 Adding many persons at the same time
@@ -221,11 +225,11 @@ be added with the same rationale:
     >>> [person.displayname for person in recipients.getRecipients()]
     [u'No Privileges Person', u'Sample Person']
 
-    >>> recipients.getReason(no_priv)
-    ('Notified for fun.', 'Fun')
+    >>> print_reason(recipients.getReason(no_priv))
+    Fun (Notified for fun.)
 
-    >>> recipients.getReason(sample_person)
-    ('Notified for fun.', 'Fun')
+    >>> print_reason(recipients.getReason(sample_person))
+    Fun (Notified for fun.)
 
 
 Removing recipients
@@ -262,8 +266,8 @@ will be the one returned.
     >>> recipients.add(sample_person, 'A good reason', 'Good')
     >>> recipients.add(sample_person, 'Not a good reason', 'No good')
 
-    >>> recipients.getReason(sample_person)
-    ('A good reason', 'Good')
+    >>> print_reason(recipients.getReason(sample_person))
+    Good (A good reason)
 
 But if a person already had a rationale added through a team, the
 rationale specific to the person is used:
@@ -274,8 +278,8 @@ rationale specific to the person is used:
     ...     'Team')
     >>> recipients.add(sample_person, 'A more specific reason', 'Specific')
 
-    >>> recipients.getReason('test@xxxxxxxxxxxxx')
-    ('A more specific reason', 'Specific')
+    >>> print_reason (recipients.getReason('test@xxxxxxxxxxxxx'))
+    Specific (A more specific reason)
 
 Adding a rationale for another team won't override the one for the first
 one:
@@ -285,8 +289,8 @@ one:
     ...     warty_security_team, 'Member of Warty', 'Warty')
     >>> recipients.add(
     ...     ubuntu_gnome_team, 'Member of Ubuntu Gnome', 'Ubuntu Gnome')
-    >>> recipients.getReason('test@xxxxxxxxxxxxx')
-    ('Member of Warty', 'Warty')
+    >>> print_reason(recipients.getReason('test@xxxxxxxxxxxxx'))
+    Warty (Member of Warty)
 
 Nor adding a team rationale, when there is already one for the person:
 
@@ -294,8 +298,8 @@ Nor adding a team rationale, when there is already one for the person:
     >>> recipients.add(sample_person, 'Sample Person', 'Person')
     >>> recipients.add(
     ...     warty_security_team, 'Member of Warty.', 'Team')
-    >>> recipients.getReason('test@xxxxxxxxxxxxx')
-    ('Sample Person', 'Person')
+    >>> print_reason(recipients.getReason('test@xxxxxxxxxxxxx'))
+    Person (Sample Person)
 
 
 Merging recipients set
@@ -313,7 +317,7 @@ recipient is already part of the first set, the reason won't be updated.
     >>> recipients.update(other_recipients)
     >>> for person in recipients:
     ...     reason, code = recipients.getReason(person)
-    ...     print '%s: %s (%s)' % (person.displayname, code, reason)
+    ...     print('%s: %s (%s)' % (person.displayname, code, reason))
     Celso Providelo: B (Reason B)
     No Privileges Person: B (Reason B)
     Sample Person: A (Reason A)
diff --git a/lib/lp/services/mail/doc/sending-mail.txt b/lib/lp/services/mail/doc/sending-mail.txt
index 90da8d7..f4f0e7f 100644
--- a/lib/lp/services/mail/doc/sending-mail.txt
+++ b/lib/lp/services/mail/doc/sending-mail.txt
@@ -182,7 +182,7 @@ surrounded by quotes and quoted if necessary:
 
     >>> from_addr, to_addr, raw_message = stub.test_emails.pop()
     >>> msg = email.message_from_string(raw_message)
-    >>> print msg['From']
+    >>> print(msg['From'])
     "Foo \[Baz\] \" Bar" <foo.bar@xxxxxxxxxxxxx>
 
 
@@ -191,14 +191,14 @@ non-ASCII str object is passed, it will throw a UnicodeDecodeError.
 
     >>> simple_sendmail(
     ...     from_addr=u'foo.bar@xxxxxxxxxxxxx',
-    ...     to_addrs='test@xxxxxxxxxxxxx',
+    ...     to_addrs=b'test@xxxxxxxxxxxxx',
     ...     subject=u'Subject',
     ...     body=u'Content')
     '...launchpad@...'
 
     >>> simple_sendmail(
-    ...     from_addr='F\xf4\xf4 Bar <foo.bar@xxxxxxxxxxxxx>',
-    ...     to_addrs='test@xxxxxxxxxxxxx',
+    ...     from_addr=b'F\xf4\xf4 Bar <foo.bar@xxxxxxxxxxxxx>',
+    ...     to_addrs=b'test@xxxxxxxxxxxxx',
     ...     subject=u'Subject',
     ...     body=u'Content')
     Traceback (most recent call last):
@@ -207,15 +207,15 @@ non-ASCII str object is passed, it will throw a UnicodeDecodeError.
     ordinal not in range(128)
 
     >>> simple_sendmail(
-    ...     from_addr='foo.bar@xxxxxxxxxxxxx',
+    ...     from_addr=b'foo.bar@xxxxxxxxxxxxx',
     ...     to_addrs=u'test@xxxxxxxxxxxxx',
     ...     subject=u'Subject',
     ...     body=u'Content')
     '...launchpad@...'
 
     >>> simple_sendmail(
-    ...     from_addr='Foo Bar <foo.bar@xxxxxxxxxxxxx>',
-    ...     to_addrs=['S\xc4\x85mple Person <test@xxxxxxxxxxxxx>'],
+    ...     from_addr=b'Foo Bar <foo.bar@xxxxxxxxxxxxx>',
+    ...     to_addrs=[b'S\xc4\x85mple Person <test@xxxxxxxxxxxxx>'],
     ...     subject=u'Subject',
     ...     body=u'Content')
     Traceback (most recent call last):
@@ -249,7 +249,7 @@ that the precedence header was not added.
     'Forgot password'
     >>> msg.get_payload(decode=True)
     'Content'
-    >>> print msg['Precedence']
+    >>> print(msg['Precedence'])
     None
 
 
@@ -313,8 +313,9 @@ are ignored.
     >>> transaction.commit()
 
     >>> from_addr, to_addrs, raw_message = stub.test_emails.pop()
-    >>> to_addrs
-    ['no-priv@xxxxxxxxxxxxx']
+    >>> for to_addr in to_addrs:
+    ...     print(to_addr)
+    no-priv@xxxxxxxxxxxxx
 
     >>> sent_msg = email.message_from_string(raw_message)
     >>> sent_msg['To']
diff --git a/lib/lp/services/mail/doc/signedmessage.txt b/lib/lp/services/mail/doc/signedmessage.txt
index cd03bf8..f80d1ef 100644
--- a/lib/lp/services/mail/doc/signedmessage.txt
+++ b/lib/lp/services/mail/doc/signedmessage.txt
@@ -14,8 +14,10 @@ the attributes are correctly set.
     True
     >>> msg['To']
     'someone'
-    >>> msg.parsed_string
-    'To: someone\n\nHello.'
+    >>> print(msg.parsed_string)
+    To: someone
+    <BLANKLINE>
+    Hello.
 
 
 We have some test messages that can be easily accessed by
@@ -27,12 +29,12 @@ signature is inline with the signed content:
 
 You can access the headers of the message:
 
-    >>> print msg['From']
+    >>> print(msg['From'])
     Sample Person <test@xxxxxxxxxxxxx>
 
 The raw text that was signed is available as msg.signedContent:
 
-    >>> print msg.signedContent
+    >>> print(msg.signedContent)
     Some signed content.
     <BLANKLINE>
     With multiple paragraphs.
@@ -41,14 +43,14 @@ And to make it easier to work with, it's available as an email.message
 object as well:
 
     >>> signed_msg = msg.signedMessage
-    >>> print signed_msg.get_payload()
+    >>> print(signed_msg.get_payload())
     Some signed content.
     <BLANKLINE>
     With multiple paragraphs.
 
 Finally the signature can be accessed via msg.signature:
 
-    >>> print msg.signature
+    >>> print(msg.signature)
     -----BEGIN PGP SIGNATURE-----
     Version: GnuPG v1.2.5 (GNU/Linux)
     <BLANKLINE>
@@ -63,14 +65,14 @@ after the content has been signed, so the signed content should be
 unescaped.
 
     >>> msg = read_test_message('signed_dash_escaped.txt')
-    >>> print msg.get_payload()
+    >>> print(msg.get_payload())
     -----BEGIN PGP SIGNED MESSAGE-----
     ...
     - --
     Sample Person
     ...
 
-    >>> print msg.signedContent
+    >>> print(msg.signedContent)
     Some signed content.
     <BLANKLINE>
     --
@@ -84,7 +86,7 @@ contains of two MIME parts, the signed text, and the signature:
 
 The signed content includes the MIME headers as well:
 
-    >>> print msg.signedContent
+    >>> print(msg.signedContent)
     Content-Type: text/plain; charset=us-ascii
     Content-Disposition: inline
     <BLANKLINE>
@@ -93,15 +95,15 @@ The signed content includes the MIME headers as well:
 In signedMessage you can access the headers and the content
 separately:
 
-    >>> print msg.signedMessage['Content-Type']
+    >>> print(msg.signedMessage['Content-Type'])
     text/plain; charset=us-ascii
-    >>> print msg.signedMessage.get_payload()
+    >>> print(msg.signedMessage.get_payload())
     Some signed content.
 
 
 And of course the signature is accessible as well:
 
-    >>> print msg.signature
+    >>> print(msg.signature)
     -----BEGIN PGP SIGNATURE-----
     Version: GnuPG v1.2.5 (GNU/Linux)
     <BLANKLINE>
@@ -124,14 +126,14 @@ It handles signed multipart messages as well:
 
     >>> msg = read_test_message('signed_multipart.txt')
     >>> content, attachment = msg.signedMessage.get_payload()
-    >>> print content.get_payload()
+    >>> print(content.get_payload())
     Some signed content.
     <BLANKLINE>
-    >>> print attachment.get_payload()
+    >>> print(attachment.get_payload())
     A signed attachment.
     <BLANKLINE>
 
-    >>> print msg.signature
+    >>> print(msg.signature)
     -----BEGIN PGP SIGNATURE-----
     Version: GnuPG v1.2.5 (GNU/Linux)
     <BLANKLINE>
@@ -141,7 +143,7 @@ It handles signed multipart messages as well:
     -----END PGP SIGNATURE-----
 
     >>> msg = read_test_message('signed_folded_header.txt')
-    >>> print msg.signedContent #doctest: -NORMALIZE_WHITESPACE
+    >>> print(msg.signedContent) #doctest: -NORMALIZE_WHITESPACE
     Content-Type: multipart/mixed;
      boundary="--------------------EuxKj2iCbKjpUGkD"
     ...
diff --git a/lib/lp/services/mail/notification.py b/lib/lp/services/mail/notification.py
index 7db0f55..adf0f95 100644
--- a/lib/lp/services/mail/notification.py
+++ b/lib/lp/services/mail/notification.py
@@ -2,6 +2,8 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 """Event handlers that send email notifications."""
 
+from __future__ import absolute_import, print_function
+
 __metaclass__ = type
 __all__ = [
     'send_process_error_notification',
@@ -83,10 +85,10 @@ def get_unified_diff(old_text, new_text, text_width):
     Before the diff is produced, the texts are wrapped to the given text
     width.
 
-        >>> print get_unified_diff(
+        >>> print(get_unified_diff(
         ...     'Some text\nAnother line\n',get_un
         ...     'Some more text\nAnother line\n',
-        ...     text_width=72)
+        ...     text_width=72))
         - Some text
         + Some more text
           Another line
diff --git a/lib/lp/services/mail/tests/incomingmail.txt b/lib/lp/services/mail/tests/incomingmail.txt
index 996e23d..3982bb1 100644
--- a/lib/lp/services/mail/tests/incomingmail.txt
+++ b/lib/lp/services/mail/tests/incomingmail.txt
@@ -122,7 +122,7 @@ wasn't a handler registered for that domain, an OOPS was recorded with
 a link to the original message.
 
     >>> from lp.testing.mail_helpers import pop_notifications
-    >>> print str(pop_notifications()[-1])
+    >>> print(str(pop_notifications()[-1]))
     From ...
     Content-Type: multipart/mixed...
     ...
@@ -184,7 +184,7 @@ silently rejected.
     >>> person_set = getUtility(IPersonSet)
     >>> bigjools = person_set.getByEmail('launchpad@xxxxxxxxxxxxxxxxxx',
     ...                                  filter_status=False)
-    >>> print bigjools.account_status.name
+    >>> print(bigjools.account_status.name)
     NOACCOUNT
 
     >>> msg = read_test_message('unsigned_inactive.txt')
@@ -259,7 +259,7 @@ And submit an email to the handler.
 An exception is raised, an OOPS is recorded, and an email is sent back
 to the user, citing the OOPS ID, with the original message attached.
 
-    >>> print str(pop_notifications()[-1])
+    >>> print(str(pop_notifications()[-1]))
     From ...
     Content-Type: multipart/mixed...
     ...
@@ -303,7 +303,7 @@ reporting in the web interface, are not ignored in the email interface.
     ...
     Unauthorized
 
-    >>> print str(pop_notifications()[-1])
+    >>> print(str(pop_notifications()[-1]))
     From ...
     Content-Type: multipart/mixed...
     ...
diff --git a/lib/lp/services/mail/tests/test_doc.py b/lib/lp/services/mail/tests/test_doc.py
index ed0f572..7a3f8ba 100644
--- a/lib/lp/services/mail/tests/test_doc.py
+++ b/lib/lp/services/mail/tests/test_doc.py
@@ -49,12 +49,14 @@ class ProcessMailLayer(LaunchpadZopelessLayer):
 special = {
     'emailauthentication.txt': LayeredDocFileSuite(
         '../doc/emailauthentication.txt',
-        setUp=setUp, tearDown=tearDown,
+        setUp=lambda test: setUp(test, future=True), tearDown=tearDown,
         layer=ProcessMailLayer,
         stdout_logging=False)
     }
 
 
 def test_suite():
-    suite = build_test_suite(here, special, layer=DatabaseFunctionalLayer)
+    suite = build_test_suite(
+        here, special, setUp=lambda test: setUp(test, future=True),
+        layer=DatabaseFunctionalLayer)
     return suite
diff --git a/lib/lp/services/mail/tests/test_incoming.py b/lib/lp/services/mail/tests/test_incoming.py
index 28bac00..3d2e625 100644
--- a/lib/lp/services/mail/tests/test_incoming.py
+++ b/lib/lp/services/mail/tests/test_incoming.py
@@ -1,7 +1,6 @@
 # Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-
 from doctest import DocTestSuite
 from email.mime.multipart import MIMEMultipart
 import logging
@@ -43,7 +42,10 @@ from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.gpgkeys import import_secret_test_key
 from lp.testing.layers import LaunchpadZopelessLayer
 from lp.testing.mail_helpers import pop_notifications
-from lp.testing.systemdocs import LayeredDocFileSuite
+from lp.testing.systemdocs import (
+    LayeredDocFileSuite,
+    setGlobs,
+    )
 
 
 @implementer(IMailHandler)
@@ -344,6 +346,7 @@ class TestExtractAddresses(TestCaseWithFactory):
 
 
 def setUp(test):
+    setGlobs(test, future=True)
     test._old_policy = setSecurityPolicy(LaunchpadSecurityPolicy)
     switch_dbuser(config.processmail.dbuser)
 
diff --git a/lib/lp/services/mail/tests/test_mbox_mailer.py b/lib/lp/services/mail/tests/test_mbox_mailer.py
index 078bb32..f46248e 100644
--- a/lib/lp/services/mail/tests/test_mbox_mailer.py
+++ b/lib/lp/services/mail/tests/test_mbox_mailer.py
@@ -9,11 +9,15 @@ import tempfile
 
 from zope.testing.cleanup import cleanUp
 
-from lp.testing.systemdocs import LayeredDocFileSuite
+from lp.testing.systemdocs import (
+    LayeredDocFileSuite,
+    setGlobs,
+    )
 
 
 def setup(testobj):
     """Set up for doc test"""
+    setGlobs(testobj, future=True)
     fd, mbox_filename = tempfile.mkstemp()
     os.close(fd)
     testobj.globs['mbox_filename'] = mbox_filename
diff --git a/lib/lp/services/mail/tests/test_stub.py b/lib/lp/services/mail/tests/test_stub.py
index 8945153..3ea22fb 100644
--- a/lib/lp/services/mail/tests/test_stub.py
+++ b/lib/lp/services/mail/tests/test_stub.py
@@ -1,6 +1,8 @@
 # Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function
+
 __metaclass__ = type
 
 from doctest import DocTestSuite
@@ -64,7 +66,7 @@ def test_simple_sendmail():
 
     >>> sorted_test_emails = sorted(list(stub.test_emails))
     >>> for from_addr, to_addrs, raw_message in sorted_test_emails:
-    ...     print from_addr, to_addrs, 'nobody@xxxxxxxxxxx' in raw_message
+    ...     print(from_addr, to_addrs, 'nobody@xxxxxxxxxxx' in raw_message)
     bounces@xxxxxxxxxxxxx ['nobody2@xxxxxxxxxxx'] True
     bounces@xxxxxxxxxxxxx ['nobody2@xxxxxxxxxxx'] False