← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/answers-pagetests-future-imports into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/answers-pagetests-future-imports into lp:launchpad.

Commit message:
Convert pagetests under lp.answers to Launchpad's preferred __future__ imports.

Requested reviews:
  Colin Watson (cjwatson)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/answers-pagetests-future-imports/+merge/347329
-- 
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.
=== modified file 'lib/lp/answers/browser/tests/faq-views.txt'
--- lib/lp/answers/browser/tests/faq-views.txt	2017-12-24 15:45:26 +0000
+++ lib/lp/answers/browser/tests/faq-views.txt	2018-06-02 19:58:15 +0000
@@ -21,7 +21,7 @@
     >>> view = create_initialized_view(
     ...     firefox, '+portlet-listfaqs', principal=firefox.owner)
     >>> for faq in view.latest_faqs:
-    ...     print faq.title
+    ...     print(faq.title)
     A FAQ
     What's the keyboard shortcut for [random feature]?
     How do I install plugins (Shockwave, QuickTime, etc.)?
@@ -29,31 +29,31 @@
     How do I install Extensions?
 
     >>> content = find_tag_by_id(view.render(), 'portlet-latest-faqs')
-    >>> print content.h2
+    >>> print(content.h2)
     <h2>...FAQs for Mozilla Firefox </h2>
 
-    >>> print extract_text(content.ul)
+    >>> print(extract_text(content.ul))
     A FAQ
     What's the keyboard shortcut for [random feature]?...
 
 Each FAQ is linked.
 
-    >>> print content.find('a', {'class': 'sprite faq'})
+    >>> print(content.find('a', {'class': 'sprite faq'}))
     <a class="..." href="http://answers.../firefox/+faq/...";>A FAQ</a>
 
 The portlet has a form to search FAQs. The view provides the action URL so
 that the form works from any page.
 
-    >>> print view.portlet_action
+    >>> print(view.portlet_action)
     http://answers.launchpad.dev/firefox/+faqs
 
-    >>> print content.form['action']
+    >>> print(content.form['action'])
     http://answers.launchpad.dev/firefox/+faqs
 
 The portlet provides a link to create a FAQ when the user that has append
 permission, such as the project owner.
 
-    >>> print content.find('a', {'class': 'menu-link-create_faq sprite add'})
+    >>> print(content.find('a', {'class': 'menu-link-create_faq sprite add'}))
     <a class="..." href=".../firefox/+createfaq">Create a new FAQ</a>
 
 Other users do not see the link.
@@ -63,5 +63,5 @@
     >>> view = create_initialized_view(
     ...     firefox, '+portlet-listfaqs', principal=user)
     >>> content = find_tag_by_id(view.render(), 'portlet-latest-faqs')
-    >>> print content.find('a', {'class': 'menu-link-create_faq sprite add'})
+    >>> print(content.find('a', {'class': 'menu-link-create_faq sprite add'}))
     None

=== modified file 'lib/lp/answers/browser/tests/question-subscribe_me.txt'
--- lib/lp/answers/browser/tests/question-subscribe_me.txt	2011-12-24 17:49:30 +0000
+++ lib/lp/answers/browser/tests/question-subscribe_me.txt	2018-06-02 19:58:15 +0000
@@ -86,9 +86,9 @@
 Make sure that whenever the view actions is modified, this test
 requires update:
 
-    >>> print "\n".join(sorted(
+    >>> print("\n".join(sorted(
     ...     action.__name__.split('.')[-1]
-    ...     for action in workflow_harness.view.actions))
+    ...     for action in workflow_harness.view.actions)))
     answer
     comment
     confirm

=== modified file 'lib/lp/answers/browser/tests/test_views.py'
--- lib/lp/answers/browser/tests/test_views.py	2017-10-25 10:02:12 +0000
+++ lib/lp/answers/browser/tests/test_views.py	2018-06-02 19:58:15 +0000
@@ -51,13 +51,16 @@
     suite = unittest.TestSuite()
     loader = unittest.TestLoader()
     suite.addTest(loader.loadTestsFromTestCase(TestEmailObfuscated))
-    suite.addTest(LayeredDocFileSuite('question-subscribe_me.txt',
-                  setUp=setUp, tearDown=tearDown,
-                  layer=DatabaseFunctionalLayer))
-    suite.addTest(LayeredDocFileSuite('views.txt',
-                  setUp=setUp, tearDown=tearDown,
-                  layer=DatabaseFunctionalLayer))
-    suite.addTest(LayeredDocFileSuite('faq-views.txt',
-                  setUp=setUp, tearDown=tearDown,
-                  layer=DatabaseFunctionalLayer))
+    suite.addTest(LayeredDocFileSuite(
+        'question-subscribe_me.txt',
+        setUp=lambda test: setUp(test, future=True), tearDown=tearDown,
+        layer=DatabaseFunctionalLayer))
+    suite.addTest(LayeredDocFileSuite(
+        'views.txt',
+        setUp=lambda test: setUp(test, future=True), tearDown=tearDown,
+        layer=DatabaseFunctionalLayer))
+    suite.addTest(LayeredDocFileSuite(
+        'faq-views.txt',
+        setUp=lambda test: setUp(test, future=True), tearDown=tearDown,
+        layer=DatabaseFunctionalLayer))
     return suite

=== modified file 'lib/lp/answers/browser/tests/views.txt'
--- lib/lp/answers/browser/tests/views.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/answers/browser/tests/views.txt	2018-06-02 19:58:15 +0000
@@ -27,10 +27,10 @@
 Subscription is done when the user click on the 'Subscribe' button.
 
     >>> view = create_initialized_view(question_three, name='+subscribe')
-    >>> print view.label
+    >>> print(view.label)
     Subscribe to question
 
-    >>> print view.page_title
+    >>> print(view.page_title)
     Subscription
 
     >>> form = {'subscribe': 'Subscribe'}
@@ -43,7 +43,7 @@
 question view page.
 
     >>> for notice in view.request.notifications:
-    ...     print notice.message
+    ...     print(notice.message)
     You have subscribed to this question.
 
     >>> view.request.response.getHeader('Location')
@@ -52,7 +52,7 @@
 Unsubscription works in a similar manner.
 
     >>> view = create_initialized_view(question_three, name='+subscribe')
-    >>> print view.label
+    >>> print(view.label)
     Unsubscribe from question
 
     >>> form = {'subscribe': 'Unsubscribe'}
@@ -62,7 +62,7 @@
     False
 
     >>> for notice in view.request.notifications:
-    ...     print notice.message
+    ...     print(notice.message)
     You have unsubscribed from this question.
 
     >>> view.request.response.getHeader('Location')
@@ -84,37 +84,37 @@
     >>> workflow_harness = LaunchpadFormHarness(
     ...     firefox_question, QuestionWorkflowView)
 
-    # Let's define a helper method that will return the names of the
+    # Let's define a helper method that will print the names of the
     # available actions.
 
-    >>> def getAvailableActionNames(view):
+    >>> def printAvailableActionNames(view):
     ...     names = [action.__name__.split('.')[-1]
     ...              for action in view.actions
     ...              if action.available()]
-    ...     return sorted(names)
+    ...     for name in sorted(names):
+    ...         print(name)
 
 Unlogged-in users cannot post any comments on the question:
 
     >>> login(ANONYMOUS)
     >>> workflow_harness.submit('', {})
-    >>> getAvailableActionNames(workflow_harness.view)
-    []
+    >>> printAvailableActionNames(workflow_harness.view)
 
 When question is in the OPEN state, the owner can comment, answer their
 own question or provide more information.
 
     >>> login('test@xxxxxxxxxxxxx')
     >>> workflow_harness.submit('', {})
-    >>> getAvailableActionNames(workflow_harness.view)
-    ['comment', 'giveinfo', 'selfanswer']
+    >>> printAvailableActionNames(workflow_harness.view)
+    comment giveinfo selfanswer
 
 But when another user sees the question, they can comment, provide an
 answer or request more information.
 
     >>> login('no-priv@xxxxxxxxxxxxx')
     >>> workflow_harness.submit('', {})
-    >>> getAvailableActionNames(workflow_harness.view)
-    ['answer', 'comment', 'requestinfo']
+    >>> printAvailableActionNames(workflow_harness.view)
+    answer comment requestinfo
 
 When the other user requests for more information, a confirmation is
 displayed, the question status is changed to NEEDSINFO and the user is
@@ -125,10 +125,10 @@
     ...         'field.message': 'Can you provide an example of an URL'
     ...             'displaying the problem?'})
     >>> for notification in workflow_harness.request.response.notifications:
-    ...     print notification.message
+    ...     print(notification.message)
     Thanks for your information request.
 
-    >>> print firefox_question.status.name
+    >>> print(firefox_question.status.name)
     NEEDSINFO
 
     >>> workflow_harness.redirectionTarget()
@@ -137,15 +137,15 @@
 The available actions for that other user are still comment, give an
 answer or request more information:
 
-    >>> getAvailableActionNames(workflow_harness.view)
-    ['answer', 'comment', 'requestinfo']
+    >>> printAvailableActionNames(workflow_harness.view)
+    answer comment requestinfo
 
 And the question owner still has the same possibilities as at first:
 
     >>> login('test@xxxxxxxxxxxxx')
     >>> workflow_harness.submit('', {})
-    >>> getAvailableActionNames(workflow_harness.view)
-    ['comment', 'giveinfo', 'selfanswer']
+    >>> printAvailableActionNames(workflow_harness.view)
+    comment giveinfo selfanswer
 
 If they reply with the requested information, the question is moved back
 to the OPEN state.
@@ -156,10 +156,10 @@
     ...     }
     >>> workflow_harness.submit('giveinfo', form)
     >>> for notification in workflow_harness.request.response.notifications:
-    ...     print notification.message
+    ...     print(notification.message)
     Thanks for adding more information to your question.
 
-    >>> print firefox_question.status.name
+    >>> print(firefox_question.status.name)
     OPEN
 
     >>> workflow_harness.redirectionTarget()
@@ -174,10 +174,10 @@
     ...             "available with SVG support enabled. Using apt-get or "
     ...             "adept you should be able to upgrade."})
     >>> for notification in workflow_harness.request.response.notifications:
-    ...     print notification.message
+    ...     print(notification.message)
     Thanks for your answer.
 
-    >>> print firefox_question.status.name
+    >>> print(firefox_question.status.name)
     ANSWERED
 
     >>> workflow_harness.redirectionTarget()
@@ -190,8 +190,8 @@
 
     >>> login('test@xxxxxxxxxxxxx')
     >>> workflow_harness.submit('', {})
-    >>> getAvailableActionNames(workflow_harness.view)
-    ['comment', 'confirm', 'reopen', 'selfanswer']
+    >>> printAvailableActionNames(workflow_harness.view)
+    comment confirm reopen selfanswer
 
 Let's say they confirm the previous answer, in this case, the question
 will move to the 'SOLVED' state. Note that the UI doesn't enable the
@@ -202,10 +202,10 @@
     ...     'confirm', {'answer_id': answer_message_number,
     ...                 'field.message': ''})
     >>> for notification in workflow_harness.request.response.notifications:
-    ...     print notification.message
+    ...     print(notification.message)
     Thanks for your feedback.
 
-    >>> print firefox_question.status.name
+    >>> print(firefox_question.status.name)
     SOLVED
 
     >>> workflow_harness.redirectionTarget()
@@ -213,15 +213,15 @@
 
 Since no confirmation message was given, a default one was used.
 
-    >>> print firefox_question.messages[-1].text_contents
+    >>> print(firefox_question.messages[-1].text_contents)
     Thanks No Privileges Person, that solved my question.
 
 Once in the SOLVED state, when the answerer is a person other than the
 question owner, the owner can now only either add a comment or reopen
 the question:
 
-    >>> getAvailableActionNames(workflow_harness.view)
-    ['comment', 'reopen']
+    >>> printAvailableActionNames(workflow_harness.view)
+    comment reopen
 
 Adding a comment doesn't change the status:
 
@@ -230,21 +230,21 @@
     ...         'field.message': "The example now displays "
     ...         "correctly. Thanks."})
     >>> for notification in workflow_harness.request.response.notifications:
-    ...     print notification.message
+    ...     print(notification.message)
     Thanks for your comment.
 
     >>> workflow_harness.redirectionTarget()
     '.../+question/2'
 
-    >>> print firefox_question.status.name
+    >>> print(firefox_question.status.name)
     SOLVED
 
 And the other user can only comment on the question:
 
     >>> login('no-priv@xxxxxxxxxxxxx')
     >>> workflow_harness.submit('', {})
-    >>> getAvailableActionNames(workflow_harness.view)
-    ['comment']
+    >>> printAvailableActionNames(workflow_harness.view)
+    comment
 
 If the question owner reopens the question, its status is changed back
 to 'OPEN'.
@@ -257,10 +257,10 @@
     ...         "http://people.w3.org/maxf/ChessGML/immortal.svg doesn't "
     ...         "display correctly."})
     >>> for notification in workflow_harness.request.response.notifications:
-    ...     print notification.message
+    ...     print(notification.message)
     Your question was reopened.
 
-    >>> print firefox_question.status.name
+    >>> print(firefox_question.status.name)
     OPEN
 
     >>> workflow_harness.redirectionTarget()
@@ -275,14 +275,14 @@
     ...         'field.message': "OK, this example requires some "
     ...         "SVG features that will only be available in Firefox 2.0."})
     >>> for notification in workflow_harness.request.response.notifications:
-    ...     print notification.message
+    ...     print(notification.message)
     Your question is solved. If a particular message helped you solve the
     problem, use the <em>'This solved my problem'</em> button.
 
-    >>> print firefox_question.status.name
+    >>> print(firefox_question.status.name)
     SOLVED
 
-    >>> print firefox_question.answerer.displayname
+    >>> print(firefox_question.answerer.displayname)
     Sample Person
 
     >>> firefox_question.answer is None
@@ -297,19 +297,19 @@
 then attribute an answerer as a contributor to the solution. The
 answerer's message is attributed as the answer in this case.
 
-    >>> getAvailableActionNames(workflow_harness.view)
-    ['comment', 'confirm', 'reopen']
+    >>> printAvailableActionNames(workflow_harness.view)
+    comment confirm reopen
 
     >>> workflow_harness.submit(
     ...     'confirm', {'answer_id': answer_message_number,
     ...                 'field.message': ''})
-    >>> print firefox_question.status.name
+    >>> print(firefox_question.status.name)
     SOLVED
 
-    >>> print firefox_question.answerer.displayname
+    >>> print(firefox_question.answerer.displayname)
     No Privileges Person
 
-    >>> print firefox_question.answer.owner.displayname
+    >>> print(firefox_question.answer.owner.displayname)
     No Privileges Person
 
     >>> answer_id = firefox_question.messages[answer_message_number].id
@@ -338,13 +338,13 @@
     []
 
     >>> makebug.initialize()
-    >>> print question_three.bugs[0].title
+    >>> print(question_three.bugs[0].title)
     Bug title
 
-    >>> print question_three.bugs[0].description
+    >>> print(question_three.bugs[0].description)
     Bug description.
 
-    >>> print makebug.user.name
+    >>> print(makebug.user.name)
     name16
 
     >>> question_three.bugs[0].isSubscribed(makebug.user)
@@ -367,7 +367,7 @@
     >>> makebug = getMultiAdapter((question_three, request), name='+makebug')
     >>> makebug.initialize()
     >>> for n in request.notifications:
-    ...     print n.message
+    ...     print(n.message)
     You cannot create a bug report...
 
 
@@ -395,10 +395,10 @@
     >>> view = getMultiAdapter((firefox_question, request), name='+reject')
     >>> view.initialize()
     >>> for notice in request.notifications:
-    ...     print notice.message
+    ...     print(notice.message)
     You have rejected this question.
 
-    >>> print firefox_question.status.title
+    >>> print(firefox_question.status.title)
     Invalid
 
 
@@ -417,10 +417,10 @@
     ...     (firefox_question, request), name='+change-status')
     >>> view.initialize()
     >>> for notice in request.notifications:
-    ...     print notice.message
+    ...     print(notice.message)
     Question status updated.
 
-    >>> print firefox_question.status.title
+    >>> print(firefox_question.status.title)
     Solved
 
 
@@ -447,13 +447,13 @@
 
     >>> view = getMultiAdapter((question_three, request), name='+edit')
     >>> view.initialize()
-    >>> print question_three.distribution.name
+    >>> print(question_three.distribution.name)
     ubuntu
 
-    >>> print question_three.sourcepackagename.name
+    >>> print(question_three.sourcepackagename.name)
     mozilla-firefox
 
-    >>> print question_three.product
+    >>> print(question_three.product)
     None
 
 Since a user must have launchpad.Edit (question creator or target answer
@@ -496,10 +496,10 @@
     >>> question_three.description
     u'A better description.'
 
-    >>> print question_three.assignee.displayname
+    >>> print(question_three.assignee.displayname)
     Foo Bar
 
-    >>> print question_three.whiteboard
+    >>> print(question_three.whiteboard)
     Some note
 
 The question language can be set to any language registered with
@@ -532,13 +532,13 @@
     >>> question_three.sourcepackagename is None
     True
 
-    >>> print question_three.distribution
-    None
-
-    >>> print question_three.sourcepackagename
-    None
-
-    >>> print question_three.product.name
+    >>> print(question_three.distribution)
+    None
+
+    >>> print(question_three.sourcepackagename)
+    None
+
+    >>> print(question_three.product.name)
     firefox
 
     # Reassign back the question to ubuntu
@@ -743,7 +743,7 @@
     <lp.services.webapp.batching.BatchNavigator ...>
 
     >>> for question in questions.batch:
-    ...     print question.title.encode('us-ascii', 'backslashreplace')
+    ...     print(question.title.encode('us-ascii', 'backslashreplace'))
     Problema al recompilar kernel con soporte smp (doble-n\xfacleo)
     Continue playing after shutdown
     Play DVDs in Totem
@@ -761,7 +761,7 @@
     >>> search_view = search_view_harness.view
     >>> questions = search_view.searchResults()
     >>> for question in questions.batch:
-    ...     print question.title, question.status.title
+    ...     print(question.title, question.status.title)
     mailto: problem in webpage Solved
 
 Specific views can provide a default filter by returning the default
@@ -779,14 +779,14 @@
     >>> search_view = search_view_harness.view
     >>> questions = search_view.searchResults()
     >>> for question in questions.batch:
-    ...     print question.title
+    ...     print(question.title)
     mailto: problem in webpage
     Better Title
 
 The status widget displays the default criteria used:
 
     >>> for status in search_view.widgets['status']._getFormValue():
-    ...     print status.title
+    ...     print(status.title)
     Solved
     Invalid
 
@@ -801,11 +801,11 @@
     >>> search_view = search_view_harness.view
     >>> questions = search_view.searchResults()
     >>> for question in questions.batch:
-    ...     print question.title
+    ...     print(question.title)
     mailto: problem in webpage
 
     >>> for status in search_view.widgets['status']._getFormValue():
-    ...     print status.title
+    ...     print(status.title)
     Solved
 
 The base view computes the page heading and the message displayed when
@@ -813,19 +813,19 @@
 
     >>> from zope.i18n import translate
     >>> search_view_harness.submit('', {})
-    >>> print translate(search_view_harness.view.page_title)
+    >>> print(translate(search_view_harness.view.page_title))
     Questions for Ubuntu
 
-    >>> print translate(search_view_harness.view.empty_listing_message)
+    >>> print(translate(search_view_harness.view.empty_listing_message))
     There are no questions for Ubuntu with the requested statuses.
 
     >>> MyCustomSearchQuestionsView.default_filter = dict(
     ...     status=[QuestionStatus.OPEN], search_text='Firefox')
     >>> search_view_harness.submit('', {})
-    >>> print translate(search_view_harness.view.page_title)
+    >>> print(translate(search_view_harness.view.page_title))
     Open questions matching "Firefox" for Ubuntu
 
-    >>> print translate(search_view_harness.view.empty_listing_message)
+    >>> print(translate(search_view_harness.view.empty_listing_message))
     There are no open questions matching "Firefox" for Ubuntu.
 
 It works also with user submitted values:
@@ -835,10 +835,10 @@
     ...     'field.search_text': '',
     ...     'field.language': ['en'],
     ...     'field.sort': 'by relevancy'})
-    >>> print translate(search_view_harness.view.page_title)
+    >>> print(translate(search_view_harness.view.page_title))
     Expired questions for Ubuntu
 
-    >>> print translate(search_view_harness.view.empty_listing_message)
+    >>> print(translate(search_view_harness.view.empty_listing_message))
     There are no expired questions for Ubuntu.
 
     >>> search_view_harness.submit('search', {
@@ -846,10 +846,10 @@
     ...     'field.search_text': 'evolution',
     ...     'field.language': ['en'],
     ...     'field.sort': 'by relevancy'})
-    >>> print translate(search_view_harness.view.page_title)
+    >>> print(translate(search_view_harness.view.page_title))
     Questions matching "evolution" for Ubuntu
 
-    >>> print translate(search_view_harness.view.empty_listing_message)
+    >>> print(translate(search_view_harness.view.empty_listing_message))
     There are no questions matching "evolution" for Ubuntu with the
     requested statuses.
 
@@ -874,7 +874,7 @@
 
     >>> table = find_tag_by_id(view.render(), 'question-listing')
     >>> for row in table.findAll('tr'):
-    ...     print extract_text(row)
+    ...     print(extract_text(row))
     Summary                Created     Submitter      Assignee  Status
     6 Newly installed...  2005-10-14   Sample Person  &mdash;   Answered ...
 
@@ -891,7 +891,7 @@
 
     >>> table = find_tag_by_id(view.render(), 'question-listing')
     >>> for row in table.findAll('tr'):
-    ...     print extract_text(row)
+    ...     print(extract_text(row))
     Summary  Created     Submitter      Source Package   Assignee  Status ...
     8 ...    2006-07-20  Sample Person  mozilla-firefox  &mdash;   Answered
     7 ...    2005-10-14  Foo Bar        &mdash;          &mdash;   Needs ...
@@ -911,7 +911,7 @@
 
     >>> table = find_tag_by_id(view.render(), 'question-listing')
     >>> for row in table.findAll('tr'):
-    ...     print extract_text(row)
+    ...     print(extract_text(row))
     Summary  Created     Submitter      In               Assignee  Status
     6 ...    2005-10-14  Sample Person  Mozilla Firefox  &mdash;   Answered...
 
@@ -931,7 +931,7 @@
 
     >>> table = find_tag_by_id(view.render(), 'question-listing')
     >>> for row in table.findAll('tr'):
-    ...     print extract_text(row)
+    ...     print(extract_text(row))
     Summary  Created     Submitter      Assignee  Status
     6 ...    2005-10-14  Sample Person  Bob       Answered
     4 ...    2005-09-05  Foo Bar        &mdash;   Open ...
@@ -972,7 +972,7 @@
 The view adds notifications about the answer contacts added:
 
     >>> for notification in request.notifications:
-    ...     print notification.message
+    ...     print(notification.message)
     <...Your preferred languages... were updated to include ...English (en).
     You have been added as an answer contact for Ubuntu.
     English was added to Ubuntu Team's ...preferred languages...
@@ -998,7 +998,7 @@
     [u'Daniel Silverstone', u'Jeff Waugh', u'Ubuntu Team']
 
     >>> for notification in request.notifications:
-    ...     print notification.message
+    ...     print(notification.message)
     <...Your preferred languages... were updated to include ...English (en).
     You have been added as an answer contact for Ubuntu.
 
@@ -1017,7 +1017,7 @@
     [u'Jeff Waugh', u'Ubuntu Team']
 
     >>> for notification in request.notifications:
-    ...     print notification.message
+    ...     print(notification.message)
     You have been removed as an answer contact for Ubuntu.
 
 It can also be used to remove a team registration when the user is a
@@ -1037,5 +1037,5 @@
     [u'Jeff Waugh']
 
     >>> for notification in request.notifications:
-    ...     print notification.message
+    ...     print(notification.message)
     Ubuntu Team has been removed as an answer contact for Ubuntu.

=== modified file 'lib/lp/answers/stories/answer-contact-report.txt'
--- lib/lp/answers/stories/answer-contact-report.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/answers/stories/answer-contact-report.txt	2018-06-02 19:58:15 +0000
@@ -7,14 +7,14 @@
 
     >>> anon_browser.open('http://answers.launchpad.dev/~no-priv')
     >>> anon_browser.getLink('Answer contact for').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Projects for which...
 
 Since No Privileges Person is not an answer contact, the report states
 that.
 
     >>> content = find_main_content(anon_browser.contents)
-    >>> print content.find('p').renderContents()
+    >>> print(content.find('p').renderContents())
     No Privileges Person is not an answer contact for any project.
 
 But when the person is an answer contact, the page displays the project
@@ -22,25 +22,25 @@
 
     >>> anon_browser.open('http://answers.launchpad.dev/~name16')
     >>> anon_browser.getLink('Answer contact for').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Projects for which...
 
     >>> content = find_tag_by_id(
     ...     anon_browser.contents, "direct-answer-contacts-for-list")
-    >>> print extract_text(content).encode('ascii', 'backslashreplace')
+    >>> print(extract_text(content).encode('ascii', 'backslashreplace'))
     gnomebaker
     ...mozilla-firefox... package in Ubuntu
 
     >>> content = find_tag_by_id(
     ...     anon_browser.contents, "team-answer-contacts-for-list")
-    >>> print extract_text(content)
+    >>> print(extract_text(content))
     Gnome Applets
     gnomebaker
 
 Clicking on the name of the project will show the project answers.
 
     >>> anon_browser.getLink('gnomebaker').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Questions : gnomebaker
 
 When the user is logged in, and they are visiting this page in their
@@ -51,17 +51,17 @@
     >>> browser.open(
     ...     'http://answers.launchpad.dev/~name12')
     >>> browser.getLink('Answer contact for').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Projects for which...
 
     >>> content = find_tag_by_id(
     ... browser.contents, "team-answer-contacts-for-list")
-    >>> print extract_text(content)
+    >>> print(extract_text(content))
     Gnome Applets Unsubscribe team
     gnomebaker Unsubscribe team
 
     >>> browser.getLink(id="gnomebaker-setteamanswercontact").click()
-    >>> print browser.title
+    >>> print(browser.title)
     Answer contact for...
 
 The Remove yourself/team links only appears in their profile. They cannot
@@ -70,11 +70,11 @@
     >>> browser.open(
     ...     'http://answers.launchpad.dev/~name16')
     >>> browser.getLink('Answer contact for').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Projects for which...
 
     >>> content = find_tag_by_id(
     ...     browser.contents, "direct-answer-contacts-for-list")
-    >>> print extract_text(content).encode('ascii', 'backslashreplace')
+    >>> print(extract_text(content).encode('ascii', 'backslashreplace'))
     gnomebaker
     ...mozilla-firefox... package in Ubuntu

=== modified file 'lib/lp/answers/stories/distribution-package-answer-contact.txt'
--- lib/lp/answers/stories/distribution-package-answer-contact.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/answers/stories/distribution-package-answer-contact.txt	2018-06-02 19:58:15 +0000
@@ -45,7 +45,7 @@
     ...     'http://launchpad.dev/ubuntu/+source/evolution/+questions')
     >>> browser.getLink('Answers').click()
     >>> browser.getLink('Set answer contact').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Answer contact for evolution package...
 
 On this page, the user can choose to become an answer contact by
@@ -67,7 +67,7 @@
 
     >>> browser.getControl('Continue').click()
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     You have been added as an answer contact for evolution in Ubuntu.
     English was added to Landscape Developers's preferred languages.
     Landscape Developers has been added as an answer contact for
@@ -88,5 +88,5 @@
 
     >>> browser.getControl('Continue').click()
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     You have been removed as an answer contact for evolution in Ubuntu.

=== modified file 'lib/lp/answers/stories/faq-add.txt'
--- lib/lp/answers/stories/faq-add.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/answers/stories/faq-add.txt	2018-06-02 19:58:15 +0000
@@ -28,11 +28,11 @@
     >>> owner_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
     >>> owner_browser.open('http://answers.launchpad.dev/firefox')
     >>> owner_browser.getLink('All FAQs').click()
-    >>> print owner_browser.title
+    >>> print(owner_browser.title)
     FAQs for Mozilla Firefox : Questions : Mozilla Firefox
 
-    >>> print extract_text(
-    ...     find_tag_by_id(owner_browser.contents, 'faqs-listing'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(owner_browser.contents, 'faqs-listing')))
     What's the keyboard shortcut for [random feature]?
     How do I install plugins (Shockwave, QuickTime, etc.)?
     How do I troubleshoot problems with extensions/themes?
@@ -47,7 +47,7 @@
 
     >>> owner_browser.url
     'http://answers.launchpad.dev/firefox/+createfaq'
-    >>> print owner_browser.title
+    >>> print(owner_browser.title)
     Create a FAQ for...
 
     >>> owner_browser.getControl('Title').value
@@ -66,7 +66,7 @@
 
     >>> owner_browser.url
     'http://answers.launchpad.dev/firefox/+faq/...'
-    >>> print owner_browser.title
+    >>> print(owner_browser.title)
     FAQ #... : Questions : Mozilla Firefox
 
     >>> content = find_main_content(owner_browser.contents)
@@ -79,14 +79,13 @@
 FAQ is listed first.
 
     >>> owner_browser.getLink('List all FAQs').click()
-    >>> print owner_browser.title
+    >>> print(owner_browser.title)
     FAQs for Mozilla Firefox : Questions : Mozilla Firefox
 
-    >>> print extract_text(
-    ...     find_tag_by_id(owner_browser.contents, 'faqs-listing'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(owner_browser.contents, 'faqs-listing')))
     How do I read RSS?
     What's the keyboard shortcut for [random feature]?
     How do I install plugins (Shockwave, QuickTime, etc.)?
     How do I troubleshoot problems with extensions/themes?
     How do I install Extensions?
-

=== modified file 'lib/lp/answers/stories/faq-browse-and-search.txt'
--- lib/lp/answers/stories/faq-browse-and-search.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/answers/stories/faq-browse-and-search.txt	2018-06-02 19:58:15 +0000
@@ -27,12 +27,12 @@
 Unfortunately, it seems that nobody has problems or questions about
 Kubuntu:
 
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/kubuntu/+faqs
-    >>> print browser.title
+    >>> print(browser.title)
     FAQs for Kubuntu : Questions : Kubuntu
 
-    >>> print extract_text(find_main_content(browser.contents).find('p'))
+    >>> print(extract_text(find_main_content(browser.contents).find('p')))
     There are no FAQs for Kubuntu.
 
 
@@ -43,12 +43,12 @@
 
     >>> browser.open('http://answers.launchpad.dev/ubuntu')
     >>> browser.getLink('All FAQs').click()
-    >>> print browser.title
+    >>> print(browser.title)
     FAQs for Ubuntu : Questions : Ubuntu
 
 She sees a listing of the current FAQs about Ubuntu:
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'faqs-listing'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'faqs-listing')))
     How can I play MP3/Divx/DVDs/Quicktime/Realmedia files or view
         Flash/Java web pages
     How can I customize my desktop?
@@ -59,9 +59,9 @@
 There is a 'Next' link to see the second batch of results:
 
     >>> browser.getLink('Next').click()
-    >>> print browser.title
+    >>> print(browser.title)
     FAQs for Ubuntu : Questions : Ubuntu
-    >>> print extract_text(find_tag_by_id(browser.contents, 'faqs-listing'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'faqs-listing')))
     Wireless Networking Documentation
 
 Going back to the first page, she realises that when she leaves the
@@ -72,7 +72,7 @@
     >>> browser.getLink('First').click()
     >>> faq_link = find_main_content(browser.contents).find(
     ...     'a', text=re.compile('How can I play MP3/Divx/DVDs'))
-    >>> print faq_link.findParent('li')['title']
+    >>> print(faq_link.findParent('li')['title'])
     Playing many common formats such as DVIX, MP3, DVD, or Flash ...
 
 She clicks on FAQ's title to display the complete FAQ content:
@@ -80,17 +80,17 @@
     >>> from lp.services.helpers import backslashreplace
 
     >>> browser.getLink('How can I play MP3/Divx').click()
-    >>> print backslashreplace(browser.title)
+    >>> print(backslashreplace(browser.title))
     FAQ #6 : Questions : Ubuntu
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/ubuntu/+faq/6
 
 The FAQ page has a link back to the FAQ listing:
 
     >>> browser.getLink('List all FAQs').click()
-    >>> print browser.title
+    >>> print(browser.title)
     FAQs for Ubuntu : Questions : Ubuntu
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/ubuntu/+faqs
 
 
@@ -101,13 +101,13 @@
 
     >>> browser.getControl(name='field.search_text').value = 'crash on boot'
     >>> browser.getControl('Search', index=0).click()
-    >>> print backslashreplace(browser.title)
+    >>> print(backslashreplace(browser.title))
     FAQs matching \u201ccrash on boot\u201d for Ubuntu : Questions : Ubuntu
 
 When no matches are found, a simple message is displayed:
 
     >>> message = find_main_content(browser.contents).find('p')
-    >>> print extract_text(message).encode('ascii', 'backslashreplace')
+    >>> print(extract_text(message).encode('ascii', 'backslashreplace'))
     There are no FAQs for Ubuntu matching \u201ccrash on boot\u201d.
 
 Otherwise, the listing only contains the matching FAQs.
@@ -115,7 +115,7 @@
     >>> browser.getControl(name='field.search_text').value = 'wifi'
     >>> browser.getControl('Search', index=0).click()
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'faqs-listing'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'faqs-listing')))
     Wireless Networking Documentation
 
 When searching for FAQs, a link to the questions matching the same
@@ -126,36 +126,36 @@
     >>> browser.getControl('Search', index=0).click()
 
     >>> message = find_main_content(browser.contents).find('p')
-    >>> print extract_text(message)
+    >>> print(extract_text(message))
     You can also consult the list of 1 question(s)
         matching &ldquo;plugin&rdquo;.
 
 Following the link will show the questions results:
 
     >>> browser.getLink('1 question(s)').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Ubuntu
 
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Installation of Java Runtime Environment for Mozilla
 
 On the question page, there is also a link to consult the FAQs matching
 the same keywords.
 
     >>> message = find_tag_by_id(browser.contents, 'found-matching-faqs')
-    >>> print extract_text(message)
+    >>> print(extract_text(message))
     You can also consult the list of 1 FAQ(s)
         matching &ldquo;plugin&rdquo;.
 
 Following the link will show the questions results:
 
     >>> browser.getLink('1 FAQ(s)').click()
-    >>> print backslashreplace(browser.title)
+    >>> print(backslashreplace(browser.title))
     FAQs matching \u201cplugin\u201d for Ubuntu : Questions : Ubuntu
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'faqs-listing'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'faqs-listing')))
     How can I play MP3/Divx/DVDs/Quicktime/Realmedia files or view
         Flash/Java web pages
 
@@ -169,9 +169,9 @@
     >>> browser.open('http://answers.launchpad.dev/ubuntu/'
     ...              '+source/mozilla-firefox')
     >>> browser.getLink('All FAQs').click()
-    >>> print browser.title
+    >>> print(browser.title)
     FAQs for Ubuntu : Questions : Ubuntu
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/ubuntu/+faqs
 
 
@@ -188,4 +188,3 @@
     Traceback (most recent call last):
       ...
     NotFound: ...
-

=== modified file 'lib/lp/answers/stories/faq-edit.txt'
--- lib/lp/answers/stories/faq-edit.txt	2016-08-23 08:05:44 +0000
+++ lib/lp/answers/stories/faq-edit.txt	2018-06-02 19:58:15 +0000
@@ -9,7 +9,7 @@
 
     >>> from lp.services.helpers import backslashreplace
     >>> anon_browser.open('http://answers.launchpad.dev/firefox/+faq/7')
-    >>> print backslashreplace(anon_browser.title)
+    >>> print(backslashreplace(anon_browser.title))
     FAQ #7 : Questions : Mozilla Firefox
 
     >>> anon_browser.getLink('Edit FAQ')
@@ -42,19 +42,19 @@
     >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
     >>> browser.open('http://answers.launchpad.dev/firefox/+faq/7')
     >>> browser.getLink('Edit FAQ').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/firefox/+faq/7/+edit
-    >>> print browser.title
+    >>> print(browser.title)
     Edit FAQ...
 
 The user can change the title, keywords and content fields. They then
 click 'Save' to save their changes.
 
-    >>> print browser.getControl('Title').value
+    >>> print(browser.getControl('Title').value)
     How do I install Java?
     >>> browser.getControl('Keywords').value
     ''
-    >>> print browser.getControl('Content').value
+    >>> print(browser.getControl('Content').value)
     Windows
     On Windows, ...
 
@@ -67,13 +67,13 @@
 
 The user can see their changes on the page:
 
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/firefox/+faq/7
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'faq-keywords'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'faq-keywords')))
     Keywords: windows ubuntu plugins extensions
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'faq-content'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'faq-content')))
     Windows
     On Windows,...
     Ubuntu: See https://help.ubuntu.com/community/Java
@@ -81,5 +81,5 @@
 The 'Last updated by' field in the 'Lifecycle' portlet is updated
 with the name of the user who made the last modification:
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'faq-updated'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'faq-updated')))
     Last updated by: Sample Person on ...

=== modified file 'lib/lp/answers/stories/project-add-question.txt'
--- lib/lp/answers/stories/project-add-question.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/answers/stories/project-add-question.txt	2018-06-02 19:58:15 +0000
@@ -27,7 +27,7 @@
 
     >>> user_browser.open('http://answers.launchpad.dev/mozilla')
     >>> user_browser.getLink('Ask a question').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Ask a question...
 
 The workflow is identical to the regular one, except that the user must
@@ -36,23 +36,23 @@
 use the word 'Project' for 'Products' (and 'Distributions') so that
 users do not have to learn our business' semantics.
 
-    >>> print user_browser.getControl('Project').displayOptions
+    >>> print(user_browser.getControl('Project').displayOptions)
     ['Mozilla Firefox', 'Mozilla Thunderbird']
 
 The first item in the list is the default value, and it will be
 submitted if the user does not change it.
 
-    >>> print user_browser.getControl('Project').displayValue
+    >>> print(user_browser.getControl('Project').displayValue)
     ['Mozilla Firefox']
 
 Like for the regular workflow, the user is shown a list of languages,
 with the supported languages flagged with an asterisk.
 
-    >>> print user_browser.getControl('Language').displayValue
+    >>> print(user_browser.getControl('Language').displayValue)
     ['English (en) *']
 
     >>> langs = sorted(user_browser.getControl('Language').displayOptions)
-    >>> for lang in langs: print lang
+    >>> for lang in langs: print(lang)
     Afrikaans (af)
     English (en) *
     Sotho, Southern (st)
@@ -73,7 +73,7 @@
     >>> similar_questions = find_tag_by_id(
     ...     user_browser.contents, 'similar-questions')
     >>> for row in similar_questions.findAll('li'):
-    ...     print row.a.renderContents()
+    ...     print(row.a.renderContents())
     2: Problem showing the SVG demo on W3C site
 
 No Privileged Person can still change the product for which they're asking
@@ -93,14 +93,14 @@
 missing:
 
     >>> soup = find_main_content(user_browser.contents)
-    >>> print soup.find('div', 'message').renderContents()
+    >>> print(soup.find('div', 'message').renderContents())
     You must enter a summary of your problem.
 
 The product Thunderbird that they selected on the previous screen is still
 selected. No Privileged Person re-enters their question summary, and
 submits the form.
 
-    >>> print user_browser.getControl('Project').displayValue
+    >>> print(user_browser.getControl('Project').displayValue)
     ['Mozilla Thunderbird']
 
     >>> user_browser.getControl('Summary').value = (
@@ -112,7 +112,7 @@
 an appropriate message is displayed to inform them of this:
 
     >>> soup = find_main_content(user_browser.contents)
-    >>> print soup.find('p').renderContents()
+    >>> print(soup.find('p').renderContents())
     There are no existing FAQs or questions similar to the summary you
     entered.
 
@@ -132,7 +132,7 @@
     >>> user_browser.url
     '.../thunderbird/+question/...'
 
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Question #... : Questions : Mozilla Thunderbird
 
 
@@ -156,7 +156,7 @@
 
     >>> daf_browser = setupBrowser(auth='Basic daf@xxxxxxxxxxxxx:test')
     >>> daf_browser.open('http://launchpad.dev/~daf/+editlanguages')
-    >>> print daf_browser.title
+    >>> print(daf_browser.title)
     Language preferences...
 
     >>> daf_browser.getControl('Japanese').selected
@@ -164,7 +164,7 @@
 
     >>> daf_browser.open(
     ...     'http://answers.launchpad.dev/thunderbird/+answer-contact')
-    >>> print daf_browser.title
+    >>> print(daf_browser.title)
     Answer contact for...
 
     >>> daf_browser.getControl('I want to be an answer contact for '
@@ -172,7 +172,7 @@
     >>> daf_browser.getControl('Continue').click()
     >>> content = find_main_content(daf_browser.contents)
     >>> for message in content.findAll('div', 'informational message'):
-    ...      print message.renderContents()
+    ...      print(message.renderContents())
     You have been added as an answer contact for Mozilla Thunderbird.
 
 And we add Japanese to No Privileges Person's preferred languages. We
@@ -182,13 +182,13 @@
 
     >>> user_browser.open(
     ...     'http://launchpad.dev/~no-priv/+editlanguages')
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Language preferences...
 
     >>> user_browser.getControl('Japanese').selected = True
     >>> user_browser.getControl('Save').click()
     >>> soup = find_main_content(user_browser.contents)
-    >>> print soup.find('div', 'informational message').renderContents()
+    >>> print(soup.find('div', 'informational message').renderContents())
     Added Japanese to your preferred languages.
 
 So if No Privileges Person were to visit the Ask a Question page for
@@ -199,7 +199,7 @@
 
     >>> user_browser.open(
     ...     'http://answers.launchpad.dev/firefox/+addquestion')
-    >>> print user_browser.getControl('Language').displayOptions
+    >>> print(user_browser.getControl('Language').displayOptions)
     ['English (en) *', 'Japanese (ja)']
 
 The supported languages will not be shown immediately when Sample Person
@@ -227,17 +227,17 @@
 
     >>> user_browser.open('http://answers.launchpad.dev/mozilla')
     >>> user_browser.getLink('Ask a question').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Ask a question...
 
 The page displays a list of products associated with the project. The
 first item in the list is the default value, and it will be submitted if
 the user does not change it.
 
-    >>> print user_browser.getControl('Project').displayOptions
+    >>> print(user_browser.getControl('Project').displayOptions)
     ['Mozilla Firefox', 'Mozilla Thunderbird']
 
-    >>> print user_browser.getControl('Project').displayValue
+    >>> print(user_browser.getControl('Project').displayValue)
     ['Mozilla Firefox']
 
 Like for the regular workflow, the user is shown a list of languages,
@@ -248,7 +248,7 @@
 to submit their question in another language, they might find that the
 language is supported on the next page.
 
-    >>> print user_browser.getControl('Language').displayOptions
+    >>> print(user_browser.getControl('Language').displayOptions)
     ['English (en) *', 'Japanese (ja)']
 
     >>> user_browser.getControl('Language').value = ['en']
@@ -267,7 +267,7 @@
 Firefox product by reviewing which languages has asterisks in the
 Languages list--only English in the example.
 
-    >>> print user_browser.getControl('Language').displayOptions
+    >>> print(user_browser.getControl('Language').displayOptions)
     ['English (en) *', 'Japanese (ja)']
 
 No Privileges Person can still change the product for which they're asking
@@ -278,7 +278,7 @@
 as the product.
 
     >>> user_browser.getControl('Mozilla Thunderbird').selected = True
-    >>> print user_browser.getControl('Language').displayOptions
+    >>> print(user_browser.getControl('Language').displayOptions)
     ['English (en) *', 'Japanese (ja)']
 
 If No Privileges Person asks a question in Japanese, it will be
@@ -297,7 +297,7 @@
 
     >>> user_browser.open('http://answers.launchpad.dev/mozilla')
     >>> user_browser.getLink('Ask a question').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Ask a question...
 
     >>> user_browser.getControl('Mozilla Thunderbird').selected = True
@@ -305,7 +305,7 @@
 They write their summary in English as he sees that is the only supported
 Language, and 'Continues' to the next page.
 
-    >>> print user_browser.getControl('Language').displayOptions
+    >>> print(user_browser.getControl('Language').displayOptions)
     ['English (en) *', 'Japanese (ja)']
 
     >>> user_browser.getControl('Summary').value = (
@@ -319,14 +319,14 @@
 answer contact for Thunderbird. We see this only after a question
 summary is submitted for a product.
 
-    >>> print user_browser.getControl('Language').displayOptions
+    >>> print(user_browser.getControl('Language').displayOptions)
     ['English (en) *', 'Japanese (ja) *']
 
 No Privileges Person sets the language to Japanese, changes their question
 summary, writes a description, and submits the form with the 'Post
 Question' button.
 
-    >>> print user_browser.getControl('Project').displayValue
+    >>> print(user_browser.getControl('Project').displayValue)
     ['Mozilla Thunderbird']
 
     >>> user_browser.getControl('Japanese (ja) *').selected = True
@@ -343,5 +343,5 @@
     >>> user_browser.url
     '.../thunderbird/+question/...'
 
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Question #... : Questions : Mozilla Thunderbird

=== modified file 'lib/lp/answers/stories/question-add-in-other-languages.txt'
--- lib/lp/answers/stories/question-add-in-other-languages.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/answers/stories/question-add-in-other-languages.txt	2018-06-02 19:58:15 +0000
@@ -13,7 +13,7 @@
 link to enable to change their preferred languages:
 
     >>> user_browser.getLink('Change your preferred languages').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Language preferences...
     >>> user_browser.url
     '.../~no-priv/+editlanguages'
@@ -55,7 +55,7 @@
     #'Problema al recompilar kernel con soporte smp (doble-n\xc3\xbacleo)'
 
     >>> for tag in find_tags_by_class(browser.contents, 'warning message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     <strong>Portuguese (Brazil) (pt_BR)</strong> doesn't seem to be
     a language spoken by any answer contacts in this community. If you
     go ahead and ask a question in that language, no answer
@@ -83,7 +83,7 @@
     >>> browser.getControl('Post Question').click()
     >>> browser.url
     '.../ubuntu/+question/...'
-    >>> print browser.title
+    >>> print(browser.title)
     Question #... : Questions : Ubuntu
 
 The page reports the question language both in the content and in the
@@ -93,11 +93,11 @@
 
     >>> from BeautifulSoup import BeautifulSoup
     >>> soup = BeautifulSoup(browser.contents)
-    >>> print soup.find('div', id='question')['lang']
+    >>> print(soup.find('div', id='question')['lang'])
     pt-BR
-    >>> print soup.html['dir']
+    >>> print(soup.html['dir'])
     ltr
-    >>> print extract_text(find_tag_by_id(soup, 'question-lang'))
+    >>> print(extract_text(find_tag_by_id(soup, 'question-lang')))
     Language: Portuguese (Brazil) ...
 
 It's also possible that the user chose English in the first page but
@@ -129,7 +129,7 @@
     'http://launchpad.dev/ubuntu/+addquestion'
 
     >>> for tag in find_tags_by_class(browser.contents, 'warning message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     <strong>Welsh (cy)</strong> doesn't seem to be
     a language spoken by any answer contacts in this community. If you
     go ahead and ask a question in that language, no answer
@@ -147,7 +147,7 @@
     >>> browser.getControl('Post Question').click()
 
     >>> for tag in find_tags_by_class(browser.contents, 'warning message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     <strong>Japanese (ja)</strong> doesn't seem to be
     a language spoken by any answer contacts in this community. If you
     go ahead and ask a question in that language, no answer
@@ -160,6 +160,6 @@
     >>> browser.getControl('Post Question').click()
     >>> browser.url
     '.../ubuntu/+question/...'
-    >>> print browser.title.decode('utf-8')
+    >>> print(browser.title.decode('utf-8'))
     Question #... : Questions : Ubuntu
     >>> portlet = find_tag_by_id(browser.contents, 'portlet-details')

=== modified file 'lib/lp/answers/stories/question-add.txt'
--- lib/lp/answers/stories/question-add.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/answers/stories/question-add.txt	2018-06-02 19:58:15 +0000
@@ -6,13 +6,13 @@
 which support is desired:
 
     >>> browser.open('http://answers.launchpad.dev/ubuntu')
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Ubuntu
 
 The user sees an involvement link to ask a question.
 
     >>> link = find_tag_by_id(browser.contents, 'involvement').a
-    >>> print extract_text(link)
+    >>> print(extract_text(link))
     Ask a question
 
 Asking a new question requires logging in:
@@ -23,7 +23,7 @@
     Unauthorized...
     >>> user_browser.open('http://answers.launchpad.dev/ubuntu/')
     >>> user_browser.getLink('Ask a question').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Ask a question...
 
 
@@ -39,11 +39,11 @@
     >>> user_browser.open(
     ...     'http://launchpad.dev/ubuntu/hoary/'
     ...     '+sources/mozilla-firefox/+gethelp')
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Help and support...
 
     >>> user_browser.getLink('Ask a question').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Ask a question...
 
 
@@ -82,15 +82,15 @@
     >>> user_browser.getControl('Continue').click()
     >>> contents = find_main_content(user_browser.contents)
     >>> similar_faqs = contents.find(id='similar-faqs')
-    >>> print extract_text(similar_faqs)
+    >>> print(extract_text(similar_faqs))
     How can I play MP3/Divx/DVDs/Quicktime/Realmedia files or view
         Flash/Java web pages
     >>> similar_faqs.a['href']
     u'http://answers.launchpad.dev/ubuntu/+faq/...'
 
     >>> similar_questions = contents.find(id='similar-questions')
-    >>> print extract_text(similar_questions).encode(
-    ...     'ascii', 'backslashreplace')
+    >>> print(extract_text(similar_questions).encode(
+    ...     'ascii', 'backslashreplace'))
     8: Installation of Java Runtime Environment for Mozilla  (Answered)
         posted on ... in ...mozilla-firefox... package in Ubuntu
     >>> similar_questions.a['href']
@@ -102,7 +102,7 @@
     >>> import re
     >>> question_link = contents.find(
     ...     'a', text=re.compile('Installation of Java'))
-    >>> print question_link.findParent('li')['title']
+    >>> print(question_link.findParent('li')['title'])
     <BLANKLINE>
     When opening http://www.gotomypc.com/ with Mozilla, a java run time
     ennvironment plugin is requested.
@@ -117,7 +117,7 @@
 
     >>> faq_link = contents.find(
     ...     'a', text=re.compile('How can I play MP3/Divx'))
-    >>> print faq_link.findParent('li')['title']
+    >>> print(faq_link.findParent('li')['title'])
     Playing many common formats such as DVIX, MP3, DVD, or Flash
     animations require the installation of plugins.
     <BLANKLINE>
@@ -162,12 +162,12 @@
     >>> user_browser.getControl('Post Question').click()
     >>> user_browser.url
     '.../ubuntu/+source/mozilla-firefox/+question/...'
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Question #... : Questions : mozilla-firefox package : Ubuntu
 
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'registration'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'registration')))
     Asked by No Privileges Person ...
     >>> contents = find_main_content(user_browser.contents)
-    >>> print extract_text(contents.find('div', 'report'))
+    >>> print(extract_text(contents.find('div', 'report')))
     I use Ubuntu on AMD64 ...

=== modified file 'lib/lp/answers/stories/question-answer-contact.txt'
--- lib/lp/answers/stories/question-answer-contact.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/answers/stories/question-answer-contact.txt	2018-06-02 19:58:15 +0000
@@ -12,19 +12,20 @@
     >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
     >>> browser.addHeader('Accept-Language', 'en, es')
     >>> browser.open('http://launchpad.dev/ubuntu/+questions')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'portlet-answer-contacts-ubuntu'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(
+    ...         browser.contents, 'portlet-answer-contacts-ubuntu')))
     Answer contacts for Ubuntu
 
 Anybody can become an answer contact. To register as an answer contact,
 the user clicks on the 'Set answer Contact' link:
 
     >>> browser.getLink('Set answer contact').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Answer contact for...
 
     >>> description = find_main_content(browser.contents).p
-    >>> print extract_text(description)
+    >>> print(extract_text(description))
     An answer contact...will receive changes related to all questions
     (written in one of your preferred languages)...
 
@@ -54,7 +55,7 @@
 languages header with English and Spanish.
 
     >>> browser.getLink('Your preferred languages').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Language preferences...
 
     >>> browser.getControl('English', index=0).selected
@@ -82,7 +83,7 @@
 A confirmation message is displayed:
 
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Landscape Developers has been removed as an answer contact for Ubuntu.
 
 
@@ -93,10 +94,10 @@
 
     >>> browser.open('http://answers.launchpad.dev/firefox')
     >>> browser.getLink('Set answer contact').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Answer contact for...
 
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/firefox/+answer-contact
 
 
@@ -115,14 +116,14 @@
 traffic to a manageable volume.
 
     >>> browser.open('http://answers.launchpad.dev/ubuntu/')
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Ubuntu
 
     >>> browser.getLink('Set answer contact').click()
     >>> browser.getControl("Landscape Developers").selected = True
     >>> browser.getControl('Continue').click()
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     Landscape Developers has been added as an answer contact for Ubuntu.
 
 Sample Person navigates to the team page to set it's preferred
@@ -133,7 +134,7 @@
     'Landscape Developers in Launchpad'
 
     >>> browser.getLink('Set preferred languages').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Language preferences...
 
 Sample Person may be surprised to see English is already selected. Per

=== modified file 'lib/lp/answers/stories/question-answers-vhost.txt'
--- lib/lp/answers/stories/question-answers-vhost.txt	2014-11-27 22:13:36 +0000
+++ lib/lp/answers/stories/question-answers-vhost.txt	2018-06-02 19:58:15 +0000
@@ -10,7 +10,7 @@
 -------
 
     >>> anon_browser.open('http://answers.launchpad.dev/firefox')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Questions : Mozilla Firefox
 
 
@@ -18,7 +18,7 @@
 ------------
 
     >>> anon_browser.open('http://answers.launchpad.dev/ubuntu')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Questions : Ubuntu
 
 
@@ -27,7 +27,7 @@
 
     >>> anon_browser.open(
     ...     'http://answers.launchpad.dev/ubuntu/+source/mozilla-firefox')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Questions : mozilla-firefox package : Ubuntu
 
 
@@ -36,7 +36,7 @@
 
     >>> anon_browser.open(
     ...     'http://answers.launchpad.dev/mozilla')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Questions : The Mozilla Project
 
 
@@ -44,5 +44,5 @@
 ------
 
     >>> anon_browser.open('http://answers.launchpad.dev/~name16')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Questions : Foo Bar

=== modified file 'lib/lp/answers/stories/question-browse-and-search.txt'
--- lib/lp/answers/stories/question-browse-and-search.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/answers/stories/question-browse-and-search.txt	2018-06-02 19:58:15 +0000
@@ -26,10 +26,10 @@
 
 He discovers that there are no questions on the Kubuntu Answers page:
 
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Kubuntu
 
-    >>> print find_main_content(browser.contents).find('p').renderContents()
+    >>> print(find_main_content(browser.contents).find('p').renderContents())
     There are no questions for Kubuntu with the requested statuses.
 
 For projects that don't have products, the Answers facet is disabled.
@@ -46,14 +46,14 @@
 Answers page and goes there to check.
 
     >>> browser.open('http://launchpad.dev/ubuntu/+questions')
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Ubuntu
 
 He sees a listing of the current questions posted on Ubuntu:
 
     >>> soup = find_main_content(browser.contents)
     >>> for question in soup.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Continue playing after shutdown
     Play DVDs in Totem
     mailto: problem in webpage
@@ -65,11 +65,11 @@
 results. There, he finds only one other question:
 
     >>> browser.getLink('Next').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Ubuntu
     >>> soup = find_main_content(browser.contents)
     >>> for question in soup.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Installation failed
 
 This is the last results page, so the next and last links are greyed
@@ -115,18 +115,18 @@
     >>> import re
     >>> soup = find_main_content(browser.contents)
     >>> question_link = soup.find('a', text=re.compile('Play DVDs'))
-    >>> print question_link.findParent('tr')['title']
+    >>> print(question_link.findParent('tr')['title'])
     How do you play DVDs in Totem..........?
 
     >>> question_link = soup.find('a', text=re.compile('Slow system'))
-    >>> print question_link.findParent('tr')['title']
+    >>> print(question_link.findParent('tr')['title'])
     I get really poor hard drive performance.
 
 He clicks on the question title to obtain the question page where the
 details of the question are available.
 
     >>> browser.getLink('Slow system').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Question #7 : ...
     >>> soup = find_main_content(browser.contents)
     >>> soup('div', 'report')
@@ -148,7 +148,7 @@
     >>> browser.getControl(name='field.search_text').value = '9'
     >>> browser.getControl('Find Answers').click()
     >>> from lp.services.helpers import backslashreplace
-    >>> print backslashreplace(browser.title)
+    >>> print(backslashreplace(browser.title))
     Question #9 : ...
 
 While reading the Ubuntu forums for a solution to his problem,
@@ -160,7 +160,7 @@
     >>> browser.open('http://answers.launchpad.dev/')
     >>> browser.getControl(name='field.search_text').value = ' #6 '
     >>> browser.getControl('Find Answers').click()
-    >>> print backslashreplace(browser.title)
+    >>> print(backslashreplace(browser.title))
     Question #6 : ...
 
 The Answer Tracker cannot identify Question ids within text. Average
@@ -171,10 +171,10 @@
     >>> browser.open('http://answers.launchpad.dev/')
     >>> browser.getControl(name='field.search_text').value = 'question 8'
     >>> browser.getControl('Find Answers').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions matching "question 8"
 
-    >>> print find_main_content(browser.contents).find('p').renderContents()
+    >>> print(find_main_content(browser.contents).find('p').renderContents())
     There are no questions matching "question 8" with the requested statuses.
 
 
@@ -195,10 +195,10 @@
 
 Unfortunately, the search doesn't return any similar questions:
 
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Ubuntu
     >>> search_summary = find_main_content(browser.contents)
-    >>> print search_summary
+    >>> print(search_summary)
     <...
     <p>There are no questions matching "firefox is slow" for Ubuntu with
     the requested statuses.</p>
@@ -220,13 +220,13 @@
 
     >>> soup = find_main_content(browser.contents)
     >>> for question in soup.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Firefox is slow and consumes too much RAM
 
 He clicks on the link to read the question description.
 
     >>> browser.getLink('Firefox is slow').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Question #3 : ...
 
 The user must choose at least one status when searching questions. An
@@ -236,7 +236,7 @@
     >>> browser.getControl(name='field.status').displayValue = []
     >>> browser.getControl('Search', index=0).click()
     >>> messages = find_tags_by_class(browser.contents, 'message')
-    >>> print messages[0].renderContents()
+    >>> print(messages[0].renderContents())
     You must choose at least one status.
 
 
@@ -291,11 +291,11 @@
 
     >>> browser.open('http://launchpad.dev/firefox/+questions')
     >>> browser.getLink('Open').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Mozilla Firefox
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Firefox loses focus and gets stuck
     Problem showing the SVG demo on W3C site
     Firefox cannot render Bank Site
@@ -315,7 +315,7 @@
     >>> browser.getControl('Search', index=0).click()
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Problem showing the SVG demo on W3C site
 
 
@@ -328,14 +328,14 @@
 
     >>> browser.open('http://launchpad.dev/ubuntu/+questions')
     >>> browser.getLink('Answered').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Ubuntu
     >>> statuses = browser.getControl(name='field.status').displayValue
     >>> [strip_label(status) for status in statuses]
     ['Answered', 'Solved']
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Play DVDs in Totem
     mailto: problem in webpage
     Installation of Java Runtime Environment for Mozilla
@@ -347,7 +347,7 @@
     >>> browser.getControl('Search', index=0).click()
 
     >>> search_summary = find_main_content(browser.contents)
-    >>> print search_summary
+    >>> print(search_summary)
     <...
     <p>There are no answered questions matching "Evolution" for Ubuntu.</p>
     ...
@@ -375,12 +375,12 @@
     ...     'http://launchpad.dev/ubuntu/+source/mozilla-firefox/'
     ...     '+questions')
     >>> sample_person_browser.getLink('My questions').click()
-    >>> print repr(sample_person_browser.title)
+    >>> print(repr(sample_person_browser.title))
     'Questions you asked about mozilla-firefox in Ubuntu : Questions : mozilla-firefox package : Ubuntu'
     >>> questions = find_tag_by_id(
     ...     sample_person_browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     mailto: problem in webpage
     Installation of Java Runtime Environment for Mozilla
 
@@ -402,7 +402,7 @@
     >>> questions = find_tag_by_id(
     ...     sample_person_browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     mailto: problem in webpage
 
 If the user didn't make any questions on the product, a message
@@ -419,8 +419,8 @@
     >>> sample_person_browser.open(
     ...     'http://launchpad.dev/gnomebaker/+questions')
     >>> sample_person_browser.getLink('My questions').click()
-    >>> print find_main_content(
-    ...     sample_person_browser.contents).find('p').renderContents()
+    >>> print(find_main_content(
+    ...     sample_person_browser.contents).find('p').renderContents())
     You didn't ask any questions about gnomebaker.
 
 
@@ -443,12 +443,12 @@
     >>> sample_person_browser.open(
     ...     'http://launchpad.dev/distros/ubuntu/+questions')
     >>> sample_person_browser.getLink('Need attention').click()
-    >>> print sample_person_browser.title
+    >>> print(sample_person_browser.title)
     Questions needing your attention for Ubuntu : Questions : Ubuntu
     >>> questions = find_tag_by_id(
     ...     sample_person_browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Play DVDs in Totem
     Installation of Java Runtime Environment for Mozilla
 
@@ -457,10 +457,10 @@
     >>> sample_person_browser.getControl(
     ...     name='field.search_text').value = 'evolution'
     >>> sample_person_browser.getControl('Search', index=0).click()
-    >>> print sample_person_browser.title
+    >>> print(sample_person_browser.title)
     Questions matching "evolution" needing your attention for Ubuntu : Questions : Ubuntu
     >>> search_summary = find_main_content(sample_person_browser.contents)
-    >>> print search_summary
+    >>> print(search_summary)
     <...
     <p>No questions matching "evolution" need your attention for Ubuntu.</p>
     ...
@@ -471,8 +471,8 @@
     >>> sample_person_browser.open(
     ...    'http://launchpad.dev/products/gnomebaker/+questions')
     >>> sample_person_browser.getLink('Need attention').click()
-    >>> print find_main_content(
-    ...     sample_person_browser.contents).find('p').renderContents()
+    >>> print(find_main_content(
+    ...     sample_person_browser.contents).find('p').renderContents())
     No questions need your attention for gnomebaker.
 
 
@@ -488,12 +488,12 @@
 
     >>> browser.open('http://launchpad.dev/~name16')
     >>> browser.getLink('Answers').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Foo Bar
 
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Continue playing after shutdown
     Play DVDs in Totem
     mailto: problem in webpage
@@ -509,7 +509,7 @@
 questions was made.
 
     >>> for question in questions.findAll('td', 'question-target'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Ubuntu
     Ubuntu
     mozilla-firefox in Ubuntu
@@ -519,7 +519,7 @@
 These contexts are links to the context question listing.
 
     >>> browser.getLink('mozilla-firefox in Ubuntu').click()
-    >>> print repr(browser.title)
+    >>> print(repr(browser.title))
     'Questions : mozilla-firefox package : Ubuntu'
 
 The listing is searchable and can restrict also the list of displayed
@@ -529,11 +529,11 @@
     >>> browser.open('http://launchpad.dev/~name16/+questions')
     >>> browser.getControl(name='field.search_text').value = 'Firefox'
     >>> browser.getControl(name='field.status').displayValue = [
-    ...     'Solved', 'Invalid']
+    ...     b'Solved', b'Invalid']
     >>> browser.getControl('Search', index=0).click()
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Firefox is slow and consumes too much RAM
     mailto: problem in webpage
 
@@ -547,9 +547,9 @@
 assigned.
 
     >>> browser.getLink('Assigned').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions for Foo Bar : Questions : Foo Bar
-    >>> print find_main_content(browser.contents).find('p').renderContents()
+    >>> print(find_main_content(browser.contents).find('p').renderContents())
     No questions assigned to Foo Bar found with the requested statuses.
 
 
@@ -559,11 +559,11 @@
 answerer.
 
     >>> browser.getLink('Answered').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions for Foo Bar : Questions : Foo Bar
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     mailto: problem in webpage
 
 
@@ -573,11 +573,11 @@
 questions commented on by the person.
 
     >>> browser.getLink('Commented').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions for Foo Bar : Questions : Foo Bar
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Continue playing after shutdown
     Play DVDs in Totem
     mailto: problem in webpage
@@ -591,11 +591,11 @@
 asked by the person.
 
     >>> browser.getLink('Asked').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions for Foo Bar : Questions : Foo Bar
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Slow system
     Firefox loses focus and gets stuck
 
@@ -606,11 +606,11 @@
 the attention of that person.
 
     >>> browser.getLink('Need attention').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions for Foo Bar : Questions : Foo Bar
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Continue playing after shutdown
     Slow system
 
@@ -621,11 +621,11 @@
 visiting the 'Subscribed' link in the 'Answers' facet.
 
     >>> browser.getLink('Subscribed').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions for Foo Bar : Questions : Foo Bar
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Slow system
 
 
@@ -636,7 +636,7 @@
 
     >>> browser.open('http://launchpad.dev/mozilla')
     >>> browser.getLink('Answers').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : The Mozilla Project
 
 The results are displayed in a format similar to the Person reports:
@@ -649,7 +649,7 @@
     ...             'td', 'questionTITLE').find('a').renderContents()
     ...         question_target = question.find(
     ...             'td', 'question-target').find('a').renderContents()
-    ...         print question_title, question_target
+    ...         print(question_title, question_target)
     >>> print_questions_with_target(browser.contents)
     Newly installed plug-in doesn't seem to be used Mozilla Firefox
     Firefox loses focus and gets stuck  Mozilla Firefox
@@ -663,27 +663,27 @@
 
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Problem showing the SVG demo on W3C site
 
 The same standard reports than on regular QuestionTarget are available:
 
     >>> browser.getLink('Open').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : The Mozilla Project
 
     >>> browser.getLink('Answered').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : The Mozilla Project
 
     # The next two reports are only available to a logged-in user.
     >>> user_browser.open('http://launchpad.dev/mozilla/+questions')
     >>> user_browser.getLink('My questions').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Questions you asked about The Mozilla Project : Questions : The Mozilla Project
 
     >>> user_browser.getLink('Need attention').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Questions needing your attention for The Mozilla Project : Questions : The Mozilla Project
 
 
@@ -696,10 +696,10 @@
     >>> browser.getControl(name='field.search_text').value = 'firefox'
     >>> browser.getControl('Find Answers').click()
 
-    >>> print browser.title
+    >>> print(browser.title)
     Questions matching "firefox"
 
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/questions/+questions?...
 
 The results are displayed in a format similar to the Person reports:
@@ -720,17 +720,17 @@
 
 When no results are found, a message informs the user of this fact:
 
-    >>> browser.getControl(name='field.status').displayValue = ['Expired']
+    >>> browser.getControl(name='field.status').displayValue = [b'Expired']
     >>> browser.getControl('Search', index=0).click()
 
-    >>> print find_main_content(
-    ...     browser.contents).find('p').renderContents()
+    >>> print(find_main_content(
+    ...     browser.contents).find('p').renderContents())
     There are no questions matching "firefox" with the requested statuses.
 
 Clicking the 'Search' button without entering any search text will
 display all questions asked in Launchpad with the selected statuses.
 
-    >>> browser.getControl(name='field.status').displayValue = ['Open']
+    >>> browser.getControl(name='field.status').displayValue = [b'Open']
     >>> browser.getControl(name='field.search_text').value = ''
     >>> browser.getControl('Search', index=0).click()
 
@@ -755,7 +755,7 @@
     >>> anon_browser.getControl('Find Answers').click()
 
     >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     Please enter a project name
 
 Entering an invalid project also displays an error message:
@@ -764,7 +764,7 @@
     >>> anon_browser.getControl('Find Answers').click()
 
     >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     There is no project named &#x27;invalid&#x27; registered in Launchpad
 
 If the browser supports javascript, there is a 'Choose' link available
@@ -772,7 +772,7 @@
 not support javascript, it is turned into a "Find" link to the /questions.
 
     >>> find_link = anon_browser.getLink('Find')
-    >>> print find_link.url
+    >>> print(find_link.url)
     http://answers.launchpad.dev/questions...
 
 
@@ -783,7 +783,7 @@
     >>> anon_browser.getControl('One project').selected = True
     >>> anon_browser.getControl(name='field.scope.target').value = 'mozilla'
     >>> anon_browser.getControl('Find Answers').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Questions : The Mozilla Project
 
 This works also with distributions:
@@ -793,7 +793,7 @@
     >>> anon_browser.getControl('One project').selected = True
     >>> anon_browser.getControl(name='field.scope.target').value = 'ubuntu'
     >>> anon_browser.getControl('Find Answers').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Questions : Ubuntu
 
 And also with products:
@@ -803,6 +803,5 @@
     >>> anon_browser.getControl('One project').selected = True
     >>> anon_browser.getControl(name='field.scope.target').value = 'firefox'
     >>> anon_browser.getControl('Find Answers').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Questions : Mozilla Firefox
-

=== modified file 'lib/lp/answers/stories/question-compatibility-urls.txt'
--- lib/lp/answers/stories/question-compatibility-urls.txt	2010-09-25 21:52:30 +0000
+++ lib/lp/answers/stories/question-compatibility-urls.txt	2018-06-02 19:58:15 +0000
@@ -7,65 +7,65 @@
 == Answer Contact Page ==
 
     >>> user_browser.open('http://launchpad.dev/firefox/+support-contact')
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://answers.launchpad.dev/firefox/+answer-contact
 
 == Add Question Page ==
 
     >>> user_browser.open('http://launchpad.dev/firefox/+addticket')
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://answers.launchpad.dev/firefox/+addquestion
 
     >>> user_browser.open('http://launchpad.dev/mozilla/+addticket')
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://answers.launchpad.dev/mozilla/+addquestion
 
 == My Questions Page ==
 
     >>> user_browser.open('http://launchpad.dev/firefox/+mytickets')
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://answers.launchpad.dev/firefox/+myquestions
 
 == Questions Listing ==
 
     >>> browser.open('http://launchpad.dev/firefox/+tickets')
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/firefox/+questions
 
 == Question Page ==
 
     >>> browser.open('http://launchpad.dev/firefox/+ticket/1')
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/firefox/+question/1
 
 == Person Questions Listing ==
 
     >>> browser.open('http://launchpad.dev/~name12/+tickets')
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/~name12/+questions
 
     >>> browser.open('http://launchpad.dev/~name12/+answeredtickets')
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/~name12/+answeredquestions
 
     >>> browser.open('http://launchpad.dev/~name12/+assignedtickets')
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/~name12/+assignedquestions
 
     >>> browser.open('http://launchpad.dev/~name12/+commentedtickets')
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/~name12/+commentedquestions
 
     >>> browser.open('http://launchpad.dev/~name12/+createdtickets')
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/~name12/+createdquestions
 
     >>> browser.open('http://launchpad.dev/~name12/+needattentiontickets')
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/~name12/+needattentionquestions
 
     >>> browser.open('http://launchpad.dev/~name12/+subscribedtickets')
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/~name12/+subscribedquestions
 
 
@@ -75,7 +75,7 @@
 links to the +by-language pages that have unsolved questions.
 
     >>> browser.open('http://launchpad.dev/ubuntu/+unsupported')
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/ubuntu/+questions
 
 
@@ -96,7 +96,7 @@
     ...        '&field.search_text=&field.actions.search=Search'
     ...        '&field.status=%s&field.status-empty-marker=1')
     >>> browser.open(url % (old_sort, old_status))
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Ubuntu
     >>> browser.getControl(name='field.sort').displayValue
     ['by status']
@@ -106,7 +106,7 @@
     >>> new_sort = 'STATUS'
     >>> new_status = 'NEEDSINFO'
     >>> browser.open(url % (new_sort, new_status))
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Ubuntu
     >>> browser.getControl(name='field.sort').displayValue
     ['by status']

=== modified file 'lib/lp/answers/stories/question-edit.txt'
--- lib/lp/answers/stories/question-edit.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/answers/stories/question-edit.txt	2018-06-02 19:58:15 +0000
@@ -14,12 +14,12 @@
     >>> test_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
     >>> test_browser.open('http://launchpad.dev/firefox/+question/2')
     >>> test_browser.getLink('Edit question').click()
-    >>> print test_browser.url
+    >>> print(test_browser.url)
     http://answers.launchpad.dev/firefox/+question/2/+edit
 
 There is a cancel link should the user decide otherwise:
 
-    >>> print test_browser.getLink('Cancel').url
+    >>> print(test_browser.getLink('Cancel').url)
     http://answers.launchpad.dev/firefox/+question/2
 
 When we post the form, we should be redirected back to the question page.
@@ -32,25 +32,25 @@
     >>> test_browser.getControl('Summary').value = summary
     >>> test_browser.getControl('Save Changes').click()
 
-    >>> print test_browser.url
+    >>> print(test_browser.url)
     http://answers.launchpad.dev/firefox/+question/2
 
 And viewing that page should show the updated information.
 
     >>> soup = find_main_content(test_browser.contents)
-    >>> print soup.find('div', 'report').renderContents().strip()
+    >>> print(soup.find('div', 'report').renderContents().strip())
     <p>Hi! I&#x27;m trying to learn about SVG but I can&#x27;t get it to
     work at all in firefox. Maybe there is a plugin? Help! Thanks.
     Mark</p>
-    >>> print soup.find('h1').renderContents()
+    >>> print(soup.find('h1').renderContents())
     Problem showing the SVG demo on W3C web site
 
 You can even modify the title and description of 'Answered' and
 'Invalid' questions:
 
     >>> def print_question_status(browser):
-    ...     print extract_text(
-    ...         find_tag_by_id(browser.contents, 'question-status'))
+    ...     print(extract_text(
+    ...         find_tag_by_id(browser.contents, 'question-status')))
 
     >>> test_browser.open('http://launchpad.dev/ubuntu/+question/3')
     >>> print_question_status(test_browser)
@@ -97,7 +97,7 @@
     >>> browser.getControl('Save Changes').click()
 
     >>> soup = find_main_content(browser.contents)
-    >>> print extract_text(find_tag_by_id(soup, 'question-whiteboard'))
+    >>> print(extract_text(find_tag_by_id(soup, 'question-whiteboard')))
     Whiteboard: Some note
     >>> portlet_details = find_tag_by_id(browser.contents, 'portlet-details')
 
@@ -113,5 +113,3 @@
     Traceback (most recent call last):
     ...
     LookupError...
-
-

=== modified file 'lib/lp/answers/stories/question-message.txt'
--- lib/lp/answers/stories/question-message.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/answers/stories/question-message.txt	2018-06-02 19:58:15 +0000
@@ -11,7 +11,7 @@
 quoted passage, and a signature with an email address in it.
 
     >>> user_browser.open('http://answers.launchpad.dev/ubuntu/+question/11')
-    >>> print user_browser.title.decode('utf-8')
+    >>> print(user_browser.title.decode('utf-8'))
     Question #11 : ...
 
     >>> user_browser.getControl('Message').value = (
@@ -36,11 +36,11 @@
 Email addresses are visible to authenticated users. Sample Person is
 authenticated already, so they will see 'human@xxxxxxxxxxxxx'.
 
-    >>> print user_browser.title.decode('utf-8')
+    >>> print(user_browser.title.decode('utf-8'))
     Question #11 :  ...
     >>> text = find_tags_by_class(
     ...     user_browser.contents, 'boardCommentBody')[-1]
-    >>> print extract_text(text.findAll('p')[-1])
+    >>> print(extract_text(text.findAll('p')[-1]))
     --
     ______________________
     human@xxxxxxxxxxxxx
@@ -51,12 +51,12 @@
 see the obfuscated email address (<email address hidden>).
 
     >>> anon_browser.open('http://answers.launchpad.dev/ubuntu/+question/11')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Question #11 : ...
 
     >>> text = find_tags_by_class(
     ...     anon_browser.contents, 'boardCommentBody')[-1]
-    >>> print extract_text(text.findAll('p')[-1])
+    >>> print(extract_text(text.findAll('p')[-1]))
     --
     ______________________
     &lt;email address hidden&gt;
@@ -74,7 +74,7 @@
 The entire content of the paragraph is wrapped by a tag of 'foldable'
 class.
 
-    >>> print text.findAll('p')[-1]
+    >>> print(text.findAll('p')[-1])
     <p><span class="foldable">--...
     &lt;email address hidden&gt;<br />
     Witty signatures rock!
@@ -87,12 +87,10 @@
 tag of 'foldable' class, citation lines are always displayed. Again
 we can continue with the anonymous user to see the markup.
 
-    >>> print text.findAll('p')[-2]
+    >>> print(text.findAll('p')[-2])
     <p>Somebody said sometime ago:<br />
     <span class="foldable-quoted">
     &gt; 1. Remove the letters  c, j, q, x, w<br />
     &gt;    from the English Language.<br />
     &gt; 2. Remove the penny from US currency.
     </span></p>
-
-

=== modified file 'lib/lp/answers/stories/question-obfuscation.txt'
--- lib/lp/answers/stories/question-obfuscation.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/answers/stories/question-obfuscation.txt	2018-06-02 19:58:15 +0000
@@ -53,7 +53,7 @@
     >>> user_browser.getControl('4').selected = True
     >>> user_browser.getControl('Link to FAQ').click()
     >>> user_browser.getLink('How can I make VOIP calls?').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     FAQ #4 : Questions : Ubuntu
 
     >>> portlet = find_portlet(user_browser.contents, 'Related questions')
@@ -75,7 +75,7 @@
     >>> user_browser.getControl('Description').value = (
     ...     'The clicking mailto:user@xxxxxxxxxx crashes the browser.')
     >>> user_browser.getControl('Post Question').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Question #... : ...
 
     >>> user_browser.open('http://answers.launchpad.dev/')
@@ -93,7 +93,7 @@
 page.
 
     >>> anon_browser.open('http://answers.launchpad.dev/')
-    >>> 'user@xxxxxxxxxx' in anon_browser.contents
+    >>> b'user@xxxxxxxxxx' in anon_browser.contents
     False
 
     >>> question_portlet = find_tag_by_id(
@@ -129,7 +129,7 @@
       link ...'
 
     >>> anon_browser.getLink('mailto: problem in webpage').click()
-    >>> 'user@xxxxxxxxxx' in anon_browser.contents
+    >>> b'user@xxxxxxxxxx' in anon_browser.contents
     False
 
     >>> description = find_main_content(anon_browser.contents).p
@@ -141,7 +141,7 @@
 questions portlet on a FAQ page.
 
     >>> anon_browser.getLink('How can I make VOIP calls?').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     FAQ #4 : Questions : Ubuntu
 
     >>> portlet = find_portlet(anon_browser.contents, 'Related questions')

=== modified file 'lib/lp/answers/stories/question-overview.txt'
--- lib/lp/answers/stories/question-overview.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/answers/stories/question-overview.txt	2018-06-02 19:58:15 +0000
@@ -14,7 +14,7 @@
 
     >>> user_browser.open('http://launchpad.dev/firefox')
     >>> user_browser.getLink('Ask a question').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Ask a question about...
 
 There is also an 'Ask a question' link on the Bugs home page of the
@@ -22,7 +22,7 @@
 
     >>> user_browser.open('http://bugs.launchpad.dev/firefox')
     >>> user_browser.getLink('Ask a question').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Ask a question about...
 
 The list of all the currently active questions for the product is
@@ -32,7 +32,7 @@
     >>> browser.getLink('Answers').click()
 
     >>> soup = find_main_content(browser.contents)
-    >>> print soup.find('h1').renderContents()
+    >>> print(soup.find('h1').renderContents())
     Questions for Mozilla Firefox
 
     >>> browser.getLink('Firefox loses focus and gets stuck').url
@@ -58,10 +58,10 @@
 question page.
 
     >>> browser.getLink('Problem showing the SVG demo on W3C site').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Question #2 : ...
 
-    >>> print find_main_content(browser.contents).find('h1').renderContents()
+    >>> print(find_main_content(browser.contents).find('h1').renderContents())
     Problem showing the SVG demo on W3C site
 
 
@@ -72,14 +72,14 @@
 
     >>> user_browser.open('http://launchpad.dev/ubuntu')
     >>> user_browser.getLink('Ask a question').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Ask a question about...
 
 As well as on the Bugs facet home page:
 
     >>> user_browser.open('http://bugs.launchpad.dev/ubuntu')
     >>> user_browser.getLink('Ask a question').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Ask a question about...
 
 The distribution home page also has a link to the 'Answers' facet:
@@ -89,7 +89,7 @@
     'http://answers.launchpad.dev/ubuntu'
 
     >>> browser.getLink('Answers').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Ubuntu
 
     >>> browser.getLink('Ask a question')
@@ -118,7 +118,7 @@
     'http://answers.launchpad.dev/ubuntu/+source/evolution'
 
     >>> browser.getLink('Answers').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : evolution package : Ubuntu
 
     >>> browser.getLink('Ask a question').url
@@ -169,7 +169,7 @@
 
     >>> questions = find_tag_by_id(
     ...     user_browser.contents, 'portlet-latest-questions')
-    >>> print extract_text(questions).encode('ASCII', 'backslashreplace')
+    >>> print(extract_text(questions).encode('ASCII', 'backslashreplace'))
     All questions
     Latest questions
     Problemas de Impress\xe3o no Firefox ...
@@ -179,7 +179,7 @@
     Firefox cannot render Bank Site ...
 
     >>> user_browser.getLink('Ask a question').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Ask a question about...
 
 
@@ -194,10 +194,10 @@
     'http://answers.launchpad.dev/~name16'
 
     >>> browser.getLink('Answers').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Questions : Foo Bar
 
-    >>> print find_main_content(browser.contents).find('h1').renderContents()
+    >>> print(find_main_content(browser.contents).find('h1').renderContents())
     Questions for Foo Bar
 
     >>> browser.getLink('Slow system').url
@@ -219,10 +219,10 @@
 the proper context where the question can be found:
 
     >>> browser.open('http://answers.launchpad.dev/questions/1')
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/firefox/+question/1
 
-    >>> print find_main_content(browser.contents).find('h1').renderContents()
+    >>> print(find_main_content(browser.contents).find('h1').renderContents())
     Firefox cannot render Bank Site
 
 Asking for a non-existent question or an invalid ID will still raise a
@@ -243,14 +243,14 @@
 useful after a question was retargeted.)
 
     >>> browser.open('http://answers.launchpad.dev/ubuntu/+question/1')
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/firefox/+question/1
 
 It also works with pages below that URL:
 
     >>> browser.open(
     ...     'http://answers.launchpad.dev/ubuntu/+question/1/+history')
-    >>> print browser.url
+    >>> print(browser.url)
     http://answers.launchpad.dev/firefox/+question/1/+history
 
 But again, an invalid ID still raises a 404:

=== modified file 'lib/lp/answers/stories/question-reject-and-change-status.txt'
--- lib/lp/answers/stories/question-reject-and-change-status.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/answers/stories/question-reject-and-change-status.txt	2018-06-02 19:58:15 +0000
@@ -31,14 +31,14 @@
 They need to enter a message explaining the rejection:
 
     >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     There is 1 error.
     You must provide an explanation message.
 
 At this point the user might decide this is a bad idea, so there is a
 cancel link to take them back to the question:
 
-    >>> print admin_browser.getLink('Cancel').url
+    >>> print(admin_browser.getLink('Cancel').url)
     http://answers.launchpad.dev/firefox/+question/2
 
 Entering an explanation message and clicking the 'Reject' button,
@@ -51,14 +51,14 @@
 Once the question is rejected, a confirmation message is shown;
 
     >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     You have rejected this question.
 
 its status is changed to 'Invalid';
 
     >>> def print_question_status(browser):
-    ...     print extract_text(
-    ...         find_tag_by_id(browser.contents, 'question-status'))
+    ...     print(extract_text(
+    ...         find_tag_by_id(browser.contents, 'question-status')))
 
     >>> print_question_status(admin_browser)
     Status: Invalid ...
@@ -66,22 +66,22 @@
 and the rejection message is added to the question board.
 
     >>> content = find_main_content(admin_browser.contents)
-    >>> print content.findAll('div', 'boardCommentBody')[-1].renderContents()
+    >>> print(content.findAll('div', 'boardCommentBody')[-1].renderContents())
     <p>Rejecting because it&#x27;s a duplicate of <a...>bug #1</a>.</p>
 
 The call to help with this problem is not displayed.
 
-    >>> print content.find(id='can-you-help-with-this-problem')
+    >>> print(content.find(id='can-you-help-with-this-problem'))
     None
 
 Selecting the 'Reject' action again, will simply display a message
 stating that the question is already rejected:
 
     >>> admin_browser.getLink('Reject question').click()
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://answers.launchpad.dev/firefox/+question/2
     >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     The question is already rejected.
 
 = Changing the Question Status =
@@ -116,7 +116,7 @@
 
 There is also a cancel link should the user decide otherwise:
 
-    >>> print admin_browser.getLink('Cancel').url
+    >>> print(admin_browser.getLink('Cancel').url)
     http://answers.launchpad.dev/firefox/+question/2
 
 The user needs to select a status and enter a message explaining the
@@ -124,7 +124,7 @@
 
     >>> admin_browser.getControl('Change Status').click()
     >>> for error in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print error.renderContents()
+    ...     print(error.renderContents())
     There are 2 errors.
     You didn&#x27;t change the status.
     You must provide an explanation message.
@@ -141,7 +141,7 @@
 Once the operation is completed, a confirmation message is displayed;
 
     >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     Question status updated.
 
 its status is updated;
@@ -152,6 +152,6 @@
 and the explanation message is added to the question discussion:
 
     >>> content = find_main_content(admin_browser.contents)
-    >>> print content.findAll('div', 'boardCommentBody')[-1].renderContents()
+    >>> print(content.findAll('div', 'boardCommentBody')[-1].renderContents())
     <p>Setting status back to &#x27;Open&#x27;. Questions similar to a
     bug report should be linked to it, not rejected.</p>

=== modified file 'lib/lp/answers/stories/question-search-multiple-languages.txt'
--- lib/lp/answers/stories/question-search-multiple-languages.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/answers/stories/question-search-multiple-languages.txt	2018-06-02 19:58:15 +0000
@@ -15,7 +15,7 @@
     >>> anon_browser.open('http://launchpad.dev/distros/ubuntu/+questions')
     >>> soup = find_main_content(anon_browser.contents)
     >>> for question in soup.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Continue playing after shutdown
     Play DVDs in Totem
     mailto: problem in webpage
@@ -26,7 +26,7 @@
     >>> anon_browser.getLink('Next').click()
     >>> soup = find_main_content(anon_browser.contents)
     >>> for question in soup.findAll('td', 'questionTITLE'):
-    ...     print question.find('a').renderContents()
+    ...     print(question.find('a').renderContents())
     Installation failed
 
 The questions match the languages inferred from by GeoIP
@@ -58,7 +58,7 @@
 of questions declares its language and text direction.
 
     >>> for question in table.findAll('tr', lang=True):
-    ...     print 'lang="%s" dir="%s"' % (question['lang'], question['dir'])
+    ...     print('lang="%s" dir="%s"' % (question['lang'], question['dir']))
     lang="es" dir="ltr"
     lang="en" dir="ltr"
     lang="en" dir="ltr"
@@ -78,7 +78,7 @@
     '\xd8\xb9\xd9\x83\xd8\xb3 ...
 
     >>> for question in table.findAll('tr', lang=True):
-    ...     print 'lang="%s" dir="%s"' % (question['lang'], question['dir'])
+    ...     print('lang="%s" dir="%s"' % (question['lang'], question['dir']))
     lang="en" dir="ltr"
     lang="en" dir="ltr"
     lang="ar" dir="rtl"
@@ -101,10 +101,10 @@
     >>> anon_browser.getControl(name='field.language')
     Traceback (most recent call last):
       ...
-    LookupError: name 'field.language...
+    LookupError: name u'field.language...
 
     >>> content = find_main_content(anon_browser.contents).find('p')
-    >>> print content.renderContents()
+    >>> print(content.renderContents())
     There are no questions for Kubuntu with the requested statuses.
 
 When the project has questions in only one language, and that language
@@ -119,7 +119,7 @@
     >>> anon_browser.getControl(name='field.language')
     Traceback (most recent call last):
       ...
-    LookupError: name 'field.language...
+    LookupError: name u'field.language...
 
 But if the user configures their browser to accept Spanish and English
 then questions with those language will be displayed:
@@ -172,7 +172,7 @@
     >>> user_browser.getControl(name='field.language')
     Traceback (most recent call last):
       ...
-    LookupError: name 'field.language...
+    LookupError: name u'field.language...
 
 When No Privileges Person adds Spanish and English to their browser, they
 are added to the language controls.
@@ -268,12 +268,12 @@
 
     >>> user_browser.open('http://answers.launchpad.dev/kubuntu')
     >>> paragraph = find_main_content(user_browser.contents).find('p')
-    >>> print extract_text(paragraph)
+    >>> print(extract_text(paragraph))
     There are no questions for Kubuntu with the requested statuses.
 
     >>> user_browser.open('http://answers.launchpad.dev/firefox')
     >>> paragraph = find_main_content(user_browser.contents).find('p')
-    >>> print extract_text(paragraph)
+    >>> print(extract_text(paragraph))
     Mozilla Firefox has unanswered questions in the following languages:
     1 in Portuguese (Brazil). Can you help?
 
@@ -284,11 +284,11 @@
 the preceding page.
 
     >>> user_browser.getLink('Portuguese (Brazil)').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Portuguese (Brazil) questions in Mozilla Firefox : Questions : Mozilla Firefox
 
     >>> language_field = user_browser.getControl(name='field.language')
-    >>> print language_field.type
+    >>> print(language_field.type)
     hidden
 
     >>> labels = user_browser.getControl(name='field.status').displayValue
@@ -315,13 +315,13 @@
     >>> sample_person_browser.open('http://answers.launchpad.dev/ubuntu')
     >>> sample_person_browser.getLink('My questions').click()
     >>> sample_person_browser.getLink('Next').click()
-    >>> print sample_person_browser.title
+    >>> print(sample_person_browser.title)
     Questions you asked about Ubuntu : Questions : Ubuntu
 
     >>> questions = find_tag_by_id(
     ...     sample_person_browser.contents, 'question-listing')
     >>> for key in ('lang', 'dir'):
-    ...     print '%s: %s ' % (key, questions.tbody.tr[key])
+    ...     print('%s: %s ' % (key, questions.tbody.tr[key]))
     lang: ar
     dir: rtl
 
@@ -333,7 +333,7 @@
 
     >>> sample_person_browser.getLink(
     ...     'Change your preferred languages').click()
-    >>> print sample_person_browser.title
+    >>> print(sample_person_browser.title)
     Language preferences...
 
     >>> sample_person_browser.getControl('Arabic', index=0).selected

=== modified file 'lib/lp/answers/stories/question-subscriptions.txt'
--- lib/lp/answers/stories/question-subscriptions.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/answers/stories/question-subscriptions.txt	2018-06-02 19:58:15 +0000
@@ -13,10 +13,10 @@
     >>> user_browser.open(
     ...     'http://launchpad.dev/firefox/+question/2')
     >>> user_browser.getLink('Subscribe').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Subscription : Question #2 ...
 
-    >>> print user_browser.getLink('your subscribed questions page')
+    >>> print(user_browser.getLink('your subscribed questions page'))
     <Link ...'http://answers.launchpad.dev/~no-priv/+subscribedquestions'>
 
     >>> user_browser.getControl('Subscribe').click()
@@ -34,11 +34,11 @@
 link and then click on the 'Unsubscribe' button.
 
     >>> link = user_browser.getLink('Unsubscribe').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Subscription : Question #2 ...
 
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'unsubscribe'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'unsubscribe')))
     Unsubscribing from this question ...
 
     >>> user_browser.getControl('Unsubscribe').click()

=== modified file 'lib/lp/answers/stories/question-workflow.txt'
--- lib/lp/answers/stories/question-workflow.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/answers/stories/question-workflow.txt	2018-06-02 19:58:15 +0000
@@ -17,15 +17,15 @@
     # added and the status of the question.
 
     >>> def find_request_status(contents):
-    ...     print extract_text(
-    ...         find_tag_by_id(contents, 'question-status'))
+    ...     print(extract_text(
+    ...         find_tag_by_id(contents, 'question-status')))
 
     >>> def  find_last_comment(contents):
     ...     soup = find_main_content(contents)
     ...     return soup.findAll('div', 'boardCommentBody')[-1]
 
     >>> def print_last_comment(contents):
-    ...     print extract_text(find_last_comment(contents))
+    ...     print(extract_text(find_last_comment(contents)))
 
 
 Logging In
@@ -34,7 +34,7 @@
 To participate in a question, the user must be logged in.
 
     >>> anon_browser.open('http://launchpad.dev/firefox/+question/2')
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     <!DOCTYPE...
     ...
     To post a message you must <a href="+login">log in</a>.
@@ -58,11 +58,11 @@
     ...     'http://launchpad.dev/firefox/+question/2')
     >>> content = find_tag_by_id(
     ...     support_browser.contents, 'can-you-help-with-this-problem')
-    >>> print content.h2.renderContents()
+    >>> print(content.h2.renderContents())
     Can you help with this problem?
 
-    >>> print extract_text(
-    ...     find_tag_by_id(support_browser.contents, 'horizontal-menu'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(support_browser.contents, 'horizontal-menu')))
     Link existing bug
     Create bug report
     Link to a FAQ
@@ -86,7 +86,7 @@
 
     >>> support_browser.getControl('Add Information Request').click()
     >>> soup = find_main_content(support_browser.contents)
-    >>> print soup.find('div', 'message').renderContents()
+    >>> print(soup.find('div', 'message').renderContents())
     Please enter a message.
 
 
@@ -104,7 +104,7 @@
 This appends the comment to the question and it doesn't change its
 status:
 
-    >>> print find_request_status(support_browser.contents)
+    >>> print(find_request_status(support_browser.contents))
     Status: Needs information ...
 
     >>> print_last_comment(support_browser.contents)
@@ -137,7 +137,7 @@
 status is changed to Open and their answer appended to the question
 discussion.
 
-    >>> print find_request_status(owner_browser.contents)
+    >>> print(find_request_status(owner_browser.contents))
     Status: Open ...
 
     >>> print_last_comment(owner_browser.contents)
@@ -162,7 +162,7 @@
 This moves the question to the Answered state and adds the answer to
 the end of the discussion:
 
-    >>> print find_request_status(support_browser.contents)
+    >>> print(find_request_status(support_browser.contents))
     Status: Answered ...
 
     >>> print_last_comment(support_browser.contents)
@@ -191,7 +191,7 @@
 
     >>> answer_button_paragraph = find_tag_by_id(
     ...     owner_browser.contents, 'answer-button-hint')
-    >>> print extract_text(answer_button_paragraph)
+    >>> print(extract_text(answer_button_paragraph))
     To confirm an answer, use the 'This Solved My Problem' button located at
     the bottom of the answer.
 
@@ -202,7 +202,7 @@
 This changes the status of the question to 'Solved' and mark 'No
 Privileges Person' as the solver.
 
-    >>> print find_request_status(owner_browser.contents)
+    >>> print(find_request_status(owner_browser.contents))
     Status: Solved ...
 
 Since no message can be provided when that button is clicked. A default
@@ -215,18 +215,18 @@
 
     >>> soup = find_main_content(owner_browser.contents)
     >>> bestAnswer = soup.findAll('div', 'boardComment')[-2]
-    >>> print bestAnswer.find('img')
+    >>> print(bestAnswer.find('img'))
     <img src="/@@/favourite-yes" ... title="Marked as best answer" />
 
-    >>> print soup.find(
-    ...     'div', 'boardCommentBody highlighted').renderContents()
+    >>> print(soup.find(
+    ...     'div', 'boardCommentBody highlighted').renderContents())
     <p>New version of the firefox package are available with SVG support
     enabled. You can use apt-get or adept to upgrade.</p>
 
 The History link should now show up.
 
-    >>> print extract_text(
-    ...     find_tag_by_id(support_browser.contents, 'horizontal-menu'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(support_browser.contents, 'horizontal-menu')))
     History
     Link existing bug
     Create bug report
@@ -248,7 +248,7 @@
 This appends the comment to the question and it doesn't change its
 status:
 
-    >>> print find_request_status(owner_browser.contents)
+    >>> print(find_request_status(owner_browser.contents))
     Status: Solved ...
 
     >>> print_last_comment(owner_browser.contents)
@@ -273,7 +273,7 @@
 This appends the new information to the question discussion and changes
 its status back to 'Open'.
 
-    >>> print find_request_status(owner_browser.contents)
+    >>> print(find_request_status(owner_browser.contents))
     Status: Open ...
 
     >>> print_last_comment(owner_browser.contents)
@@ -299,8 +299,8 @@
 In addition, this creates a reopening record that is displayed in the
 reopening portlet.
 
-    >>> print extract_text(
-    ...     find_tag_by_id(owner_browser.contents, 'portlet-reopenings'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(owner_browser.contents, 'portlet-reopenings')))
     This question was reopened ... Sample Person
 
 
@@ -334,7 +334,7 @@
 question owner to solve their problem.
 
     >>> for message in soup.findAll('div', 'informational message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     Your question is solved. If a particular message helped you solve the
     problem, use the 'This solved my problem' button.
 
@@ -349,21 +349,21 @@
 
     >>> soup = find_main_content(owner_browser.contents)
     >>> bestAnswer = soup.find('img', {'title' : 'Marked as best answer'})
-    >>> print bestAnswer
+    >>> print(bestAnswer)
     <img src="/@@/favourite-yes" ... title="Marked as best answer" />
 
     >>> answerer = bestAnswer.parent.find('a')
-    >>> print extract_text(answerer)
+    >>> print(extract_text(answerer))
     No Privileges Person (no-priv)
 
     >>> message = soup.find(
     ...     'div', 'boardCommentBody highlighted')
-    >>> print message
+    >>> print(message)
     <div class="boardCommentBody highlighted"
     itemprop="commentText"><p>New version of the firefox package are
     available with SVG support enabled. You can use apt-get or adept to
     upgrade.</p></div>
-    >>> print extract_text(message)
+    >>> print(extract_text(message))
     New version of the firefox package are available with SVG support
     enabled. You can use apt-get or adept to upgrade.
 
@@ -376,7 +376,7 @@
     >>> anon_browser.open(
     ...     'http://launchpad.dev/firefox/+question/2')
     >>> anon_browser.getLink('History').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     History of question #2...
 
 It lists all the actions performed through workflow on the question:
@@ -384,7 +384,7 @@
     >>> soup = find_main_content(anon_browser.contents)
     >>> action_listing = soup.find('table', 'listing')
     >>> for header in action_listing.findAll('th'):
-    ...     print header.renderContents()
+    ...     print(header.renderContents())
     When
     Who
     Action
@@ -395,7 +395,7 @@
     ...     who = extract_text(cells[1].find('a'))
     ...     action = cells[2].renderContents()
     ...     new_status = cells[3].renderContents()
-    ...     print who.lstrip('&nbsp;'), action, new_status
+    ...     print(who.lstrip('&nbsp;'), action, new_status)
     No Privileges Person Request for more information Needs information
     No Privileges Person Comment Needs information
     Sample Person        Give more information        Open
@@ -421,7 +421,7 @@
 
     >>> carlos_browser = setupBrowser(auth='Basic carlos@xxxxxxxxxxxxx:test')
     >>> carlos_browser.open('http://launchpad.dev/firefox/+question/12')
-    >>> print find_request_status(carlos_browser.contents)
+    >>> print(find_request_status(carlos_browser.contents))
     Status: Open ...
 
     >>> answer_button_paragraph = find_tag_by_id(
@@ -432,7 +432,7 @@
     >>> carlos_browser.getControl('Message').value = (
     ...     "There is a bug in that version. SMP is fine after upgrading.")
     >>> carlos_browser.getControl("Problem Solved").click()
-    >>> print find_request_status(carlos_browser.contents)
+    >>> print(find_request_status(carlos_browser.contents))
     Status: Solved ...
 
     >>> content = find_main_content(carlos_browser.contents)
@@ -456,9 +456,9 @@
     >>> user_browser.open('http://launchpad.dev/firefox/+question/2')
 
     >>> content = find_main_content(user_browser.contents)
-    >>> print content.find(id='can-you-help-with-this-problem')
+    >>> print(content.find(id='can-you-help-with-this-problem'))
     None
 
     >>> user_browser.getLink('Ask a question').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Ask a question about...

=== modified file 'lib/lp/answers/stories/questions-index.txt'
--- lib/lp/answers/stories/questions-index.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/answers/stories/questions-index.txt	2018-06-02 19:58:15 +0000
@@ -17,14 +17,14 @@
     >>> transaction.commit()
 
     >>> anon_browser.open('http://answers.launchpad.dev/')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Launchpad Answers
 
 It shows the 5 latest questions asked:
 
     >>> latest_questions_asked = find_tag_by_id(
     ...     anon_browser.contents, 'latest-questions-asked')
-    >>> print latest_questions_asked.find('h2').renderContents()
+    >>> print(latest_questions_asked.find('h2').renderContents())
     Latest questions asked
     >>> for row in latest_questions_asked.findAll('li'):
     ...     row.find('a').renderContents()
@@ -38,7 +38,7 @@
 
     >>> latest_questions_solved = find_tag_by_id(
     ...     anon_browser.contents, 'latest-questions-solved')
-    >>> print latest_questions_solved.find('h2').renderContents()
+    >>> print(latest_questions_solved.find('h2').renderContents())
     Latest questions solved
     >>> for row in latest_questions_solved.findAll('li'):
     ...     row.find('a').renderContents()
@@ -48,8 +48,8 @@
 
     # Replace numbers with X in output.
     >>> import re
-    >>> print re.sub('\d+', 'X', extract_text(find_tag_by_id(
-    ...     anon_browser.contents, 'application-footer')))
+    >>> print(re.sub('\d+', 'X', extract_text(find_tag_by_id(
+    ...     anon_browser.contents, 'application-footer'))))
     X questions answered and X questions solved out of
     X questions asked across X projects
 
@@ -57,8 +57,8 @@
 (Since sample data contains no projects with a question asked in the
 last 60 days, this list is empty):
 
-    >>> print extract_text(find_tag_by_id(
-    ...     anon_browser.contents, 'most-active-projects'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     anon_browser.contents, 'most-active-projects')))
     Most active projects
 
 Add some recent questions so that this listing contains something.
@@ -72,8 +72,8 @@
     >>> logout()
 
     >>> anon_browser.open('http://answers.launchpad.dev/')
-    >>> print extract_text(find_tag_by_id(
-    ...     anon_browser.contents, 'most-active-projects'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     anon_browser.contents, 'most-active-projects')))
     Most active projects
     Ubuntu
     Mozilla Firefox
@@ -82,7 +82,7 @@
 Answers front page:
 
     >>> anon_browser.getLink('Ubuntu').click()
-    >>> print anon_browser.url
+    >>> print(anon_browser.url)
     http://answers.launchpad.dev/ubuntu
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Questions : Ubuntu

=== modified file 'lib/lp/answers/stories/this-is-a-faq.txt'
--- lib/lp/answers/stories/this-is-a-faq.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/answers/stories/this-is-a-faq.txt	2018-06-02 19:58:15 +0000
@@ -36,11 +36,11 @@
     >>> from lp.services.helpers import backslashreplace
     >>> user_browser.open(
     ...     'http://answers.launchpad.dev/firefox/+question/2')
-    >>> print backslashreplace(user_browser.title)
+    >>> print(backslashreplace(user_browser.title))
     Question #2 : ...
 
     >>> user_browser.getLink('Link to a FAQ').click()
-    >>> print backslashreplace(user_browser.title)
+    >>> print(backslashreplace(user_browser.title))
     Is question #2 a FAQ...
 
 This page lists the existing FAQs matching the question's title. In
@@ -63,20 +63,20 @@
     ...             link = button.findNext('a').renderContents()
     ...         else:
     ...             link = ''
-    ...         print radio, label, link
+    ...         print(radio, label, link)
     >>> printFAQOptions(user_browser.contents)
     (*) No existing FAQs are relevant
     ( ) 8: How do I install Extensions?
     ( ) 9: How do I troubleshoot problems with extensions/themes?
 
-    >>> print user_browser.getLink('How do I troubleshoot problems').url
+    >>> print(user_browser.getLink('How do I troubleshoot problems').url)
     http://answers.launchpad.dev/firefox/+faq/9
 
 The query used to find these results is displayed in the search field
 under the radio widgets. That query defaults to the question's title.
 
     >>> search_field = user_browser.getControl(name='field.faq-query')
-    >>> print search_field.value
+    >>> print(search_field.value)
     SVG extension
 
 From the titles, it doesn't seem like one of these FAQs would be
@@ -88,7 +88,7 @@
 
 The page is updated with a new list of FAQs:
 
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Is question #2 a FAQ...
     >>> printFAQOptions(user_browser.contents)
     (*) No existing FAQs are relevant
@@ -104,7 +104,7 @@
 It is pre-filled, but they can change its value. The FAQ reference will
 be appended to the message.
 
-    >>> print user_browser.getControl('Message').value
+    >>> print(user_browser.getControl('Message').value)
     No Privileges Person suggests this article as an answer to your question:
 
 They can then click 'Link to FAQ' to answer the question with the selected
@@ -112,31 +112,31 @@
 page.
 
     >>> user_browser.getControl('Link to FAQ').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://answers.launchpad.dev/firefox/+question/2
 
 They see that the question's status was changed to 'Answered':
 
     >>> def print_question_status(browser):
-    ...     print extract_text(
-    ...         find_tag_by_id(browser.contents, 'question-status'))
+    ...     print(extract_text(
+    ...         find_tag_by_id(browser.contents, 'question-status')))
 
     >>> print_question_status(user_browser)
     Status: Answered
 
 A link to the FAQ appears under the question's description:
 
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'related-faq'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'related-faq')))
     Related FAQ: How do I install plugins (Shockwave, QuickTime, etc.)? ...
-    >>> print user_browser.getLink('How do I install plugins').url
+    >>> print(user_browser.getLink('How do I install plugins').url)
     http://answers.launchpad.dev/firefox/+faq/10
 
 The answer message was added to the question's discussion:
 
-    >>> print extract_text(find_tags_by_class(
+    >>> print(extract_text(find_tags_by_class(
     ...     user_browser.contents, 'boardCommentBody')[-1]).encode(
-    ...     'ascii', 'backslashreplace')
+    ...     'ascii', 'backslashreplace'))
     No Privileges Person suggests this article as an answer to your question:
     FAQ #10: \u201cHow do I install plugins...
 
@@ -169,7 +169,7 @@
 But since they forgot to change the link, the form is displayed again
 with an error message.
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://answers.launchpad.dev/firefox/+question/2/+linkfaq
     >>> print_feedback_messages(user_browser.contents)
     There is 1 error.
@@ -183,15 +183,15 @@
 
 The new message was added to the question:
 
-    >>> print extract_text(find_tags_by_class(
+    >>> print(extract_text(find_tags_by_class(
     ...     user_browser.contents, 'boardCommentBody')[-1]).encode(
-    ...     'ascii', 'backslashreplace')
+    ...     'ascii', 'backslashreplace'))
     Sorry, this document doesn&#x27;t really answer your question.
 
 The link was also removed from the details portlet:
 
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'related-faq'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'related-faq')))
     Related FAQ: None ...
 
 
@@ -231,19 +231,19 @@
     <Link text='Create a new FAQ' url='http://.../firefox/+question/2/+createfaq'>
     >>> owner_browser.getLink('Link to a FAQ').click()
     >>> owner_browser.getLink('create a new FAQ').click()
-    >>> print owner_browser.url
+    >>> print(owner_browser.url)
     http://answers.launchpad.dev/firefox/+question/2/+createfaq
-    >>> print owner_browser.title
+    >>> print(owner_browser.title)
     Create a FAQ for Mozilla...
 
 The FAQ title and content are pre-filled with the target question. They
 edit them to be more appropriate:
 
-    >>> print owner_browser.getControl('Title').value
+    >>> print(owner_browser.getControl('Title').value)
     SVG extension
     >>> owner_browser.getControl('Title').value = 'Displaying SVG in Firefox'
 
-    >>> print owner_browser.getControl('Content').value
+    >>> print(owner_browser.getControl('Content').value)
     Hi! I'm trying to learn about SVG but I can't get it to work at all in
     firefox. Maybe there is a plugin? Help! Thanks.
 
@@ -258,8 +258,8 @@
 There is a 'Message' field that will be used to answer the question.
 It is pre-filled, but they can change its value:
 
-    >>> print owner_browser.getControl(
-    ...     'Additional comment for question #2').value
+    >>> print(owner_browser.getControl(
+    ...     'Additional comment for question #2').value)
     Sample Person suggests this article as an answer to your question:
 
     >>> owner_browser.getControl(
@@ -270,22 +270,22 @@
 returned to the question page.
 
     >>> owner_browser.getControl('Create and Link').click()
-    >>> print owner_browser.url
+    >>> print(owner_browser.url)
     http://answers.launchpad.dev/firefox/+question/2
 
 The answer message was added to the question's discussion:
 
-    >>> print extract_text(find_tags_by_class(
+    >>> print(extract_text(find_tags_by_class(
     ...     owner_browser.contents, 'boardCommentBody')[-1]).encode(
-    ...     'ascii', 'backslashreplace')
+    ...     'ascii', 'backslashreplace'))
     Read the Fine Answer:
     FAQ...: \u201cDisplaying SVG in Firefox\u201d.
 
 And the link to the created FAQ is displayed under the question's
 description:
 
-    >>> print extract_text(
-    ...     find_tag_by_id(owner_browser.contents, 'related-faq'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(owner_browser.contents, 'related-faq')))
     Related FAQ: Displaying SVG in Firefox ...
 
 
@@ -295,37 +295,36 @@
 FAQ title to display the FAQ content.
 
     >>> owner_browser.getLink('Displaying SVG in Firefox').click()
-    >>> print owner_browser.url
+    >>> print(owner_browser.url)
     http://answers.launchpad.dev/firefox/+faq/...
-    >>> print backslashreplace(owner_browser.title)
+    >>> print(backslashreplace(owner_browser.title))
     FAQ #... : Questions : Mozilla Firefox
 
 The FAQ keywords and content appears just below:
 
-    >>> print extract_text(find_tag_by_id(
-    ...     owner_browser.contents, 'faq-keywords'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     owner_browser.contents, 'faq-keywords')))
     Keywords: scalable vector graphic
 
-    >>> print extract_text(find_tag_by_id(
-    ...     owner_browser.contents, 'faq-content'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     owner_browser.contents, 'faq-content')))
     Upgrade your browser to Firefox 2.0.
 
 The FAQ's original author and creation date appears in the header:
 
-    >>> print extract_text(
-    ...     find_tag_by_id(owner_browser.contents, 'registration'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(owner_browser.contents, 'registration')))
     Created by Sample Person on ...
 
 A 'Related questions' portlet contains links to the question answered by
 the FAQ:
 
-    >>> print extract_text(find_portlet(
-    ...     owner_browser.contents, 'Related questions'))
+    >>> print(extract_text(find_portlet(
+    ...     owner_browser.contents, 'Related questions')))
     Related questions
     #2 SVG extension
 
-    >>> print owner_browser.getLink(
-    ...     'SVG extension').url
+    >>> print(owner_browser.getLink('SVG extension').url)
     http://answers.launchpad.dev/firefox/+question/2
 
 
@@ -336,16 +335,16 @@
 
     >>> user_browser.open(
     ...     'http://answers.launchpad.dev/ubuntu/+question/11')
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Question #11 : ...
     >>> user_browser.getLink('Link to a FAQ').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Is question #11 a FAQ...
 
     >>> user_browser.open(
     ...     'http://answers.launchpad.dev/ubuntu/+source/mozilla-firefox'
     ...     '/+question/8')
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Question #8 : ...
     >>> user_browser.getLink('Link to a FAQ').click()
     >>> user_browser.title
@@ -370,7 +369,7 @@
     Status: Solved
     >>> user_browser.getLink('Link to a FAQ').click()
 
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Is question #9 a FAQ...
     >>> user_browser.getControl(name='field.faq-query').value = 'flash'
     >>> user_browser.getControl('Search', index=0).click()
@@ -382,19 +381,19 @@
 added to the question, and their message was added to the question's
 discussion.
 
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Question #9 : ...
     >>> print_question_status(user_browser)
     Status: Solved
 
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'related-faq'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'related-faq')))
     Related FAQ:
     How can I play MP3/Divx/DVDs/Quicktime/Realmedia files ...
 
-    >>> print extract_text(find_tags_by_class(
+    >>> print(extract_text(find_tags_by_class(
     ...     user_browser.contents, 'boardCommentBody')[-1]).encode(
-    ...     'ascii', 'backslashreplace')
+    ...     'ascii', 'backslashreplace'))
     The FAQ mentions this:
     FAQ #6: ...How can I play MP3/Divx/DVDs/Quicktime/Realmedia files...
 

=== modified file 'lib/lp/answers/stories/webservice.txt'
--- lib/lp/answers/stories/webservice.txt	2012-08-13 19:34:10 +0000
+++ lib/lp/answers/stories/webservice.txt	2018-06-02 19:58:15 +0000
@@ -142,10 +142,10 @@
     ...     sort='oldest first',
     ...     api_version='devel').jsonBody()
     >>> for question in questions['entries']:
-    ...     print question['title']
+    ...     print(question['title'])
     Q 1 great
 
-    >>> print questions['total_size']
+    >>> print(questions['total_size'])
     1
 
 Anyone can retrieve a collection of questions from an `IQuestionTarget` that
@@ -158,7 +158,7 @@
     ...     phrase='q great',
     ...     api_version='devel').jsonBody()
     >>> for question in questions['entries']:
-    ...     print question['title']
+    ...     print(question['title'])
     Q 1 great
     Q 2 greater
 
@@ -168,7 +168,7 @@
     >>> question_1 = anon_webservice.named_get(
     ...     project['self_link'], 'getQuestion', question_id=_question_1.id,
     ...     api_version='devel').jsonBody()
-    >>> print question_1['title']
+    >>> print(question_1['title'])
     Q 1 great
 
 
@@ -185,7 +185,7 @@
     ...     sort='oldest first',
     ...     api_version='devel').jsonBody()
     >>> for question in questions['entries']:
-    ...     print question['title']
+    ...     print(question['title'])
     Q 1 great
 
 

=== modified file 'lib/lp/answers/tests/test_doc.py'
--- lib/lp/answers/tests/test_doc.py	2018-06-01 23:12:25 +0000
+++ lib/lp/answers/tests/test_doc.py	2018-06-02 19:58:15 +0000
@@ -22,6 +22,7 @@
     login,
     )
 from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.pages import setUpGlobs
 from lp.testing.systemdocs import (
     LayeredDocFileSuite,
     setUp,
@@ -124,4 +125,5 @@
 
 def test_suite():
     return build_test_suite(
-        here, special, setUp=lambda test: setUp(test, future=True))
+        here, special, setUp=lambda test: setUp(test, future=True),
+        pageTestsSetUp=lambda test: setUpGlobs(test, future=True))


References