← Back to team overview

launchpad-reviewers team mailing list archive

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

 

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

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

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/bugs-pagetests-future-imports/+merge/348788
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/bugs-pagetests-future-imports into lp:launchpad.
=== modified file 'lib/lp/bugs/browser/tests/bug-nomination-views.txt'
--- lib/lp/bugs/browser/tests/bug-nomination-views.txt	2014-11-28 22:07:05 +0000
+++ lib/lp/bugs/browser/tests/bug-nomination-views.txt	2018-06-30 16:32:46 +0000
@@ -30,9 +30,9 @@
     >>> ignored = login_person(nominator)
     >>> request = LaunchpadTestRequest()
     >>> bug_one_in_ubuntu_firefox = getUtility(IBugTaskSet).get(17)
-    >>> print bug_one_in_ubuntu_firefox.bug.id
+    >>> print(bug_one_in_ubuntu_firefox.bug.id)
     1
-    >>> print bug_one_in_ubuntu_firefox.target.bugtargetdisplayname
+    >>> print(bug_one_in_ubuntu_firefox.target.bugtargetdisplayname)
     mozilla-firefox (Ubuntu)
 
     >>> nomination_view = getMultiAdapter(
@@ -80,15 +80,15 @@
     >>> len(request.response.notifications)
     1
 
-    >>> print request.response.notifications[0].message
+    >>> print(request.response.notifications[0].message)
     Added nominations for: Ubuntu Warty
 
 Here's an example of nominating a bug for a productseries.
 
     >>> bug_one_in_firefox = getUtility(IBugTaskSet).get(2)
-    >>> print bug_one_in_firefox.bug.id
+    >>> print(bug_one_in_firefox.bug.id)
     1
-    >>> print bug_one_in_firefox.target.bugtargetdisplayname
+    >>> print(bug_one_in_firefox.target.bugtargetdisplayname)
     Mozilla Firefox
 
     >>> firefox = bug_one_in_firefox.target
@@ -121,7 +121,7 @@
     >>> len(request.response.notifications)
     1
 
-    >>> print request.response.notifications[0].message
+    >>> print(request.response.notifications[0].message)
     Added nominations for: Mozilla Firefox trunk
 
 
@@ -158,7 +158,7 @@
 approval.
 
     >>> hoary_nomination.decline(launchbag.user)
-    >>> print hoary_nomination.status.title
+    >>> print(hoary_nomination.status.title)
     Declined
 
     >>> hoary_nomination_edit_form.shouldShowApproveButton(None)
@@ -179,14 +179,14 @@
 
     >>> ubuntu_nominations = bug_one.getNominations(ubuntu)
     >>> for nomination in ubuntu_nominations:
-    ...     print nomination.target.bugtargetdisplayname
+    ...     print(nomination.target.bugtargetdisplayname)
     Ubuntu Hoary
     Ubuntu Warty
 
 Bug #1 currently has three tasks.
 
     >>> for bugtask in bug_one.bugtasks:
-    ...     print bugtask.bugtargetdisplayname
+    ...     print(bugtask.bugtargetdisplayname)
     Mozilla Firefox
     mozilla-firefox (Ubuntu)
     mozilla-firefox (Debian)
@@ -201,7 +201,7 @@
     >>> login("celso.providelo@xxxxxxxxxxxxx")
 
     >>> cprov = launchbag.user
-    >>> print cprov.name
+    >>> print(cprov.name)
     cprov
 
     >>> cprov.inTeam(ubuntu_team)
@@ -220,9 +220,9 @@
 
     >>> def print_nominations(nominations):
     ...     for nomination in nominations:
-    ...         print "%s, %s" % (
+    ...         print("%s, %s" % (
     ...             nomination.target.bugtargetdisplayname,
-    ...             nomination.status.title)
+    ...             nomination.status.title))
     >>> print_nominations(bug_one.getNominations(ubuntu))
     Ubuntu Hoary, Declined
     Ubuntu Warty, Nominated
@@ -240,7 +240,7 @@
     Ubuntu Warty, Nominated
 
     >>> for bugtask in bug_one.bugtasks:
-    ...     print bugtask.bugtargetdisplayname
+    ...     print(bugtask.bugtargetdisplayname)
     Mozilla Firefox
     mozilla-firefox (Ubuntu)
     mozilla-firefox (Ubuntu Grumpy)
@@ -248,5 +248,5 @@
 
 The notification message also changes slightly.
 
-    >>> print request.response.notifications[0].message
+    >>> print(request.response.notifications[0].message)
     Targeted bug to: Ubuntu Grumpy

=== modified file 'lib/lp/bugs/browser/tests/bug-views.txt'
--- lib/lp/bugs/browser/tests/bug-views.txt	2015-10-06 06:48:01 +0000
+++ lib/lp/bugs/browser/tests/bug-views.txt	2018-06-30 16:32:46 +0000
@@ -15,7 +15,7 @@
     >>> from lazr.lifecycle.event import IObjectCreatedEvent
     >>> from lp.bugs.interfaces.bug import IBug
     >>> def on_created_event(object, event):
-    ...     print "ObjectCreatedEvent: %r" % object
+    ...     print("ObjectCreatedEvent: %r" % object)
     >>> on_created_listener = TestEventListener(
     ...     IBug, IObjectCreatedEvent, on_created_event)
 
@@ -71,13 +71,13 @@
 The user specified a binary package name, so that's been added to the
 bug description and the first comment:
 
-    >>> print latest_ubuntu_bugtask.bug.description
+    >>> print(latest_ubuntu_bugtask.bug.description)
     a bug in a bin pkg
 
 the source package from which the binary was built has been set on
 the bugtask.
 
-    >>> print latest_ubuntu_bugtask.sourcepackagename.name
+    >>> print(latest_ubuntu_bugtask.sourcepackagename.name)
     linux-source-2.6.15
 
 2. Filing a bug on a product.
@@ -157,7 +157,7 @@
 
     >>> ubuntu_bugview = getMultiAdapter(
     ...     (latest_ubuntu_bugtask, request), name="+index")
-    >>> print len(ubuntu_bugview.comments)
+    >>> print(len(ubuntu_bugview.comments))
     3
     >>> [ (c.index, c.owner.name, c.text_contents)
     ...  for c in ubuntu_bugview.comments ]
@@ -172,12 +172,12 @@
 When a user posts a new bug, the first comment and the description are
 identical. Take as an example the first bug posted above:
 
-    >>> print latest_ubuntu_bugtask.bug.description
+    >>> print(latest_ubuntu_bugtask.bug.description)
     a bug in a bin pkg
 
 Its description has the same contents as the bug's first comment:
 
-    >>> print latest_ubuntu_bugtask.bug.messages[0].text_contents
+    >>> print(latest_ubuntu_bugtask.bug.messages[0].text_contents)
     a bug in a bin pkg
 
 The view class offers a method to check exactly that:
@@ -203,7 +203,7 @@
 Because we omit the first comment, and because the third comment is
 indentical to the second, we really only display one comment:
 
-    >>> print len(comments)
+    >>> print(len(comments))
     1
     >>> [(c.index, c.owner.name, c.text_contents) for c in comments]
     [(1, u'name16', u'I can reproduce this bug.')]
@@ -233,9 +233,9 @@
 
     >>> bug_five_in_firefox = bugtaskset.get(14)
 
-    >>> print bug_five_in_firefox.bug.id
+    >>> print(bug_five_in_firefox.bug.id)
     5
-    >>> print bug_five_in_firefox.product.name
+    >>> print(bug_five_in_firefox.product.name)
     firefox
 
 
@@ -247,7 +247,7 @@
     >>> getUtility(IOpenLaunchBag).add(bug_five_in_firefox)
 
     >>> for dupe in bug_page_view.duplicates():
-    ...     print dupe['url']
+    ...     print(dupe['url'])
     http://.../firefox/+bug/6
 
 Bug 2 is not reported in Firefox. Let's mark bug 2 as a dupe of bug 5,
@@ -261,7 +261,7 @@
     ...     (bug_five_in_firefox.bug, request), name="+portlet-duplicates")
 
     >>> for dupe in bug_page_view.duplicates():
-    ...     print dupe['url']
+    ...     print(dupe['url'])
     http://.../bugs/2
     ...
 
@@ -476,10 +476,10 @@
     >>> def print_tasks_and_nominations(task_and_nomination_views):
     ...     for task_or_nomination_view in task_and_nomination_views:
     ...         task_or_nomination = task_or_nomination_view.context
-    ...         print "%s, %s, %s" % (
+    ...         print("%s, %s, %s" % (
     ...             get_object_type(task_or_nomination),
     ...             task_or_nomination.status.title,
-    ...             task_or_nomination.target.bugtargetdisplayname)
+    ...             task_or_nomination.target.bugtargetdisplayname))
 
     >>> task_and_nomination_views = (
     ...     bugtasks_and_nominations_view.getBugTaskAndNominationViews())
@@ -604,7 +604,7 @@
     >>> from lp.bugs.browser.bug import BugEditView
     >>> class BugEditViewTest(BugEditView):
     ...     def index(self):
-    ...         print 'EDIT BUG'
+    ...         print('EDIT BUG')
 
     >>> firefox_task = bug_one.bugtasks[0]
     >>> firefox_task.bugtargetdisplayname
@@ -737,23 +737,23 @@
     ...     for activity in activities:
     ...         target_name = activity['target']
     ...         if target_name is None:
-    ...             print "Changed:"
+    ...             print("Changed:")
     ...         else:
-    ...             print "Changed in %s:" % target_name
+    ...             print("Changed in %s:" % target_name)
     ...         activity_items = activity['activity']
     ...         for activity_item in activity_items:
-    ...             print "* %s: %s => %s" % (
+    ...             print("* %s: %s => %s" % (
     ...                 activity_item.change_summary,
     ...                 activity_item.oldvalue,
-    ...                 activity_item.newvalue)
+    ...                 activity_item.newvalue))
 
     >>> def print_comment(comment):
-    ...     print comment.text_for_display
+    ...     print(comment.text_for_display)
     ...     print_activities(comment.activity)
 
     >>> def print_activity_and_comments(activity_and_comments):
     ...     for activity_or_comment in activity_and_comments:
-    ...         print "-- {person.name} --".format(**activity_or_comment)
+    ...         print("-- {person.name} --".format(**activity_or_comment))
     ...         if 'activity' in activity_or_comment:
     ...             print_activities(activity_or_comment["activity"])
     ...         if 'comment' in activity_or_comment:
@@ -826,7 +826,7 @@
 searched for.
 
     >>> for bug in view.similar_bugs:
-    ...     print bug.title
+    ...     print(bug.title)
     New title
     Reflow problems with complex page layouts
     Firefox install instructions should be complete
@@ -841,5 +841,5 @@
     ...     (firefox, request), name="+filebug-show-similar")
     >>> view.initialize()
     >>> for bug in view.similar_bugs:
-    ...     print bug.title
+    ...     print(bug.title)
     Reflow problems with complex page layouts

=== modified file 'lib/lp/bugs/browser/tests/buglinktarget-views.txt'
--- lib/lp/bugs/browser/tests/buglinktarget-views.txt	2012-12-10 13:43:47 +0000
+++ lib/lp/bugs/browser/tests/buglinktarget-views.txt	2018-06-30 16:32:46 +0000
@@ -28,10 +28,10 @@
 The +linkbug view is used to link bugs to IBugLinkTarget.
 
     >>> view = create_view(cve, name='+linkbug')
-    >>> print view.label
+    >>> print(view.label)
     Link a bug report
 
-    >>> print view.cancel_url
+    >>> print(view.cancel_url)
     http://bugs.launchpad.dev/bugs/cve/2005-2730
 
 It has a simple widget to enter the bug number or nickname of the bug to link
@@ -50,7 +50,7 @@
 
 Bug #1 was added to the object:
 
-    >>> print [bug.id for bug in cve.bugs]
+    >>> print([bug.id for bug in cve.bugs])
     [1]
 
 A ObjectModifiedEvent was sent:
@@ -82,10 +82,10 @@
 IBugLinkTarget. 
 
     >>> view = create_view(cve, name='+unlinkbug')
-    >>> print view.label
+    >>> print(view.label)
     Remove links to bug reports
 
-    >>> print view.cancel_url
+    >>> print(view.cancel_url)
     http://bugs.launchpad.dev/bugs/cve/2005-2730
 
 After removing the bugs, it sends a SQLObjectModified event.
@@ -103,7 +103,7 @@
 
 The two bugs were removed and only bug #3 should still be present:
 
-    >>> print [bug.id for bug in cve.bugs]
+    >>> print([bug.id for bug in cve.bugs])
     [3]
 
 A ObjectModifiedEvent was sent:
@@ -117,7 +117,7 @@
     True
     >>> event.edited_fields
     ['bugs']
-    >>> print [bug.id for bug in event.object_before_modification.bugs]
+    >>> print([bug.id for bug in event.object_before_modification.bugs])
     [1, 2, 3]
 
 
@@ -143,7 +143,7 @@
 The notification contains the escaped bug title.
 
     >>> for notification in request.response.notifications:
-    ...     print notification.message.encode('utf8')
+    ...     print(notification.message.encode('utf8'))
     Added link to bug #2:
     ...<script>window.alert("Hello!")</script>....
 

=== modified file 'lib/lp/bugs/browser/tests/bugs-views.txt'
--- lib/lp/bugs/browser/tests/bugs-views.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/browser/tests/bugs-views.txt	2018-06-30 16:32:46 +0000
@@ -26,7 +26,7 @@
     ...  for bugtask in BugTask.selectBy(_status=BugTaskStatus.FIXRELEASED)]
     [8]
     >>> for bug in bugs_view.most_recently_fixed_bugs:
-    ...     print "%s: %s" % (bug.id, bug.title)
+    ...     print("%s: %s" % (bug.id, bug.title))
     8: Printing doesn't work
 
 Let's reopen it and close it again, to ensure that the date closed isn't
@@ -53,7 +53,7 @@
     >>> fix_bug(4)
 
     >>> for bug in bugs_view.most_recently_fixed_bugs:
-    ...     print "%s: %s" % (bug.id, bug.title)
+    ...     print("%s: %s" % (bug.id, bug.title))
     4: Reflow problems with complex page layouts
     2: Blackhole Trash folder
     1: Firefox does not support SVG
@@ -66,7 +66,7 @@
     >>> fix_bug(1, bugtask_index=1)
 
     >>> for bug in bugs_view.most_recently_fixed_bugs:
-    ...     print "%s: %s" % (bug.id, bug.title)
+    ...     print("%s: %s" % (bug.id, bug.title))
     1: Firefox does not support SVG
     4: Reflow problems with complex page layouts
     2: Blackhole Trash folder
@@ -84,7 +84,7 @@
     >>> bugs_view = MaloneView(MaloneApplication(), LaunchpadTestRequest())
     >>> bugs_view.initialize()
     >>> for bug in bugs_view.most_recently_fixed_bugs:
-    ...     print "%s: %s" % (bug.id, bug.title)
+    ...     print("%s: %s" % (bug.id, bug.title))
     1: Firefox does not support SVG
     2: Blackhole Trash folder
     8: Printing doesn't work
@@ -102,7 +102,7 @@
     >>> bugs_view = MaloneView(MaloneApplication(), LaunchpadTestRequest())
     >>> bugs_view.initialize()
     >>> for bug in bugs_view.most_recently_fixed_bugs:
-    ...     print "%s: %s" % (bug.id, bug.title)
+    ...     print("%s: %s" % (bug.id, bug.title))
     1: Firefox does not support SVG
     4: Reflow problems with complex page layouts
     2: Blackhole Trash folder

=== modified file 'lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt'
--- lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt	2017-07-21 14:06:38 +0000
+++ lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt	2018-06-30 16:32:46 +0000
@@ -60,14 +60,14 @@
 The inline_filebug_form_url property returns the URL of the inline
 filebug form so that it may be loaded asynchronously.
 
-    >>> print filebug_view.inline_filebug_form_url
+    >>> print(filebug_view.inline_filebug_form_url)
     http://launchpad.dev/firefox/+filebug-inline-form
 
 Similarly, the duplicate_search_url property returns the base URL for
 the duplicate search view, which can be used to load the list of
 possible duplicates for a bug asynchronously.
 
-    >>> print filebug_view.duplicate_search_url
+    >>> print(filebug_view.duplicate_search_url)
     http://launchpad.dev/firefox/+filebug-show-similar
 
 
@@ -95,7 +95,7 @@
     u'Test description.'
 
     >>> for tag in filebug_view.added_bug.tags:
-    ...     print tag
+    ...     print(tag)
     bar
     foo
 
@@ -150,7 +150,7 @@
     True
 
     >>> for field in sorted(supervisor_fields - normal_fields):
-    ...     print field
+    ...     print(field)
     assignee
     importance
     information_type
@@ -169,20 +169,20 @@
     ...     title=u'Extra Fields Bug', comment=u'Test description.',
     ...     assignee=owner, importance=BugTaskImportance.HIGH,
     ...     milestone=milestone, status=BugTaskStatus.TRIAGED)
-    >>> print filebug_view.validate(bug_data)
+    >>> print(filebug_view.validate(bug_data))
     None
 
     >>> filebug_view.submit_bug_action.success(bug_data)
     >>> [added_bugtask] = filebug_view.added_bug.bugtasks
 
-    >>> print added_bugtask.status.title
+    >>> print(added_bugtask.status.title)
     Triaged
 
-    >>> print added_bugtask.importance.title
+    >>> print(added_bugtask.importance.title)
     High
 
-    >>> print added_bugtask.assignee.name
+    >>> print(added_bugtask.assignee.name)
     bug-superdude
 
-    >>> print added_bugtask.milestone.name
+    >>> print(added_bugtask.milestone.name)
     bug-superdude-milestone

=== modified file 'lib/lp/bugs/browser/tests/bugtask-adding-views.txt'
--- lib/lp/bugs/browser/tests/bugtask-adding-views.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/browser/tests/bugtask-adding-views.txt	2018-06-30 16:32:46 +0000
@@ -39,7 +39,7 @@
 Since we gave the view an upstream product as its context, it can't
 guess which product we want to add, so it will ask us to specify it.
 
-    >>> print add_task_view.widgets['product']._getFormInput()
+    >>> print(add_task_view.widgets['product']._getFormInput())
     None
     >>> add_task_view.step_name
     'choose_product'
@@ -140,7 +140,7 @@
     >>> add_task_view.field_names
     ['product', 'add_packaging', '__visited_steps__']
 
-    >>> print add_task_view.widgets['product']._getFormInput()
+    >>> print(add_task_view.widgets['product']._getFormInput())
     None
 
 Sometimes the distribution won't have any series, though. In that
@@ -159,7 +159,7 @@
     ...     method='GET')
     >>> add_task_view.step_name
     'choose_product'
-    >>> print add_task_view.widgets['product']._getFormInput()
+    >>> print(add_task_view.widgets['product']._getFormInput())
     None
 
     >>> len(add_task_view.request.response.notifications)
@@ -175,7 +175,7 @@
     >>> from lazr.lifecycle.interfaces import IObjectCreatedEvent
 
     >>> def on_created_event(object, event):
-    ...     print "ObjectCreatedEvent: %r" % object
+    ...     print("ObjectCreatedEvent: %r" % object)
 
     >>> on_created_listener = TestEventListener(
     ...     Interface, IObjectCreatedEvent, on_created_event)
@@ -199,7 +199,7 @@
     ...     firefox_task, '+choose-affected-product', form)
     >>> add_task_view.step_name
     'choose_product'
-    >>> print add_task_view.widgets['product']._getFormInput()
+    >>> print(add_task_view.widgets['product']._getFormInput())
     no-such-product
 
     >>> form['field.product'] = u'firefox'
@@ -207,7 +207,7 @@
     ...     firefox_task, '+choose-affected-product', form)
     >>> add_task_view.step_name
     'choose_product'
-    >>> print add_task_view.widgets['product']._getFormInput()
+    >>> print(add_task_view.widgets['product']._getFormInput())
     firefox
 
 If we specify a valid product, no errors will occur, and a bugtask will
@@ -219,7 +219,7 @@
     ObjectCreatedEvent: <BugTask ...>
 
     >>> for bugtask in bug_four.bugtasks:
-    ...     print bugtask.bugtargetdisplayname
+    ...     print(bugtask.bugtargetdisplayname)
     Evolution
     Mozilla Firefox
 
@@ -243,7 +243,7 @@
 
     >>> add_task_view.step_name
     'specify_remote_bug_url'
-    >>> print add_task_view.widgets['product']._getFormInput()
+    >>> print(add_task_view.widgets['product']._getFormInput())
     alsa-utils
 
 As you can see, we're still in the second step, because the user has
@@ -263,13 +263,13 @@
     >>> add_task_view = get_and_setup_view(
     ...     firefox_task, '+choose-affected-product', form)
     ObjectCreatedEvent: <BugTask ...>
-    >>> print add_task_view.notifications
+    >>> print(add_task_view.notifications)
     []
     >>> add_task_view.next_url is not None
     True
 
     >>> for bugtask in bug_four.bugtasks:
-    ...     print bugtask.bugtargetdisplayname
+    ...     print(bugtask.bugtargetdisplayname)
     alsa-utils
     Evolution
     Mozilla Firefox
@@ -295,14 +295,14 @@
     ...     firefox_task, '+choose-affected-product', form)
     >>> add_task_view.step_name
     'specify_remote_bug_url'
-    >>> print add_task_view.getFieldError('bug_url')
+    >>> print(add_task_view.getFieldError('bug_url'))
     Launchpad does not recognize the bug tracker at this URL.
 
 Note that this caused the transaction to be aborted, thus the
 alsa-utils bugtask added earlier is now gone:
 
     >>> for bugtask in bug_four.bugtasks:
-    ...     print bugtask.bugtargetdisplayname
+    ...     print(bugtask.bugtargetdisplayname)
     Evolution
     Mozilla Firefox
 
@@ -327,13 +327,13 @@
     ObjectCreatedEvent: <BugTask ...>
 
     >>> for bugtask in bug_four.bugtasks:
-    ...     print bugtask.bugtargetdisplayname
+    ...     print(bugtask.bugtargetdisplayname)
     APTonCD
     Evolution
     Mozilla Firefox
 
     >>> for bugwatch in bug_four.watches:
-    ...     print "%s: %s" % (bugwatch.bugtracker.title, bugwatch.remotebug)
+    ...     print("%s: %s" % (bugwatch.bugtracker.title, bugwatch.remotebug))
     bugzilla.somewhere.org/bugs/: 84
 
 If we specify a URL of an already registered bug tracker, both the task
@@ -349,18 +349,18 @@
     ObjectCreatedEvent: <BugWatch at ...>
     ObjectCreatedEvent: <BugTask ...>
 
-    >>> print add_task_view.notifications
+    >>> print(add_task_view.notifications)
     []
 
     >>> for bugtask in bug_four.bugtasks:
-    ...     print bugtask.bugtargetdisplayname
+    ...     print(bugtask.bugtargetdisplayname)
     alsa-utils
     APTonCD
     Evolution
     Mozilla Firefox
 
     >>> for bugwatch in bug_four.watches:
-    ...     print "%s: %s" % (bugwatch.bugtracker.title, bugwatch.remotebug)
+    ...     print("%s: %s" % (bugwatch.bugtracker.title, bugwatch.remotebug))
     GnomeGBug GTracker: 84
     bugzilla.somewhere.org/bugs/: 84
 
@@ -396,12 +396,12 @@
     'http://.../+bug/5'
 
     >>> for notification in add_task_view.request.response.notifications:
-    ...     print notification.message
+    ...     print(notification.message)
     <a href="...">Bug #4</a> also links to the added bug watch
     (gnome-bugzilla #84).
 
     >>> for bugwatch in bug_five.watches:
-    ...     print "%s: %s" % (bugwatch.bugtracker.title, bugwatch.remotebug)
+    ...     print("%s: %s" % (bugwatch.bugtracker.title, bugwatch.remotebug))
     GnomeGBug GTracker: 84
 
 There's a property for easily retrieving the target for use on the
@@ -444,7 +444,7 @@
     []
 
     >>> for bugtask in bug_four.bugtasks:
-    ...     print bugtask.bugtargetdisplayname
+    ...     print(bugtask.bugtargetdisplayname)
     alsa-utils
     ...
     mozilla-firefox (Ubuntu)
@@ -473,7 +473,7 @@
 We have no products using http://bugs.foo.org as its bug tracker, so we have
 nothing to present to the user.
 
-    >>> print add_task_view.existing_products
+    >>> print(add_task_view.existing_products)
     None
 
 Since the user is just creating the product in Launchpad to link to an
@@ -483,9 +483,9 @@
 
     >>> from lp.registry.interfaces.product import IProductSet
     >>> foo_product = getUtility(IProductSet).getByName('foo-product')
-    >>> print foo_product.owner.displayname
+    >>> print(foo_product.owner.displayname)
     Registry Administrators
-    >>> print foo_product.registrant.displayname
+    >>> print(foo_product.registrant.displayname)
     Sample Person
 
 The licence is set to DONT_KNOW for now.
@@ -599,11 +599,11 @@
 
     >>> def print_links(links_dict):
     ...     if links_dict is None:
-    ...         print None
+    ...         print(None)
     ...         return
     ...
     ...     for key in sorted(links_dict):
-    ...         print "%s: %s" % (key, links_dict[key])
+    ...         print("%s: %s" % (key, links_dict[key]))
 
 upstream_bugtracker_links is a dict of `bug_filing_url` and `bug_search_url`.
 The bug filing link includes the summary and description of the bug; the

=== modified file 'lib/lp/bugs/browser/tests/bugtask-edit-views.txt'
--- lib/lp/bugs/browser/tests/bugtask-edit-views.txt	2013-04-11 04:54:04 +0000
+++ lib/lp/bugs/browser/tests/bugtask-edit-views.txt	2018-06-30 16:32:46 +0000
@@ -59,7 +59,7 @@
 changed to the corresponding source package.
 
     >>> for notification in edit_view.request.response.notifications:
-    ...     print notification.message
+    ...     print(notification.message)
     &#x27;linux-2.6.12&#x27; is a binary package. This bug has been
     assigned to its source package &#x27;linux-source-2.6.15&#x27;
     instead.
@@ -83,7 +83,7 @@
     ...     (ubuntu_thunderbird_task, request), name='+editstatus')
     >>> edit_view.initialize()
     >>> for error in edit_view.errors:
-    ...     print error
+    ...     print(error)
     (u'ubuntu_thunderbird.target', u'Target',
      LaunchpadValidationError(u'There is no package named
      &#x27;no-such-package&#x27; published in Ubuntu.'))
@@ -137,7 +137,7 @@
     ...     (product_task, request), name='+editstatus')
     >>> edit_view.initialize()
     >>> for error in edit_view.errors:
-    ...     print error
+    ...     print(error)
     ('product', u'Project', RequiredMissing('product'))
 
 

=== modified file 'lib/lp/bugs/browser/tests/bugtask-search-views.txt'
--- lib/lp/bugs/browser/tests/bugtask-search-views.txt	2014-11-29 01:33:59 +0000
+++ lib/lp/bugs/browser/tests/bugtask-search-views.txt	2018-06-30 16:32:46 +0000
@@ -67,7 +67,7 @@
     >>> milestones = (
     ...     distro_advanced_search_listingview.getMilestoneWidgetValues())
     >>> for value in milestones:
-    ...     print value['title']
+    ...     print(value['title'])
     Debian 3.1 (2056-05-16)
     Debian 3.1-rc1 (2056-02-16)
 
@@ -81,7 +81,7 @@
     >>> milestones = (
     ...     package_advanced_search_listingview.getMilestoneWidgetValues())
     >>> for value in milestones:
-    ...     print value['title']
+    ...     print(value['title'])
     Debian 3.1 (2056-05-16)
     Debian 3.1-rc1 (2056-02-16)
 
@@ -302,7 +302,7 @@
 
     >>> open_bugtasks = list(mozilla_search_listingview.search().batch)
     >>> for bugtask in open_bugtasks:
-    ...     print bugtask.bug.id, bugtask.product.name, bugtask.status.name
+    ...     print(bugtask.bug.id, bugtask.product.name, bugtask.status.name)
     15 thunderbird NEW
     5 firefox NEW
     4 firefox NEW
@@ -328,7 +328,7 @@
     >>> mozilla_search_listingview = create_view(mozilla, '+bugs', form_values)
     >>> invalid_bugtasks = list(mozilla_search_listingview.search().batch)
     >>> for bugtask in invalid_bugtasks:
-    ...     print bugtask.bug.id, bugtask.product.name, bugtask.status.name
+    ...     print(bugtask.bug.id, bugtask.product.name, bugtask.status.name)
     15 thunderbird INVALID
 
     >>> open_bugtasks[0].transitionToStatus(
@@ -348,7 +348,7 @@
     >>> advanced_search_view = create_view(
     ...     mozilla, '+bugs', form_values)
     >>> for value in advanced_search_view.getMilestoneWidgetValues():
-    ...     print value['title']
+    ...     print(value['title'])
     Mozilla Firefox 1.0 (2056-10-16)
 
 
@@ -379,7 +379,8 @@
     >>> mozilla_search_listingview = create_view(mozilla, '+bugs', form_values)
     >>> userdata_bugtasks = list(mozilla_search_listingview.search().batch)
     >>> for bugtask in userdata_bugtasks:
-    ...     print bugtask.bug.id, bugtask.product.name, bugtask.bug.information_type.name
+    ...     print(bugtask.bug.id, bugtask.product.name,
+    ...           bugtask.bug.information_type.name)
     15 thunderbird USERDATA
 
     >>> open_bugtasks[0].bug.transitionToInformationType(
@@ -406,49 +407,49 @@
 return a plain search URL which, when visited, will display all open
 bugs.
 
-    >>> print get_buglisting_search_filter_url()
+    >>> print(get_buglisting_search_filter_url())
     +bugs?search=Search
 
 Passing an assignee will add an assignee field to the query string. Not
 that get_buglisting_search_filter_url() doesn't check any of the data
 that's passed to it; that's for the target search to do.
 
-    >>> print get_buglisting_search_filter_url(assignee='gmb')
+    >>> print(get_buglisting_search_filter_url(assignee='gmb'))
     +bugs?search=Search&field.assignee=gmb
 
 Passing an importance will add an importance field to the query string.
 
-    >>> print get_buglisting_search_filter_url(importance='UNDECIDED')
+    >>> print(get_buglisting_search_filter_url(importance='UNDECIDED'))
     +bugs?search=Search&field.importance=UNDECIDED
 
 Importance can be a single item or a list of items:
 
-    >>> print get_buglisting_search_filter_url(importance=['LOW', 'HIGH'])
+    >>> print(get_buglisting_search_filter_url(importance=['LOW', 'HIGH']))
     +bugs?search=Search&field.importance=LOW&field.importance=HIGH
 
 Passing a status will add a status field to the query string:
 
-    >>> print get_buglisting_search_filter_url(status='TRIAGED')
+    >>> print(get_buglisting_search_filter_url(status='TRIAGED'))
     +bugs?search=Search&field.status=TRIAGED
 
 Status, like importance, can be a list:
 
-    >>> print get_buglisting_search_filter_url(status=['NEW', 'INCOMPLETE'])
+    >>> print(get_buglisting_search_filter_url(status=['NEW', 'INCOMPLETE']))
     +bugs?search=Search&field.status=NEW&field.status=INCOMPLETE
 
 Passing a status_upstream parameter will add a status_upstream field to
 the query string.
 
-    >>> print get_buglisting_search_filter_url(
-    ...     status_upstream='open_upstream')
+    >>> print(get_buglisting_search_filter_url(
+    ...     status_upstream='open_upstream'))
     +bugs?search=Search&field.status_upstream=open_upstream
 
 The fields will always be rendered in the order assignee, importance,
 status, status_upstream, regardless of what order they're passed to
 get_buglisting_search_filter_url().
 
-    >>> print get_buglisting_search_filter_url(
+    >>> print(get_buglisting_search_filter_url(
     ...     status_upstream='open_upstream', status='NEW',
-    ...     importance='WISHLIST', assignee='mark')
+    ...     importance='WISHLIST', assignee='mark'))
     +bugs?search=Search&field.assignee=mark&field.importance=WISHLIST&field.status=NEW&field.status_upstream=open_upstream
 

=== modified file 'lib/lp/bugs/browser/tests/bugwatch-views.txt'
--- lib/lp/bugs/browser/tests/bugwatch-views.txt	2011-12-24 17:49:30 +0000
+++ lib/lp/bugs/browser/tests/bugwatch-views.txt	2018-06-30 16:32:46 +0000
@@ -57,7 +57,7 @@
 
     >>> new_watch_edit_view = create_initialized_view(
     ...     new_watch, '+edit')
-    >>> print new_watch_edit_view.watch_has_activity
+    >>> print(new_watch_edit_view.watch_has_activity)
     False
 
 Adding a successful activity entry for the watch will cause it to show
@@ -72,7 +72,7 @@
 
     >>> new_watch_edit_view = create_initialized_view(
     ...     new_watch, '+edit')
-    >>> print new_watch_edit_view.watch_has_activity
+    >>> print(new_watch_edit_view.watch_has_activity)
     True
 
 Each entry in the recent_watch_activity list is a dict containing data

=== modified file 'lib/lp/bugs/browser/tests/person-bug-views.txt'
--- lib/lp/bugs/browser/tests/person-bug-views.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/browser/tests/person-bug-views.txt	2018-06-30 16:32:46 +0000
@@ -99,9 +99,9 @@
 
     >>> from lp.bugs.interfaces.bugtask import BugTaskStatus
     >>> bug_task = reported_bugtasks[1]
-    >>> print bug_task.distribution.name
+    >>> print(bug_task.distribution.name)
     ubuntu
-    >>> print bug_task.sourcepackagename.name
+    >>> print(bug_task.sourcepackagename.name)
     thunderbird
     >>> bug_task.bug.id
     9
@@ -192,13 +192,13 @@
     >>> ubuntu_firefox_bugcounts['package_search_url']
     u'http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox?field.status=New&field.status=Incomplete&field.status=Confirmed&field.status=Triaged&field.status=In+Progress&field.status=Fix+Committed&search=Search'
 
-    >>> print ubuntu_firefox_bugcounts['open_bugs_count']
+    >>> print(ubuntu_firefox_bugcounts['open_bugs_count'])
     1
-    >>> print ubuntu_firefox_bugcounts['critical_bugs_count']
+    >>> print(ubuntu_firefox_bugcounts['critical_bugs_count'])
     0
-    >>> print ubuntu_firefox_bugcounts['unassigned_bugs_count']
+    >>> print(ubuntu_firefox_bugcounts['unassigned_bugs_count'])
     1
-    >>> print ubuntu_firefox_bugcounts['inprogress_bugs_count']
+    >>> print(ubuntu_firefox_bugcounts['inprogress_bugs_count'])
     0
 
     >>> ubuntu_firefox_bugcounts['open_bugs_url']
@@ -214,13 +214,13 @@
 bug counts, is also available.
 
     >>> total_counts = packagebugs_search_view.total_bug_counts
-    >>> print total_counts['open_bugs_count']
+    >>> print(total_counts['open_bugs_count'])
     1
-    >>> print total_counts['critical_bugs_count']
+    >>> print(total_counts['critical_bugs_count'])
     0
-    >>> print total_counts['unassigned_bugs_count']
+    >>> print(total_counts['unassigned_bugs_count'])
     1
-    >>> print total_counts['inprogress_bugs_count']
+    >>> print(total_counts['inprogress_bugs_count'])
     0
 
 Adding another bug will update the totals returned by
@@ -251,25 +251,25 @@
 We can see that the firefox bug counts have been altered:
 
     >>> firefox_bug_counts = packagebugs_search_view.package_bug_counts[0]
-    >>> print firefox_bug_counts['open_bugs_count']
+    >>> print(firefox_bug_counts['open_bugs_count'])
     2
-    >>> print firefox_bug_counts['critical_bugs_count']
+    >>> print(firefox_bug_counts['critical_bugs_count'])
     1
-    >>> print firefox_bug_counts['unassigned_bugs_count']
+    >>> print(firefox_bug_counts['unassigned_bugs_count'])
     2
-    >>> print firefox_bug_counts['inprogress_bugs_count']
+    >>> print(firefox_bug_counts['inprogress_bugs_count'])
     0
 
 And the total bug counts reflect this:
 
     >>> total_counts = packagebugs_search_view.total_bug_counts
-    >>> print total_counts['open_bugs_count']
+    >>> print(total_counts['open_bugs_count'])
     2
-    >>> print total_counts['critical_bugs_count']
+    >>> print(total_counts['critical_bugs_count'])
     1
-    >>> print total_counts['unassigned_bugs_count']
+    >>> print(total_counts['unassigned_bugs_count'])
     2
-    >>> print total_counts['inprogress_bugs_count']
+    >>> print(total_counts['inprogress_bugs_count'])
     0
 
 Adding a new bug to a package other than Ubuntu Firefox will naturally
@@ -292,37 +292,37 @@
 So the total counts will have changed:
 
     >>> total_counts = packagebugs_search_view.total_bug_counts
-    >>> print total_counts['open_bugs_count']
+    >>> print(total_counts['open_bugs_count'])
     3
-    >>> print total_counts['critical_bugs_count']
+    >>> print(total_counts['critical_bugs_count'])
     1
-    >>> print total_counts['unassigned_bugs_count']
+    >>> print(total_counts['unassigned_bugs_count'])
     3
-    >>> print total_counts['inprogress_bugs_count']
+    >>> print(total_counts['inprogress_bugs_count'])
     1
 
 Whilst the firefox ones remain static:
 
     >>> firefox_bug_counts = packagebugs_search_view.package_bug_counts[0]
-    >>> print firefox_bug_counts['open_bugs_count']
+    >>> print(firefox_bug_counts['open_bugs_count'])
     2
-    >>> print firefox_bug_counts['critical_bugs_count']
+    >>> print(firefox_bug_counts['critical_bugs_count'])
     1
-    >>> print firefox_bug_counts['unassigned_bugs_count']
+    >>> print(firefox_bug_counts['unassigned_bugs_count'])
     2
-    >>> print firefox_bug_counts['inprogress_bugs_count']
+    >>> print(firefox_bug_counts['inprogress_bugs_count'])
     0
 
 And the pmount counts make up the difference between the two:
 
     >>> pmount_bug_counts = packagebugs_search_view.package_bug_counts[1]
-    >>> print pmount_bug_counts['open_bugs_count']
+    >>> print(pmount_bug_counts['open_bugs_count'])
     1
-    >>> print pmount_bug_counts['critical_bugs_count']
+    >>> print(pmount_bug_counts['critical_bugs_count'])
     0
-    >>> print pmount_bug_counts['unassigned_bugs_count']
+    >>> print(pmount_bug_counts['unassigned_bugs_count'])
     1
-    >>> print pmount_bug_counts['inprogress_bugs_count']
+    >>> print(pmount_bug_counts['inprogress_bugs_count'])
     1
 
     >>> transaction.abort()
@@ -378,9 +378,9 @@
 A new user will have no related bugs, and therefore no related
 milestones.
 
-    >>> print pretty(list(related_bugs_view.searchUnbatched()))
+    >>> print(pretty(list(related_bugs_view.searchUnbatched())))
     []
-    >>> print pretty(related_bugs_view.getMilestoneWidgetValues())
+    >>> print(pretty(related_bugs_view.getMilestoneWidgetValues()))
     []
 
 Even if the user registers a product with a milestone, the list of
@@ -389,7 +389,7 @@
     >>> product = factory.makeProduct(owner=user, displayname='Coughing Bob')
     >>> milestone09 = factory.makeMilestone(product=product, name="0.9")
 
-    >>> print pretty(related_bugs_view.getMilestoneWidgetValues())
+    >>> print(pretty(related_bugs_view.getMilestoneWidgetValues()))
     []
 
 Even if the user files a bug against a product with a milestone, the
@@ -398,9 +398,9 @@
     >>> bug = factory.makeBug(target=product, owner=user)
     >>> transaction.commit()
 
-    >>> print pretty(list(related_bugs_view.searchUnbatched()))
+    >>> print(pretty(list(related_bugs_view.searchUnbatched())))
     [<BugTask ...>]
-    >>> print pretty(related_bugs_view.getMilestoneWidgetValues())
+    >>> print(pretty(related_bugs_view.getMilestoneWidgetValues()))
     []
 
 Only when a milestone is set for a related bug task does the advanced
@@ -409,7 +409,7 @@
     >>> bug.bugtasks[0].milestone = milestone09
     >>> transaction.commit()
 
-    >>> print pretty(related_bugs_view.getMilestoneWidgetValues())
+    >>> print(pretty(related_bugs_view.getMilestoneWidgetValues()))
     [{'checked': False,
       'title': u'Coughing Bob 0.9',
       'value': ...}]
@@ -426,7 +426,7 @@
 The earlier bug was reported by our user, so the assigned milestone
 will already appear.
 
-    >>> print pretty(reported_bugs_view.getMilestoneWidgetValues())
+    >>> print(pretty(reported_bugs_view.getMilestoneWidgetValues()))
     [{'checked': False,
       'title': u'Coughing Bob 0.9',
       'value': ...}]
@@ -438,7 +438,7 @@
     >>> bug = factory.makeBug(target=product, owner=user)
     >>> bug.bugtasks[0].milestone = milestone10
 
-    >>> print pretty(reported_bugs_view.getMilestoneWidgetValues())
+    >>> print(pretty(reported_bugs_view.getMilestoneWidgetValues()))
     [{'checked': False,
       'title': u'Coughing Bob 1.0',
       'value': ...},
@@ -456,14 +456,14 @@
 No bugs have been assigned to our user, so no relevant milestones are
 found.
 
-    >>> print pretty(assigned_bugs_view.getMilestoneWidgetValues())
+    >>> print(pretty(assigned_bugs_view.getMilestoneWidgetValues()))
     []
 
 Once a bug has been assigned, the milestone appears.
 
     >>> bug.bugtasks[0].transitionToAssignee(user)
 
-    >>> print pretty(assigned_bugs_view.getMilestoneWidgetValues())
+    >>> print(pretty(assigned_bugs_view.getMilestoneWidgetValues()))
     [{'checked': False,
       'title': u'Coughing Bob 1.0',
       'value': ...}]
@@ -478,7 +478,7 @@
 Our user has not commented on any bugs, so no relevant milestones are
 found.
 
-    >>> print pretty(commented_bugs_view.getMilestoneWidgetValues())
+    >>> print(pretty(commented_bugs_view.getMilestoneWidgetValues()))
     []
 
 Once the user has commented, the related milestone does appear.
@@ -486,7 +486,7 @@
     >>> bug.newMessage(user)
     <Message at ...>
 
-    >>> print pretty(commented_bugs_view.getMilestoneWidgetValues())
+    >>> print(pretty(commented_bugs_view.getMilestoneWidgetValues()))
     [{'checked': False,
       'title': u'Coughing Bob 1.0',
       'value': ...}]
@@ -502,7 +502,7 @@
 Our new_user is not subscribed to any bugs, so no relevant milestones
 are found.
 
-    >>> print pretty(subscribed_bugs_view.getMilestoneWidgetValues())
+    >>> print(pretty(subscribed_bugs_view.getMilestoneWidgetValues()))
     []
 
 Once new_user has subscribed, the related milestones appear.
@@ -510,7 +510,7 @@
     >>> bug.subscribe(new_user, new_user)
     <lp.bugs.model.bugsubscription.BugSubscription ...>
 
-    >>> print pretty(subscribed_bugs_view.getMilestoneWidgetValues())
+    >>> print(pretty(subscribed_bugs_view.getMilestoneWidgetValues()))
     [{'checked': False,
       'title': u'Coughing Bob 1.0',
       'value': ...}]

=== modified file 'lib/lp/bugs/browser/tests/test_views.py'
--- lib/lp/bugs/browser/tests/test_views.py	2011-12-28 17:03:06 +0000
+++ lib/lp/bugs/browser/tests/test_views.py	2018-06-30 16:32:46 +0000
@@ -44,8 +44,9 @@
         path = filename
         layer = special_test_layer.get(path, DatabaseFunctionalLayer)
         one_test = LayeredDocFileSuite(
-            path, setUp=setUp, tearDown=tearDown, layer=layer,
-            stdout_logging_level=logging.WARNING)
+            path,
+            setUp=lambda test: setUp(test, future=True), tearDown=tearDown,
+            layer=layer, stdout_logging_level=logging.WARNING)
         suite.addTest(one_test)
 
     return suite

=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-also-affects-new-upstream.txt'
--- lib/lp/bugs/stories/bug-also-affects/xx-also-affects-new-upstream.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bug-also-affects/xx-also-affects-new-upstream.txt	2018-06-30 16:32:46 +0000
@@ -30,7 +30,7 @@
 
     >>> user_browser.open(
     ...     'http://bugs.launchpad.dev/tomcat/+bug/2/+affects-new-product')
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Register project affected by...
     >>> user_browser.getControl('Bug URL').value = (
     ...     'http://bugs.foo.org/bugs/show_bug.cgi?id=421')
@@ -39,7 +39,7 @@
     >>> user_browser.getControl('Project summary').value = 'The Bar Project'
     >>> user_browser.getControl('Continue').click()
 
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Register project affected by...
 
     >>> print_feedback_messages(user_browser.contents)
@@ -73,7 +73,7 @@
 
     >>> user_browser.goBack()
     >>> user_browser.getControl('Use Existing Project').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Register project affected by...
     >>> print_feedback_messages(user_browser.contents)
     There is 1 error.
@@ -106,7 +106,7 @@
     >>> user_browser.getControl('Project ID').value = 'bazfoo'
     >>> user_browser.getControl('Project summary').value = 'The Foo Project'
     >>> user_browser.getControl('Continue').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Register project affected by...
     >>> print_feedback_messages(user_browser.contents)
     There is 1 error.

=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt'
--- lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt	2018-06-30 16:32:46 +0000
@@ -87,7 +87,7 @@
 
     >>> browser.open('http://localhost/firefox/+bug/1')
     >>> browser.getLink(url='+distrotask').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/firefox/+bug/1/+distrotask
 
     >>> browser.getControl(name='field.distribution').value = ['debian']
@@ -95,7 +95,7 @@
     >>> browser.getControl('URL').value = (
     ...     'http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=123')
     >>> browser.getControl('Continue').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/debian/+source/alsa-utils/+bug/1
 
 If we try to add an Ubuntu task together with a bug watch we get an
@@ -107,7 +107,7 @@
     >>> browser.getControl('URL').value = (
     ...     'https://bugzilla.mozilla.org/show_bug.cgi?id=84')
     >>> browser.getControl('Continue').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/debian/+source/alsa-utils/+bug/1/+distrotask
 
     >>> print_feedback_messages(browser.contents)
@@ -121,20 +121,20 @@
 
     >>> browser.getControl('URL').value = ''
     >>> browser.getControl('Continue').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/alsa-utils/+bug/1
 
 It's not possible to change a bugtask to a existing one.
 
     >>> browser.getLink(
     ...     url='ubuntu/+source/mozilla-firefox/+bug/1/+editstatus').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.../ubuntu/+source/mozilla-firefox/+bug/1/+editstatus
 
     >>> browser.getControl(name="ubuntu_mozilla-firefox.target.package"
     ...     ).value = 'alsa-utils'
     >>> browser.getControl('Save Changes').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.../ubuntu/+source/mozilla-firefox/+bug/1/+editstatus
 
     >>> print_feedback_messages(browser.contents)
@@ -144,7 +144,7 @@
     >>> browser.getControl(name="ubuntu_mozilla-firefox.target.package"
     ...     ).value = 'pmount'
     >>> browser.getControl('Save Changes').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/pmount/+bug/1
 
 We want to make people aware of that they should link bugtasks to bug
@@ -165,7 +165,7 @@
     >>> browser.getControl('Distribution').value = ['debian']
     >>> browser.getControl('Source Package Name').value = 'pmount'
     >>> browser.getControl('Continue').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/pmount/+bug/1/+distrotask
 
     >>> print_feedback_messages(browser.contents)
@@ -181,7 +181,7 @@
 notification will still be displayed.
 
     >>> browser.getControl('Continue').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/pmount/+bug/1/+distrotask
 
     >>> print_feedback_messages(browser.contents)
@@ -191,10 +191,10 @@
 redirected to the bug page.
 
     >>> browser.getControl('Add Anyway').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/debian/+source/pmount/+bug/1
 
-    >>> print browser.contents
+    >>> print(browser.contents)
     <...
     ...>pmount (Debian)</a>...
     ...
@@ -230,7 +230,7 @@
     >>> browser.getLink(url='+choose-affected-product').click()
     >>> browser.getControl(name='field.product').value = other_product_name
     >>> browser.getControl('Continue').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/proprietary-product/+bug/.../+choose-affected-product
 
     >>> print_feedback_messages(browser.contents)
@@ -257,7 +257,7 @@
     >>> user_browser.getControl('Project').value
     Traceback (most recent call last):
     ...
-    LookupError: label 'Project'
+    LookupError: label u'Project'
 
     >>> user_browser.getControl(name='field.product').value
     'evolution'
@@ -266,7 +266,7 @@
 product, though.
 
     >>> user_browser.getLink('Choose another project').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/evolution/+bug/6/+choose-affected-product?field.product=evolution
 
     >>> user_browser.getControl('Project').value
@@ -276,7 +276,7 @@
 to the bug page:
 
     >>> cancel_link = user_browser.getLink('Cancel')
-    >>> print cancel_link.url
+    >>> print(cancel_link.url)
     http://bugs.launchpad.dev/ubuntu/+source/evolution/+bug/6
 
 But we'll choose Thunderbird.
@@ -311,7 +311,7 @@
         tracker.
 
     >>> user_browser.getControl('Add to Bug Report').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/thunderbird/+bug/6
 
 Let's add the evolution task as well.
@@ -319,12 +319,12 @@
     >>> user_browser.open(
     ...     'http://launchpad.dev/ubuntu/+source/evolution/+bug/6')
     >>> user_browser.getLink(url='+choose-affected-product').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://.../ubuntu/+source/evolution/+bug/6/+choose-affected-product
 
     >>> user_browser.getControl('Add to Bug Report').click()
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/evolution/+bug/6
 
 
@@ -336,14 +336,14 @@
     >>> user_browser.open(
     ...     'http://launchpad.dev/debian/+source/mozilla-firefox/+bug/3')
     >>> user_browser.getLink(url='+choose-affected-product').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://.../debian/+source/mozilla-firefox/+bug/3/+choose-affected-product
 
     >>> user_browser.getControl('Project').value
     ''
 
     >>> user_browser.getControl('Continue').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://.../debian/+source/mozilla-firefox/+bug/3/+choose-affected-product
 
 We get a nice error message.
@@ -357,7 +357,7 @@
 
     >>> user_browser.getControl('Project').value = 'no-such-product'
     >>> user_browser.getControl('Continue').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://.../debian/+source/mozilla-firefox/+bug/3/+choose-affected-product
 
     >>> print_feedback_messages(user_browser.contents)
@@ -365,7 +365,7 @@
     There is no project in Launchpad named "no-such-product"...
 
     >>> search_link = user_browser.getLink('search for it')
-    >>> print search_link.url
+    >>> print(search_link.url)
     http://bugs.launchpad.dev/projects
 
 Since we don't restrict the input, the user can write anything, so we
@@ -375,9 +375,9 @@
     ...     'http://launchpad.dev/debian/+source/mozilla-firefox/+bug/3'
     ...     '/+choose-affected-product')
 
-    >>> user_browser.getControl('Project').value = 'N\xc3\xb6 Such Product&<>'
+    >>> user_browser.getControl('Project').value = b'N\xc3\xb6 Such Product&<>'
     >>> user_browser.getControl('Continue').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://.../debian/+source/mozilla-firefox/+bug/3/+choose-affected-product
 
     >>> print_feedback_messages(user_browser.contents)
@@ -406,7 +406,7 @@
 Launchpad redirects to the newly created bugtask page, with a row for
 the new bug watch.
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/alsa-utils/+bug/3
 
     >>> affects_table = find_tags_by_class(
@@ -427,12 +427,12 @@
 And now we try to add the same upstream again.
 
     >>> user_browser.getLink(url='+choose-affected-product').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/alsa-utils/+bug/3/+choose-affected-product
 
     >>> user_browser.getControl('Project').value = 'alsa-utils'
     >>> user_browser.getControl('Continue').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/alsa-utils/+bug/3/+choose-affected-product
 
 We get a nice error message.
@@ -446,20 +446,20 @@
     >>> user_browser.getControl('Project').value = 'evolution'
     >>> user_browser.getControl('Continue').click()
     >>> user_browser.getControl('Add to Bug Report').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/evolution/+bug/3
 
 But if we try to change it to the target of an existing upstream
 bugtask, our validator springs into action.
 
     >>> user_browser.getLink(url='evolution/+bug/3/+editstatus').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/evolution/+bug/3/+editstatus
 
     >>> user_browser.getControl(name='evolution.target.product').value = (
     ...     'alsa-utils')
     >>> user_browser.getControl('Save Changes').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/evolution/+bug/3/+editstatus
 
     >>> print_feedback_messages(user_browser.contents)
@@ -482,7 +482,7 @@
     ...     '+choose-affected-product')
     >>> user_browser.getControl('Project').value = 'gnome-terminal'
     >>> user_browser.getControl('Continue').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/4/+choose-affected-product
 
     >>> user_browser.getControl('I have the URL').selected = True
@@ -493,19 +493,19 @@
 that points back to the bug page:
 
     >>> cancel_link = user_browser.getLink('Cancel')
-    >>> print cancel_link.url
+    >>> print(cancel_link.url)
     http://bugs.launchpad.dev/firefox/+bug/4
 
 But we're happy, so we add the bug watch.
 
     >>> user_browser.getControl('Add to Bug Report').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/gnome-terminal/+bug/4
 
     >>> bug_watches = find_portlet(
     ...     user_browser.contents, 'Remote bug watches')
     >>> for li in bug_watches('li'):
-    ...     print li.findAll('a')[0].renderContents()
+    ...     print(li.findAll('a')[0].renderContents())
     gnome-bugzilla #42
 
 It's possible to supply an HTTPS URL, even though the bug tracker's base
@@ -516,14 +516,14 @@
     ...     '+choose-affected-product')
     >>> user_browser.getControl('Project').value = 'netapplet'
     >>> user_browser.getControl('Continue').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/4/+choose-affected-product
 
     >>> user_browser.getControl('I have the URL').selected = True
     >>> user_browser.getControl(name='field.bug_url').value = (
     ...     u'https://bugzilla.gnome.org/bugs/show_bug.cgi?id=84')
     >>> user_browser.getControl('Add to Bug Report').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/netapplet/+bug/4
 
 The URL was automatically converted to HTTP:
@@ -531,7 +531,7 @@
     >>> bug_watches = find_portlet(
     ...     user_browser.contents, 'Remote bug watches')
     >>> for li in bug_watches('li'):
-    ...     print li.findAll('a')[0]['href']
+    ...     print(li.findAll('a')[0]['href'])
     http://bugzilla.gnome.org/bugs/show_bug.cgi?id=42
     http://bugzilla.gnome.org/bugs/show_bug.cgi?id=84
 
@@ -543,18 +543,18 @@
     ...     '+choose-affected-product')
     >>> user_browser.getControl('Project').value = 'alsa-utils'
     >>> user_browser.getControl('Continue').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/4/+choose-affected-product
 
     >>> user_browser.getControl('I have the URL').selected = True
     >>> user_browser.getControl(name='field.bug_url').value = (
     ...     u'http://bugs.unknown/42')
     >>> user_browser.getControl('Add to Bug Report').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/4/+choose-affected-product
 
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     There is 1 error.
     Launchpad does not recognize the bug tracker at this URL.
 
@@ -565,7 +565,7 @@
     >>> user_browser.getControl(name='field.bug_url').value = (
     ...     u"http://new.trac/ticket/42";)
     >>> user_browser.getControl('Add to Bug Report').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/4/+choose-affected-product
 
     >>> print_feedback_messages(user_browser.contents)
@@ -576,7 +576,7 @@
 As before, if we change our mind, we can back out if we want.
 
     >>> cancel_link = user_browser.getLink('Cancel')
-    >>> print cancel_link.url
+    >>> print(cancel_link.url)
     http://bugs.launchpad.dev/firefox/+bug/4
 
 Now the user confirms they want us to register the bug tracker for them
@@ -586,7 +586,7 @@
 
 The bug watch is linked, and we're redirected to the bug's page.
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/alsa-utils/+bug/4
 
 The bug tracker and bug watch were added. We can see that the bugtracker
@@ -596,7 +596,7 @@
     >>> bug_watches = find_portlet(
     ...     user_browser.contents, 'Remote bug watches')
     >>> for li in bug_watches('li'):
-    ...     print li.findAll('a')[0].renderContents()
+    ...     print(li.findAll('a')[0].renderContents())
     gnome-bugzilla #42
     gnome-bugzilla #84
     auto-new.trac #42
@@ -609,20 +609,20 @@
     ...     '+choose-affected-product')
     >>> user_browser.getControl('Project').value = 'thunderbird'
     >>> user_browser.getControl('Continue').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/4/+choose-affected-product
 
     >>> user_browser.getControl('I have the URL').selected = True
     >>> user_browser.getControl(name='field.bug_url').value = (
     ...     u'bugzilla.gnome.org/bugs/show_bug.cgi?id=168')
     >>> user_browser.getControl('Add to Bug Report').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/thunderbird/+bug/4
 
     >>> bug_watches = find_portlet(
     ...     user_browser.contents, 'Remote bug watches')
     >>> for li in bug_watches('li'):
-    ...     print li.findAll('a')[0]['href']
+    ...     print(li.findAll('a')[0]['href'])
     http://bugzilla.gnome.org/bugs/show_bug.cgi?id=168
     http://bugzilla.gnome.org/bugs/show_bug.cgi?id=42
     http://bugzilla.gnome.org/bugs/show_bug.cgi?id=84
@@ -659,19 +659,19 @@
 confirmation.
 
     >>> user_browser.getControl('Add to Bug Report').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/gnome-terminal/+bug/12
 
     >>> def print_remote_bug_watches_portlet(browser):
     ...     bug_watches = find_portlet(
     ...         browser.contents, 'Remote bug watches')
     ...     for li in bug_watches('li'):
-    ...         print ' '.join(extract_text(li).splitlines())
+    ...         print(' '.join(extract_text(li).splitlines()))
     ...         bug_watch_link = li.find('a', {'class': 'link-external'})
     ...         if bug_watch_link is None:
-    ...             print '  --> None'
+    ...             print('  --> None')
     ...         else:
-    ...             print '  --> %s' % bug_watch_link.get('href')
+    ...             print('  --> %s' % bug_watch_link.get('href'))
 
     >>> import re
     >>> def print_assigned_bugtasks(browser):
@@ -687,10 +687,10 @@
     ...         if assignee and not 'Unassigned' in assignee:
     ...             assignee_link = cells[-2].a
     ...             if assignee_link is None:
-    ...                 print '%s -->\n  %s' % (affects, assignee)
+    ...                 print('%s -->\n  %s' % (affects, assignee))
     ...             else:
-    ...                 print '%s -->\n  %s\n  %s' % (
-    ...                     affects, assignee, assignee_link['href'])
+    ...                 print('%s -->\n  %s\n  %s' % (
+    ...                     affects, assignee, assignee_link['href']))
 
     >>> print_remote_bug_watches_portlet(user_browser)
     auto-dark-master-o-bugs...
@@ -702,7 +702,7 @@
       mailto:dark-master-o-bugs@xxxxxxxxxxxxxxxx
 
     >>> user_browser.contents.count(
-    ...     'mailto:dark-master-o-bugs@xxxxxxxxxxxxxxxx')
+    ...     b'mailto:dark-master-o-bugs@xxxxxxxxxxxxxxxx')
     3
 
 To evade harvesting, the email address above is obfuscated if you're not
@@ -718,5 +718,5 @@
       auto-dark-master-o-bugs
 
     >>> anon_browser.contents.count(
-    ...     'mailto:dark-master-o-bugs@xxxxxxxxxxxxxxxx')
+    ...     b'mailto:dark-master-o-bugs@xxxxxxxxxxxxxxxx')
     0

=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-bugtracker-information.txt'
--- lib/lp/bugs/stories/bug-also-affects/xx-bugtracker-information.txt	2012-06-12 16:09:33 +0000
+++ lib/lp/bugs/stories/bug-also-affects/xx-bugtracker-information.txt	2018-06-30 16:32:46 +0000
@@ -8,7 +8,7 @@
     ...    'http://launchpad.dev/firefox/+bug/1/+choose-affected-product')
     >>> user_browser.getControl('Project').value = 'gnome-terminal'
     >>> user_browser.getControl('Continue').click()
-    >>> print user_browser.contents.decode('ascii', 'ignore')
+    >>> print(user_browser.contents.decode('ascii', 'ignore'))
     <...GNOME Terminal uses
     <a href="http://bugzilla.gnome.org/bugs";>GnomeGBug GTracker</a>
     to track its bugs...
@@ -30,7 +30,7 @@
     ...    'http://launchpad.dev/firefox/+bug/1/+choose-affected-product')
     >>> user_browser.getControl('Project').value = 'thunderbird'
     >>> user_browser.getControl('Continue').click()
-    >>> print user_browser.contents.decode('ascii', 'ignore')
+    >>> print(user_browser.contents.decode('ascii', 'ignore'))
     <...Mozilla Thunderbird doesn't use Launchpad to track its bugs...
 
     >>> print_upstream_linking_form(user_browser)

=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-duplicate-bugwatches.txt'
--- lib/lp/bugs/stories/bug-also-affects/xx-duplicate-bugwatches.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/bugs/stories/bug-also-affects/xx-duplicate-bugwatches.txt	2018-06-30 16:32:46 +0000
@@ -21,7 +21,7 @@
     >>> bugwatch_portlet = find_portlet(
     ...     user_browser.contents, 'Remote bug watches')
     >>> for li_tag in bugwatch_portlet.findAll('li'):
-    ...     print li_tag.findAll('a')[0].renderContents()
+    ...     print(li_tag.findAll('a')[0].renderContents())
     debbugs #42
 
 If we add another bug watch, pointing to the same URL, the previous one
@@ -42,12 +42,12 @@
     >>> user_browser.getControl('URL').value = debian_bug
     >>> user_browser.getControl('Continue').click()
     >>> for tag in find_tags_by_class(user_browser.contents, 'message'):
-    ...   print tag
+    ...   print(tag)
 
     >>> bugwatch_portlet = find_portlet(
     ...     user_browser.contents, 'Remote bug watches')
     >>> for li_tag in bugwatch_portlet.findAll('li'):
-    ...     print li_tag.findAll('a')[0].string
+    ...     print(li_tag.findAll('a')[0].string)
     debbugs #42
 
 Both the thunderbird and gnome-terminal Debian tasks are pointing to the

=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-upstream-bugtracker-links.txt'
--- lib/lp/bugs/stories/bug-also-affects/xx-upstream-bugtracker-links.txt	2010-06-18 00:46:17 +0000
+++ lib/lp/bugs/stories/bug-also-affects/xx-upstream-bugtracker-links.txt	2018-06-30 16:32:46 +0000
@@ -18,7 +18,7 @@
 general.
 
     >>> text = find_tag_by_id(user_browser.contents, 'upstream-text')
-    >>> print extract_text(text)
+    >>> print(extract_text(text))
     Mozilla Thunderbird
     doesn't use Launchpad to track its bugs. If you know this bug
     has been reported in another bug tracker, you can link to it;
@@ -42,7 +42,7 @@
     >>> user_browser.getControl('Continue').click()
 
     >>> text = find_tag_by_id(user_browser.contents, 'upstream-text')
-    >>> print extract_text(text)
+    >>> print(extract_text(text))
     Mozilla Thunderbird
     uses The Mozilla.org Bug Tracker to
     track its bugs. If you know this bug has been reported there,
@@ -60,7 +60,7 @@
     >>> user_browser.getControl('Continue').click()
 
     >>> text = find_tag_by_id(user_browser.contents, 'upstream-text')
-    >>> print extract_text(text)
+    >>> print(extract_text(text))
     GNOME Terminal uses GnomeGBug GTracker to track its bugs.
     If you know this bug has been reported there, you can link to it;
     Launchpad will keep track of its status for you.
@@ -80,7 +80,7 @@
     >>> scheme, netloc, path, params, query, fragment = urlparse(url)
     >>> [long_desc] = parse_qs(query)['long_desc']
 
-    >>> print long_desc
+    >>> print(long_desc)
     Originally reported at:
       http://bugs.launchpad.dev/bugs/13
     <BLANKLINE>
@@ -104,7 +104,7 @@
     >>> user_browser.getControl('Continue').click()
 
     >>> text = find_tag_by_id(user_browser.contents, 'upstream-text')
-    >>> print extract_text(text)
+    >>> print(extract_text(text))
     GNOME Terminal uses Debian Bug tracker to track its bugs.
     If you know this bug has been reported there, you can link to it;
     Launchpad will keep track of its status for you.
@@ -127,5 +127,5 @@
 
     >>> admin_browser.open(
     ...     'http://launchpad.dev/thunderbird/+configure-bugtracker')
-    >>> print admin_browser.getControl(name='field.remote_product').value
+    >>> print(admin_browser.getControl(name='field.remote_product').value)
     Thunderbird

=== modified file 'lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt'
--- lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt	2018-06-30 16:32:46 +0000
@@ -10,7 +10,7 @@
 
     >>> browser.getControl("Private", index=1).selected = True
     >>> browser.getControl("Change").click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/2
 
 When we go back to the secrecy form, the previously set value is pre-selected.
@@ -40,17 +40,17 @@
     >>> browser.getControl("Submit Bug Report").click()
 
     >>> bug_id = browser.url.split("/")[-1]
-    >>> print browser.url.replace(bug_id, "BUG-ID")
+    >>> print(browser.url.replace(bug_id, "BUG-ID"))
     http://bugs.launchpad.dev/ubuntu/+source/evolution/+bug/BUG-ID
 
-    >>> print browser.contents
+    >>> print(browser.contents)
     <!DOCTYPE...
     ...Security-related bugs are by default private...
 
 Foo Bar sees the private bug they filed.
 
     >>> browser.open("http://launchpad.dev/ubuntu/+bugs";)
-    >>> print browser.contents.replace(bug_id, "BUG-ID")
+    >>> print(browser.contents.replace(bug_id, "BUG-ID"))
     <!DOCTYPE...
     ...
     ...Ubuntu...

=== modified file 'lib/lp/bugs/stories/bug-privacy/xx-presenting-private-bugs.txt'
--- lib/lp/bugs/stories/bug-privacy/xx-presenting-private-bugs.txt	2012-07-17 14:29:17 +0000
+++ lib/lp/bugs/stories/bug-privacy/xx-presenting-private-bugs.txt	2018-06-30 16:32:46 +0000
@@ -5,7 +5,7 @@
 
     >>> browser = setupBrowser(auth="Basic foo.bar@xxxxxxxxxxxxx:test")
     >>> browser.open("http://launchpad.dev/bugs/4";)
-    >>> print extract_text(find_tag_by_id(browser.contents, 'privacy'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'privacy')))
     This report contains Public information...
 
 But when marked private, it gains the standard Launchpad presentation
@@ -14,16 +14,16 @@
     >>> browser.open("http://bugs.launchpad.dev/firefox/+bug/4/+secrecy";)
     >>> browser.getControl("Private", index=1).selected = True
     >>> browser.getControl("Change").click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/firefox/+bug/4
-    >>> print extract_text(find_tag_by_id(browser.contents, 'privacy'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'privacy')))
     This report contains Private information...
 
 Bugs created before we started recording the date and time and who
 marked the bug private show only a simple message:
 
     >>> browser.open("http://launchpad.dev/bugs/14";)
-    >>> print extract_text(find_tag_by_id(browser.contents, 'privacy'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'privacy')))
     This report contains Private Security information...
 
 But newer bugs that are filed private at creation time (like security
@@ -39,9 +39,9 @@
     >>> browser.getControl("Private Security").selected = True
     >>> browser.getControl("Submit Bug Report").click()
 
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/firefox/+bug/...
-    >>> print extract_text(find_tag_by_id(browser.contents, 'privacy'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'privacy')))
     This report contains Private Security information...
 
 XXX 20080708 mpt: Bug 246671 again.

=== modified file 'lib/lp/bugs/stories/bug-release-management/xx-bug-release-management.txt'
--- lib/lp/bugs/stories/bug-release-management/xx-bug-release-management.txt	2012-10-04 23:49:42 +0000
+++ lib/lp/bugs/stories/bug-release-management/xx-bug-release-management.txt	2018-06-30 16:32:46 +0000
@@ -29,7 +29,7 @@
 
     >>> feedback_msg = find_tags_by_class(
     ...     admin_browser.contents, "informational message")[0]
-    >>> print feedback_msg.renderContents()
+    >>> print(feedback_msg.renderContents())
     Approved nomination for Mozilla Firefox 1.0
 
 After a productseries task has been created, it's editable.
@@ -58,7 +58,7 @@
 
     >>> feedback_msg = find_tags_by_class(
     ...     admin_browser.contents, "informational message")[0]
-    >>> print feedback_msg.renderContents()
+    >>> print(feedback_msg.renderContents())
     Declined nomination for Ubuntu Hoary
 
 Nominate a bug to a distribution release
@@ -102,7 +102,7 @@
     >>> nominater_browser.getControl("Aqua").selected = True
     >>> nominater_browser.getControl("Nominate").click()
     >>> for tag in find_tags_by_class(nominater_browser.contents, 'message'):
-    ...     print tag
+    ...     print(tag)
     <div...Added nominations for: Poseidon Aqua...
 
 Now, if the nominater, having the form open in another browser window,
@@ -114,7 +114,7 @@
 
     >>> for tag in find_tags_by_class(nominater_other_browser.contents,
     ...     'message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     There is 1 error.
     This bug has already been nominated for these series: Aqua
 
@@ -129,7 +129,7 @@
     >>> admin_browser.getControl("Target").click()
 
     >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print tag
+    ...     print(tag)
     <div...Targeted bug to: Poseidon Hydro...
 
 Nominating a bug for a product series
@@ -168,7 +168,7 @@
     >>> nominater_browser.getControl("Nominate").click()
 
     >>> for tag in find_tags_by_class(nominater_browser.contents, 'message'):
-    ...     print tag
+    ...     print(tag)
     <div...Added nominations for: Widget beta...
 
 Now, if the nominater, having the form open in another browser window,
@@ -178,7 +178,7 @@
 
     >>> for tag in find_tags_by_class(nominater_other_browser.contents,
     ...     'message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     There is 1 error.
     This bug has already been nominated for these series: Beta
 
@@ -192,7 +192,7 @@
     >>> admin_browser.getControl("Target").click()
 
     >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print tag
+    ...     print(tag)
     <div...Targeted bug to: Widget trunk...
 
 When a bug is targeted to the current development release, the general
@@ -220,8 +220,8 @@
     >>> admin_browser.getControl('Bug Supervisor').value = 'no-priv'
     >>> admin_browser.getControl('Change').click()
 
-    >>> print extract_text(
-    ...     find_tag_by_id(admin_browser.contents, 'bug-supervisor'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'bug-supervisor')))
     Bug supervisor:
     No Privileges Person
 
@@ -307,8 +307,8 @@
     >>> admin_browser.getControl('Bug Supervisor').value = 'no-priv'
     >>> admin_browser.getControl('Change').click()
 
-    >>> print extract_text(find_tag_by_id(admin_browser.contents,
-    ...     'bug-supervisor'))
+    >>> print(extract_text(find_tag_by_id(admin_browser.contents,
+    ...     'bug-supervisor')))
     Bug supervisor:
     No Privileges Person
 

=== modified file 'lib/lp/bugs/stories/bug-tags/xx-official-bug-tags.txt'
--- lib/lp/bugs/stories/bug-tags/xx-official-bug-tags.txt	2012-08-08 07:22:51 +0000
+++ lib/lp/bugs/stories/bug-tags/xx-official-bug-tags.txt	2018-06-30 16:32:46 +0000
@@ -7,7 +7,7 @@
     >>> admin_browser.open(
     ...     'http://bugs.launchpad.dev/firefox')
     >>> admin_browser.getLink('Edit official tags').click()
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://bugs.launchpad.dev/firefox/+manage-official-tags
 
 Tags are entered into a textarea as a list of white-spaces separated
@@ -15,11 +15,11 @@
 
     >>> admin_browser.getControl('Official Bug Tags').value = 'foo bar'
     >>> admin_browser.getControl('Save').click()
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://bugs.launchpad.dev/firefox
     >>> admin_browser.open(
     ...     'http://bugs.launchpad.dev/firefox/+manage-official-tags')
-    >>> print admin_browser.getControl('Official Bug Tags').value
+    >>> print(admin_browser.getControl('Official Bug Tags').value)
     bar foo
 
 The link as well as the edit form is only available for products and
@@ -27,7 +27,7 @@
 
     >>> admin_browser.open(
     ...     'http://bugs.launchpad.dev/firefox/1.0')
-    >>> print admin_browser.getLink('Edit official tags')
+    >>> print(admin_browser.getLink('Edit official tags'))
     Traceback (most recent call last):
     ...
     LinkNotFoundError
@@ -42,7 +42,7 @@
 administrators but not for ordinary users.
 
     >>> browser.open('http://bugs.launchpad.dev/firefox')
-    >>> print browser.getLink('Edit official tags')
+    >>> print(browser.getLink('Edit official tags'))
     Traceback (most recent call last):
     ...
     LinkNotFoundError
@@ -68,18 +68,18 @@
     >>> bug_super_browser.open(
     ...     'http://bugs.launchpad.dev/youbuntu')
     >>> bug_super_browser.getLink('Edit official tags').click()
-    >>> print bug_super_browser.url
+    >>> print(bug_super_browser.url)
     http://bugs.launchpad.dev/youbuntu/+manage-official-tags
 
 The bug supervisor can also set the tags for the product.
 
     >>> bug_super_browser.getControl('Official Bug Tags').value = 'foo bar'
     >>> bug_super_browser.getControl('Save').click()
-    >>> print bug_super_browser.url
+    >>> print(bug_super_browser.url)
     http://bugs.launchpad.dev/youbuntu
     >>> bug_super_browser.open(
     ...     'http://bugs.launchpad.dev/youbuntu/+manage-official-tags')
-    >>> print bug_super_browser.getControl('Official Bug Tags').value
+    >>> print(bug_super_browser.getControl('Official Bug Tags').value)
     bar foo
 
 Official Tags on Bug Pages
@@ -133,6 +133,6 @@
 
     >>> browser.open(bug_url)
     >>> js = find_tag_by_id(browser.contents, 'available-official-tags-js')
-    >>> print js
+    >>> print(js)
     <script...>var available_official_tags =
     ["eenie", "meenie", "miney", "moe"];</script>

=== modified file 'lib/lp/bugs/stories/bug-tags/xx-searching-for-tags.txt'
--- lib/lp/bugs/stories/bug-tags/xx-searching-for-tags.txt	2012-12-10 13:43:47 +0000
+++ lib/lp/bugs/stories/bug-tags/xx-searching-for-tags.txt	2018-06-30 16:32:46 +0000
@@ -32,7 +32,7 @@
     >>> anon_browser.getControl('Search', index=0).click()
 
     >>> for tag in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     &#x27;!!invalid!!&#x27; isn&#x27;t a valid tag name. Tags must start
     with a letter or number and be lowercase. The characters
     &quot;+&quot;, &quot;-&quot; and &quot;.&quot; are also allowed
@@ -58,7 +58,7 @@
 
 Indeed, the markup is valid and correctly escaped:
 
-    >>> print find_tag_by_id(anon_browser.contents, 'field.tag').prettify()
+    >>> print(find_tag_by_id(anon_browser.contents, 'field.tag').prettify())
     <input class="textType" id="field.tag"
            name="field.tag" size="20" type="text"
            value='&lt;script&gt;alert("cheezburger");&lt;/script&gt;' />
@@ -66,7 +66,7 @@
 The error message is also valid and correctly escaped:
 
     >>> for tag in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print tag.prettify()
+    ...     print(tag.prettify())
     <div class="message">
     &#x27;&lt;script&gt;alert(&quot;cheezburger&quot;);&lt;/script&gt;&#x27; isn&#x27;t ...
     </div>

=== modified file 'lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-listings-page.txt'
--- lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-listings-page.txt	2012-10-02 06:36:44 +0000
+++ lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-listings-page.txt	2018-06-30 16:32:46 +0000
@@ -4,11 +4,11 @@
 
     >>> anon_browser.open('http://launchpad.dev/ubuntu/+bugs')
     >>> anon_browser.getLink(id='tags-content-link').click()
-    >>> print anon_browser.url
+    >>> print(anon_browser.url)
     http://launchpad.dev/ubuntu/+bugtarget-portlet-tags-content
     >>> tags_portlet = find_tags_by_class(anon_browser.contents, 'data-list')[0]
     >>> for a_tag in tags_portlet('a'):
-    ...     print a_tag.renderContents()
+    ...     print(a_tag.renderContents())
     crash
     pebcak
     dataloss
@@ -48,5 +48,5 @@
 
     >>> anon_browser.open(
     ...     'http://launchpad.dev/tomcat/+bugtarget-portlet-tags-content')
-    >>> print extract_text(anon_browser.contents)
+    >>> print(extract_text(anon_browser.contents))
     Tags

=== modified file 'lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-page.txt'
--- lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-page.txt	2012-12-10 13:43:47 +0000
+++ lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-page.txt	2018-06-30 16:32:46 +0000
@@ -3,7 +3,7 @@
     >>> user_browser.open('http://bugs.launchpad.dev/firefox/+bug/1')
     >>> user_browser.url
     'http://bugs.launchpad.dev/firefox/+bug/1'
-    >>> print extract_text(find_tag_by_id(user_browser.contents, 'bug-tags'))
+    >>> print(extract_text(find_tag_by_id(user_browser.contents, 'bug-tags')))
     Add tags  Tag help
 
 Let's specify some tags:
@@ -19,7 +19,7 @@
     >>> user_browser.getControl('Change').click()
 
     >>> for tag in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     There is 1 error.
     &#x27;!!invalid!!&#x27; isn&#x27;t a valid tag name. Tags must start
     with a letter or number and be lowercase. The characters
@@ -35,11 +35,11 @@
 
 Now the tags will be displayed on the bug page:
 
-    >>> 'Tags:' in user_browser.contents
-    True
-    >>> 'foo' in user_browser.contents
-    True
-    >>> 'bar' in user_browser.contents
+    >>> b'Tags:' in user_browser.contents
+    True
+    >>> b'foo' in user_browser.contents
+    True
+    >>> b'bar' in user_browser.contents
     True
 
 Simply changing the ordering of the bug tags won't cause anything to

=== modified file 'lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.txt'
--- lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.txt	2015-10-06 06:48:01 +0000
+++ lib/lp/bugs/stories/bugattachments/xx-attachments-to-bug-report.txt	2018-06-30 16:32:46 +0000
@@ -11,7 +11,7 @@
 
     >>> browser.open("http://bugs.launchpad.dev/redfish/+bug/11";)
     >>> comment_1 = find_tags_by_class(browser.contents, 'boardComment')[0]
-    >>> print extract_text(comment_1)
+    >>> print(extract_text(comment_1))
     Valentina Commissari (tsukimi)
     wrote
     on 2007-03-15:
@@ -42,7 +42,7 @@
 
     >>> browser.open("http://bugs.launchpad.dev/redfish/+bug/11";)
     >>> comment_0 = find_tags_by_class(browser.contents, 'boardComment')[0]
-    >>> print extract_text(comment_0)
+    >>> print(extract_text(comment_0))
     Daniel Silverstone (kinnison)
     wrote
     on 2007-03-15:
@@ -51,9 +51,9 @@
     (8 bytes,
     text/plain)
     >>> link = browser.getLink('sample data')
-    >>> print link.url
+    >>> print(link.url)
     http://bugs.launchpad.dev/jokosher/+bug/11/+attachment/.../+files/test.txt
-    >>> print comment_0.find('a', text='Edit').parent
+    >>> print(comment_0.find('a', text='Edit').parent)
     <a class="sprite edit action-icon"
        href="/jokosher/+bug/11/+attachment/...">Edit</a>
 
@@ -70,7 +70,7 @@
     ...     description='a patch')
     >>> logout()
     >>> browser.open("http://bugs.launchpad.dev/redfish/+bug/11";)
-    >>> print extract_text(browser.contents)
+    >>> print(extract_text(browser.contents))
     Bug #11 ...
     ...Patches...
     ...a patch...

=== modified file 'lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt'
--- lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt	2015-10-06 06:48:01 +0000
+++ lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt	2018-06-30 16:32:46 +0000
@@ -36,21 +36,21 @@
 
   >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
   >>> for li_tag in attachments.findAll('li', 'download-attachment'):
-  ...   print li_tag.a.renderContents()
+  ...   print(li_tag.a.renderContents())
   Some information
 
   >>> link = user_browser.getLink('Some information')
   >>> link.url
   'http://bugs.launchpad.dev/firefox/+bug/1/+attachment/.../+files/foo.txt'
 
-  >>> 'Added some information' in user_browser.contents
+  >>> b'Added some information' in user_browser.contents
   True
 
 And that we stripped the leading and trailing whitespace correctly
 
-  >>> '   Some information   ' in user_browser.contents
+  >>> b'   Some information   ' in user_browser.contents
   False
-  >>> 'Some information' in user_browser.contents
+  >>> b'Some information' in user_browser.contents
   True
 
 If no description is given it gets set to the attachment filename. It's
@@ -69,7 +69,7 @@
 
   >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
   >>> for li_tag in attachments.findAll('li', 'download-attachment'):
-  ...   print li_tag.a.renderContents()
+  ...   print(li_tag.a.renderContents())
   Some information
   bar.txt
 
@@ -118,8 +118,8 @@
 ...where we see a message that we should double-check if this file
 is indeed not a patch.
 
-  >>> print extract_text(find_tags_by_class(
-  ...     user_browser.contents, 'documentDescription')[0])
+  >>> print(extract_text(find_tags_by_class(
+  ...     user_browser.contents, 'documentDescription')[0]))
   This file looks like a patch.
   What is a patch?
 
@@ -146,7 +146,7 @@
   'http://bugs.launchpad.dev/firefox/+bug/1'
   >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
   >>> for li_tag in attachments.findAll('li', 'download-attachment'):
-  ...   print li_tag.a.renderContents()
+  ...   print(li_tag.a.renderContents())
   Some information
   bar.txt
   More data
@@ -174,8 +174,8 @@
 ...where we see a message asking us if we really ant to declare this file
 as a patch.
 
-  >>> print extract_text(find_tags_by_class(
-  ...     user_browser.contents, 'documentDescription')[0])
+  >>> print(extract_text(find_tags_by_class(
+  ...     user_browser.contents, 'documentDescription')[0]))
   This file does not look like a patch.
   What is a patch?
 
@@ -203,7 +203,7 @@
 
   >>> patches = find_portlet(user_browser.contents, 'Patches')
   >>> for li_tag in patches.findAll('li', 'download-attachment'):
-  ...   print li_tag.a.renderContents()
+  ...   print(li_tag.a.renderContents())
   A fix for this bug.
   A better icon for foo
 
@@ -264,7 +264,7 @@
     >>> user_browser.url
     'http://bugs.launchpad.dev/firefox/+bug/1/+attachment/...'
 
-    >>> 'Edit attachment' in user_browser.contents
+    >>> b'Edit attachment' in user_browser.contents
     True
 
 There's also an option to cancel, which takes you back to the bug
@@ -285,7 +285,7 @@
     >>> user_browser.url
     'http://bugs.launchpad.dev/firefox/+bug/1'
 
-    >>> 'Another title' in user_browser.contents
+    >>> b'Another title' in user_browser.contents
     True
 
 We can edit the attachment to be a patch.
@@ -303,8 +303,8 @@
     >>> user_browser.url
     'http://bugs.launchpad.dev/firefox/+bug/1/+attachment/.../+confirm-is-patch'
 
-    >>> print extract_text(find_tags_by_class(
-    ...     user_browser.contents, 'documentDescription')[0])
+    >>> print(extract_text(find_tags_by_class(
+    ...     user_browser.contents, 'documentDescription')[0]))
     This file does not look like a patch.
     What is a patch?
 
@@ -331,7 +331,7 @@
 
     >>> patches = find_portlet(user_browser.contents, 'Patches')
     >>> for li_tag in patches.findAll('li', 'download-attachment'):
-    ...   print li_tag.a.renderContents()
+    ...   print(li_tag.a.renderContents())
     Another title
     A fix for this bug.
     A better icon for foo
@@ -343,7 +343,7 @@
 
     >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
     >>> for li_tag in attachments.findAll('li', 'download-attachment'):
-    ...   print li_tag.a.renderContents()
+    ...   print(li_tag.a.renderContents())
     bar.txt
     More data
 
@@ -406,7 +406,7 @@
     ...     "Show only bugs with patches available").selected = True
     >>> user_browser.getControl("Search", index=1).click()
 
-    >>> print user_browser.contents
+    >>> print(user_browser.contents)
     <!DOCTYPE...
     ...1...
     ...of

=== modified file 'lib/lp/bugs/stories/bugattachments/xx-delete-bug-attachment.txt'
--- lib/lp/bugs/stories/bugattachments/xx-delete-bug-attachment.txt	2015-10-06 06:48:01 +0000
+++ lib/lp/bugs/stories/bugattachments/xx-delete-bug-attachment.txt	2018-06-30 16:32:46 +0000
@@ -18,7 +18,7 @@
     >>> attachment_portlet = find_portlet(
     ...     user_browser.contents, 'Bug attachments')
     >>> for li in attachment_portlet.findAll('li', 'download-attachment'):
-    ...     print li.a.renderContents()
+    ...     print(li.a.renderContents())
     Great deal
 
 There will also be a comment with a link to the attachment in its body.
@@ -37,7 +37,7 @@
 
     >>> import re
     >>> user_browser.getLink(url=re.compile(r'[+]attachment/\d+$')).click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Bug #2...
     >>> user_browser.getControl('Delete Attachment') is not None
     True
@@ -50,12 +50,12 @@
     >>> user_browser.url
     'http://.../+bug/2'
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     Attachment
     "<a href="http://bugs.launchpad.dev/...+files/foo.txt";>Great deal</a>"
     has been deleted.
 
-    >>> print find_portlet(user_browser.contents, 'Bug attachments')
+    >>> print(find_portlet(user_browser.contents, 'Bug attachments'))
     None
 
 Since the attachment has been deleted, the comment referencing it will no

=== modified file 'lib/lp/bugs/stories/bugattachments/xx-display-filesize-attachment.txt'
--- lib/lp/bugs/stories/bugattachments/xx-display-filesize-attachment.txt	2012-06-16 13:21:23 +0000
+++ lib/lp/bugs/stories/bugattachments/xx-display-filesize-attachment.txt	2018-06-30 16:32:46 +0000
@@ -21,7 +21,7 @@
     ...     user_browser.contents, 'boardCommentBody')[-1]
     >>> fileinfo = '(270 bytes, text/plain)'
     >>> for attachment in last_comment('li', 'download-attachment'):
-    ...     print extract_text(attachment)
+    ...     print(extract_text(attachment))
     description text Edit
     (270 bytes, text/plain)
 
@@ -52,7 +52,7 @@
     ...     user_browser.contents, 'boardCommentBody')[-1]
     >>> fileinfo = '(270 bytes, text/plain)'
     >>> for attachment in last_comment('li', 'download-attachment'):
-    ...     print extract_text(attachment)
+    ...     print(extract_text(attachment))
     description text Edit
     (2.6 KiB, text/plain)
 

=== modified file 'lib/lp/bugs/stories/bugs/bug-add-subscriber.txt'
--- lib/lp/bugs/stories/bugs/bug-add-subscriber.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bugs/bug-add-subscriber.txt	2018-06-30 16:32:46 +0000
@@ -75,7 +75,7 @@
     >>> from lp.services.mail import stub
     >>> len(stub.test_emails)
     1
-    >>> print stub.test_emails[0][2]
+    >>> print(stub.test_emails[0][2])
     MIME-Version: 1.0
     ...
     To: david.allouche@xxxxxxxxxxxxx
@@ -181,8 +181,8 @@
 entry in the activity log for Private Team.
 
     >>> def print_row(row):
-    ...     print ' | '.join(
-    ...         extract_text(cell) for cell in row(('th', 'td')))
+    ...     print(' | '.join(
+    ...         extract_text(cell) for cell in row(('th', 'td'))))
     >>> user_browser.open(
     ...     'http://bugs.launchpad.dev/firefox/+bug/1/+activity')
     >>> main_content = find_main_content(user_browser.contents)

=== modified file 'lib/lp/bugs/stories/bugs/xx-add-comment-bugtask-edit.txt'
--- lib/lp/bugs/stories/bugs/xx-add-comment-bugtask-edit.txt	2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/stories/bugs/xx-add-comment-bugtask-edit.txt	2018-06-30 16:32:46 +0000
@@ -20,5 +20,5 @@
 
     >>> main_content = find_main_content(user_browser.contents)
     >>> last_comment = main_content('div', 'boardCommentBody')[-1]
-    >>> print last_comment.div.renderContents()
+    >>> print(last_comment.div.renderContents())
     <p>A comment with no change to the bug task.</p>

=== modified file 'lib/lp/bugs/stories/bugs/xx-add-comment-distribution-no-current-release.txt'
--- lib/lp/bugs/stories/bugs/xx-add-comment-distribution-no-current-release.txt	2011-04-20 14:56:23 +0000
+++ lib/lp/bugs/stories/bugs/xx-add-comment-distribution-no-current-release.txt	2018-06-30 16:32:46 +0000
@@ -16,5 +16,5 @@
 
     >>> for comment_div in find_tags_by_class(
     ...     user_browser.contents, 'boardCommentBody'):
-    ...     print comment_div.div.renderContents()
+    ...     print(comment_div.div.renderContents())
     <p>A new comment.</p>

=== modified file 'lib/lp/bugs/stories/bugs/xx-add-comment-with-bugwatch-and-cve.txt'
--- lib/lp/bugs/stories/bugs/xx-add-comment-with-bugwatch-and-cve.txt	2010-09-29 11:15:46 +0000
+++ lib/lp/bugs/stories/bugs/xx-add-comment-with-bugwatch-and-cve.txt	2018-06-30 16:32:46 +0000
@@ -17,16 +17,16 @@
   'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/1'
 
   >>> added_cve_link = user_browser.getLink('1991-9911')
-  >>> print added_cve_link
+  >>> print(added_cve_link)
   <.../bugs/cve/1991-9911'>
 
   >>> bugwatch_portlet = find_portlet(user_browser.contents,
   ...    'Remote bug watches')
   >>> added_bugwatch_link = bugwatch_portlet('a')[-2]
-  >>> print added_bugwatch_link['href']
+  >>> print(added_bugwatch_link['href'])
   http://some.bugzilla/show_bug.cgi?id=9876
 
-  >>> print extract_text(added_bugwatch_link)
+  >>> print(extract_text(added_bugwatch_link))
   auto-some.bugzilla #9876
 
 When extracting the remote bug URLs, we can use whatever text we want and
@@ -47,7 +47,7 @@
   >>> bugwatch_portlet = find_portlet(user_browser.contents, 
   ...    'Remote bug watches')
   >>> added_bugwatch_link = bugwatch_portlet('a')[-2]
-  >>> print extract_text(added_bugwatch_link)
+  >>> print(extract_text(added_bugwatch_link))
   auto-bugzilla.example.org #1235555
   >>> 'answers.launchpad.net' in extract_text(bugwatch_portlet)
   False

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-activity.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-activity.txt	2015-06-27 04:10:49 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-activity.txt	2018-06-30 16:32:46 +0000
@@ -25,8 +25,8 @@
 The activity log itself is presented as a table.
 
     >>> def print_row(row):
-    ...     print ' | '.join(
-    ...         extract_text(cell) for cell in row(('th', 'td')))
+    ...     print(' | '.join(
+    ...         extract_text(cell) for cell in row(('th', 'td'))))
 
     >>> for row in main_content.table('tr'):
     ...     print_row(row)
@@ -40,7 +40,7 @@
     >>> anon_browser.open(
     ...     'http://bugs.launchpad.dev/debian/+source/'
     ...     'mozilla-firefox/+bug/3')
-    >>> print anon_browser.getLink('See full activity log').url
+    >>> print(anon_browser.getLink('See full activity log').url)
     http://.../+bug/3/+activity
 
 
@@ -54,8 +54,8 @@
     ...     """Print all the comments on the page."""
     ...     comment_divs = find_tags_by_class(page, 'boardComment')
     ...     for div_tag in comment_divs[subset]:
-    ...         print extract_text(div_tag).replace("&#8594;", "=>")
-    ...         print '-' * 8
+    ...         print(extract_text(div_tag).replace("&#8594;", "=>"))
+    ...         print('-' * 8)
 
     >>> user_browser.open(
     ...     'http://bugs.launchpad.dev/redfish/+bug/15/+addcomment')

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-affects-me-too.txt	2018-06-30 16:32:46 +0000
@@ -14,24 +14,24 @@
 the bug).
 
    >>> user_browser.open(test_bug_url)
-   >>> print extract_text(find_tag_by_id(
+   >>> print(extract_text(find_tag_by_id(
    ...     user_browser.contents, 'affectsmetoo').find(
-   ...         None, 'static'))
+   ...         None, 'static')))
    This bug affects 1 person. Does this bug affect you?
 
 Next to the statement is a link containing an edit icon.
 
    >>> edit_link = find_tag_by_id(
    ...     user_browser.contents, 'affectsmetoo').a
-   >>> print edit_link['href']
+   >>> print(edit_link['href'])
    +affectsmetoo
-   >>> print edit_link.img['src']
+   >>> print(edit_link.img['src'])
    /@@/edit
 
 The user is affected by this bug, so clicks the link.
 
    >>> user_browser.getLink(url='+affectsmetoo').click()
-   >>> print user_browser.url
+   >>> print(user_browser.url)
    http://bugs.launchpad.dev/.../+bug/.../+affectsmetoo
    >>> user_browser.getControl(name='field.affects').value
    ['YES']
@@ -43,9 +43,9 @@
 The bug page loads again, and now the text is changed, to make it
 clear to the user that they have marked this bug as affecting them.
 
-   >>> print extract_text(find_tag_by_id(
+   >>> print(extract_text(find_tag_by_id(
    ...     user_browser.contents, 'affectsmetoo').find(
-   ...         None, 'static'))
+   ...         None, 'static')))
    This bug affects you and 1 other person
 
 On second thoughts, the user realises that this bug does not affect
@@ -60,9 +60,9 @@
 
 Back at the bug page, the text changes once again.
 
-   >>> print extract_text(find_tag_by_id(
+   >>> print(extract_text(find_tag_by_id(
    ...     user_browser.contents, 'affectsmetoo').find(
-   ...         None, 'static'))
+   ...         None, 'static')))
    This bug affects 1 person, but not you
 
 
@@ -71,8 +71,8 @@
 Anonymous users just see the number of affected users.
 
    >>> anon_browser.open(test_bug_url)
-   >>> print extract_text(find_tag_by_id(
-   ...     anon_browser.contents, 'affectsmetoo'))
+   >>> print(extract_text(find_tag_by_id(
+   ...     anon_browser.contents, 'affectsmetoo')))
    This bug affects 1 person
 
 If no one is marked as affected by the bug, the message does not
@@ -83,7 +83,7 @@
    >>> logout()
 
    >>> anon_browser.open(test_bug_url)
-   >>> print find_tag_by_id(anon_browser.contents, 'affectsmetoo')
+   >>> print(find_tag_by_id(anon_browser.contents, 'affectsmetoo'))
    None
 
 
@@ -114,10 +114,10 @@
 The dynamic content is hidden by the presence of the "hidden" CSS
 class.
 
-   >>> print static_content.get('class')
+   >>> print(static_content.get('class'))
    static
 
-   >>> print dynamic_content.get('class')
+   >>> print(dynamic_content.get('class'))
    dynamic hidden
 
 It is the responsibilty of Javascript running in the page to unhide

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-comment-attach-file.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-comment-attach-file.txt	2013-09-27 04:13:23 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-comment-attach-file.txt	2018-06-30 16:32:46 +0000
@@ -17,7 +17,7 @@
     >>> user_browser.getControl(name='field.comment').value = "a test comment"
     >>> user_browser.getControl("Post Comment").click()
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/1
 
     >>> print_feedback_messages(user_browser.contents)
@@ -47,7 +47,7 @@
     >>> user_browser.getControl("Description").value = "some file"
     >>> user_browser.getControl("Post Comment").click()
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/1
 
     >>> print_feedback_messages(user_browser.contents)
@@ -62,7 +62,7 @@
     ...     StringIO(""), "text/plain", "foo.txt")
     >>> user_browser.getControl("Post Comment").click()
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/1/+addcomment
 
     >>> print_feedback_messages(user_browser.contents)
@@ -74,7 +74,7 @@
 
     >>> from lp.services.config import config
 
-    >>> print config.launchpad.max_attachment_size
+    >>> print(config.launchpad.max_attachment_size)
     1024
 
     >>> user_browser.open(
@@ -96,7 +96,7 @@
     >>> user_browser.getControl("Description").value = "some file"
     >>> user_browser.getControl("Post Comment").click()
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/1
 
     >>> print_feedback_messages(user_browser.contents)

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-comments-truncated.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-comments-truncated.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-comments-truncated.txt	2018-06-30 16:32:46 +0000
@@ -29,7 +29,7 @@
     ...     soup = find_main_content(page)
     ...     comment_divs = soup('div', 'boardCommentBody')
     ...     for div_tag in comment_divs:
-    ...         print div_tag
+    ...         print(div_tag)
     >>> print_comments(browser.contents) #doctest: -ELLIPSIS
     <div class="boardCommentBody">
     <a href="http://bugs.launchpad.dev/tomcat/+bug/2/comments/1/+download";>Download
@@ -58,7 +58,7 @@
 
 The whole comment is visible on this page:
 
-    >>> print browser.title
+    >>> print(browser.title)
     Comment #1 : Bug #2 (blackhole) : Bugs : Tomcat
 
     >>> print_comments(browser.contents) #doctest: -ELLIPSIS
@@ -123,7 +123,7 @@
     u'Bug #2 (blackhole) ... : Bugs : Tomcat'
     >>> text = find_tags_by_class(
     ...     user_browser.contents, 'boardCommentBody')[-1]
-    >>> print extract_text(text.findAll('p')[-2])
+    >>> print(extract_text(text.findAll('p')[-2]))
     --
     ______________________
     human@xxxxxxxxxxx
@@ -138,7 +138,7 @@
     u'Bug #2 (blackhole) ... : Bugs : Tomcat'
     >>> text = find_tags_by_class(
     ...     anon_browser.contents, 'boardCommentBody')[-1]
-    >>> print extract_text(text.findAll('p')[-2])
+    >>> print(extract_text(text.findAll('p')[-2]))
     --
     ______________________
     &lt;email address hidden&gt;
@@ -155,7 +155,7 @@
 Pagetests cannot test CSS and JS behaviour.  We can only check that the markup
 includes the hooks for the style and script.
 
-    >>> print text.findAll('p')[-2]
+    >>> print(text.findAll('p')[-2])
     <p><span class="foldable">--...
     &lt;email address hidden&gt;<br />
     Witty signatures rock!
@@ -169,7 +169,7 @@
 always displayed. Again we can continue with the anonymous user to
 see the markup.
 
-    >>> print text.findAll('p')[-3]
+    >>> print(text.findAll('p')[-3])
     <p>Somebody said sometime ago:<br />
     <span class="foldable-quoted">
     &gt; 1. Remove the letters  c, j, q, x, w<br />
@@ -181,12 +181,12 @@
 starts with '-----BEGIN PGP'.  There are two kinds of PGP blocks,
 the notice that the message is signed, and the signature.
 
-    >>> print text.findAll('p')[0]
+    >>> print(text.findAll('p')[0])
     <p><span class="foldable">-----BEGIN PGP SIGNED MESSAGE-----<br />
     Hash: SHA1
     </span></p>
 
-    >>> print text.findAll('p')[-1]
+    >>> print(text.findAll('p')[-1])
     <p><span class="foldable">-----BEGIN PGP SIGNATURE-----<br />
     Version: GnuPG v1.4.1 (GNU/Linux)<br />
     Comment: Using GnuPG with Thunderbird<br />

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-create-question.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-create-question.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-create-question.txt	2018-06-30 16:32:46 +0000
@@ -60,7 +60,7 @@
 The bug status is Invalid for 'linux-source-2.6.15 (Ubuntu)'--It cannot
 be edited.
 
-    >>> print extract_text(content(['table'], {'class' : 'listing'})[0])
+    >>> print(extract_text(content(['table'], {'class' : 'listing'})[0]))
     Affects                        Status   Importance  Assigned to  Milestone
     linux-source-2.6.15 (Ubuntu) ... Invalid  Medium      Unassigned ...
 
@@ -82,15 +82,15 @@
 
 They see their comment was appended to the bug report's messages.
 
-    >>> print extract_text(
-    ...     find_tags_by_class(str(content), 'boardCommentBody')[-1])
+    >>> print(extract_text(
+    ...     find_tags_by_class(str(content), 'boardCommentBody')[-1]))
     This bug is a question.
 
 No Privileges Person looks at the page heading and sees that Foo Bar is
 the bug owner. They see the link to the question in the 'Related
 questions' portlet, and uses it to go to the question page.
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/ubuntu/.../+bug/10
 
     >>> portlet = find_portlet(user_browser.contents, 'Related questions')
@@ -104,11 +104,11 @@
 No Privileges Person case see that the question was created from a bug.
 They use the link to Related bug to return to the bug.
 
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Question #... : Questions : linux-source-2.6.15 package : Ubuntu
 
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'original-bug'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'original-bug')))
     This question was originally filed as bug #10.
 
     >>> user_browser.getLink('#10: another test bug').click()
@@ -129,7 +129,7 @@
     'Bug #9 ...'
 
     >>> user_browser.getLink('Convert to a question').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Convert this bug to a question...
 
     >>> find_main_content(user_browser.contents).p
@@ -199,7 +199,7 @@
     'Bug #12 ...'
 
     >>> message = find_tag_by_id(user_browser.contents, 'bug-is-question')
-    >>> print extract_text(message)
+    >>> print(extract_text(message))
     This bug report was converted into a question:...question #...
 
 
@@ -214,7 +214,7 @@
     'Bug #12 ... : Bugs : Jokosher'
 
     >>> user_browser.getLink('Convert back to a bug').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Bug #12 - Convert this...
 
 The 'Convert back to a bug' page explains what will happen if No
@@ -242,17 +242,17 @@
     Copy, Cut and Delete operations should work...
 
     >>> portlet = find_portlet(user_browser.contents, 'Related questions')
-    >>> print portlet
+    >>> print(portlet)
     None
 
     >>> user_browser.getLink(
     ...     'Copy, Cut and Delete operations should work on '
     ...     'selections').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Question #... : Questions : Jokosher
 
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'question-status'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'question-status')))
     Status: Open
 
 No Privileges Person uses their browser's back button to view the bug
@@ -260,7 +260,7 @@
 
     >>> user_browser.goBack(count=1)
     >>> content = find_main_content(user_browser.contents)
-    >>> print extract_text(content(['table'], {'class' : 'listing'})[0])
+    >>> print(extract_text(content(['table'], {'class' : 'listing'})[0]))
     Affects                       Status   Importance  Assigned to  Milestone
     ... Jokosher ...              Invalid  Critical    Unassigned ...
     Affecting: Jokosher
@@ -268,8 +268,8 @@
 
 They read their comment that was appended to the bug report's messages.
 
-    >>> print extract_text(
-    ...     find_tags_by_class(str(content), 'boardComment')[-1])
+    >>> print(extract_text(
+    ...     find_tags_by_class(str(content), 'boardComment')[-1]))
     No Privileges Person (no-priv)
     wrote
     ...
@@ -284,7 +284,7 @@
 
     >>> user_browser.open(
     ...     'http://bugs.launchpad.dev/jokosher/+bug/12/+remove-question')
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Bug #12 - Convert this...
 
     >>> find_main_content(user_browser.contents).p

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-edit.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-edit.txt	2012-08-03 01:42:13 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-edit.txt	2018-06-30 16:32:46 +0000
@@ -6,10 +6,10 @@
 
     >>> user_browser.open(
     ...     'http://bugs.launchpad.dev/jokosher/+bug/11/+edit')
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Edit details for bug #11 ...
     >>> main = find_main_content(user_browser.contents)
-    >>> print extract_text(main.h1)
+    >>> print(extract_text(main.h1))
     Edit details for bug #11
 
 And now we show that any logged-in user can edit public bugs.
@@ -26,9 +26,9 @@
     >>> browser.url
     'http://.../debian/+source/mozilla-firefox/+bug/3'
     >>> content = find_main_content(browser.contents)
-    >>> print extract_text(content.h1)
+    >>> print(extract_text(content.h1))
     New Summary Edit
-    >>> print extract_text(find_tag_by_id(content, 'maincontentsub'))
+    >>> print(extract_text(find_tag_by_id(content, 'maincontentsub')))
     Edit Bug Description New description. ...
 
 
@@ -72,9 +72,9 @@
 
     >>> user_browser.url
     'http://bugs.launchpad.dev/firefox/+bug/1'
-    >>> 'layout-test' in user_browser.contents
+    >>> b'layout-test' in user_browser.contents
     True
-    >>> 'new-tag' in user_browser.contents
+    >>> b'new-tag' in user_browser.contents
     False
 
 Now, let's add 'new-tag' again, and confirm it this time.

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-heat-on-bug-page.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-heat-on-bug-page.txt	2012-02-07 07:20:50 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-heat-on-bug-page.txt	2018-06-30 16:32:46 +0000
@@ -4,5 +4,5 @@
 
     >>> anon_browser.open('http://bugs.launchpad.dev/firefox/+bug/1')
     >>> content = find_main_content(anon_browser.contents)
-    >>> print content.find('a', href='/+help-bugs/bug-heat.html')
+    >>> print(content.find('a', href='/+help-bugs/bug-heat.html'))
     <a href="/+help-bugs/bug-heat.html" target="help" class="sprite flame">0</a>

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-hidden-comments.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-hidden-comments.txt	2015-09-14 16:38:15 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-hidden-comments.txt	2018-06-30 16:32:46 +0000
@@ -13,7 +13,7 @@
     >>> main_content = find_main_content(user_browser.contents)
     >>> new_comment = main_content('div', 'boardCommentBody')[-1]
     >>> new_comment_text = extract_text(new_comment.div)
-    >>> print new_comment_text
+    >>> print(new_comment_text)
     This comment will not be visible when the test completes.
 
 Admin users can set a message's visible attribute to False.
@@ -26,7 +26,7 @@
     >>> login('foo.bar@xxxxxxxxxxxxx')
     >>> bug_11 = getUtility(IBugSet).get(11)
     >>> message = bug_11.messages[-1]
-    >>> print message.text_contents
+    >>> print(message.text_contents)
     This comment will not be visible when the test completes.
     >>> bug_message = getUtility(IBugMessageSet).getByBugAndMessage(
     ...     bug_11, message)
@@ -44,7 +44,7 @@
     >>> main_content = find_main_content(test_browser.contents)
     >>> last_comment = main_content('div', 'boardCommentBody')[-1]
     >>> last_comment_text = extract_text(last_comment.div)
-    >>> print last_comment_text
+    >>> print(last_comment_text)
     This title, however, is the same as the bug title and so it will
     be suppressed in the UI.
 
@@ -70,13 +70,13 @@
     >>> main_content = find_main_content(admin_browser.contents)
     >>> last_comment = main_content('div', 'boardCommentBody')[-1]
     >>> last_comment_text = extract_text(last_comment.div)
-    >>> print last_comment_text
+    >>> print(last_comment_text)
     This comment will not be visible when the test completes.
 
 Admin users will see the hidden message highlighted with an
 'adminHiddenComment' style.
 
-    >>> print last_comment.parent['class']
+    >>> print(last_comment.parent['class'])
     boardComment adminHiddenComment
 
 Admin users can also reach the message via direct link, and it is
@@ -86,14 +86,14 @@
     ...     'http://bugs.launchpad.dev'
     ...     '/jokosher/+bug/11/comments/%d' % (int(latest_index[1:]) + 1))
     >>> contents = extract_text(admin_browser.contents)
-    >>> print contents
+    >>> print(contents)
     Comment #7 : Bug #11 ...
     This comment will not be visible when the test completes.
     ...
     >>> main_content = find_main_content(admin_browser.contents)
     >>> last_comment = main_content('div', 'boardCommentBody')[-1]
     >>> last_comment_text = extract_text(last_comment.div)
-    >>> print last_comment.parent['class']
+    >>> print(last_comment.parent['class'])
     boardComment adminHiddenComment
 
 Also for the owner of comment the message is still visible in the bug page.
@@ -104,13 +104,13 @@
     >>> main_content = find_main_content(user_browser.contents)
     >>> last_comment = main_content('div', 'boardCommentBody')[-1]
     >>> last_comment_text = extract_text(last_comment.div)
-    >>> print last_comment_text
+    >>> print(last_comment_text)
     This comment will not be visible when the test completes.
 
 Owner of the comment will see the hidden message highlighted with an
 'adminHiddenComment' style.
 
-    >>> print last_comment.parent['class']
+    >>> print(last_comment.parent['class'])
     boardComment adminHiddenComment
 
 Owner of the comment can also reach the message via direct link, and it is
@@ -120,12 +120,12 @@
     ...     'http://bugs.launchpad.dev'
     ...     '/jokosher/+bug/11/comments/%d' % (int(latest_index[1:]) + 1))
     >>> contents = extract_text(user_browser.contents)
-    >>> print contents
+    >>> print(contents)
     Comment #7 : Bug #11 ...
     This comment will not be visible when the test completes.
     ...
     >>> main_content = find_main_content(user_browser.contents)
     >>> last_comment = main_content('div', 'boardCommentBody')[-1]
     >>> last_comment_text = extract_text(last_comment.div)
-    >>> print last_comment.parent['class']
+    >>> print(last_comment.parent['class'])
     boardComment adminHiddenComment

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-index-lots-of-comments.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-index-lots-of-comments.txt	2016-04-29 13:25:26 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-index-lots-of-comments.txt	2018-06-30 16:32:46 +0000
@@ -34,8 +34,8 @@
 
 With a warning telling the user where the comments have gone:
 
-    >>> print extract_text(first_tag_by_class(
-    ...     user_browser.contents, 'informational message'))
+    >>> print(extract_text(first_tag_by_class(
+    ...     user_browser.contents, 'informational message')))
     Displaying first 1
     and last 2
     comments.
@@ -45,8 +45,8 @@
 The add comment box is present but hidden so people don't accidentally
 reply to the wrong message.
 
-    >>> print find_tag_by_id(
-    ...     user_browser.contents, 'add-comment-form-container')
+    >>> print(find_tag_by_id(
+    ...     user_browser.contents, 'add-comment-form-container'))
     <div id="add-comment-form-container" class="hidden">...
     <div id="add-comment-form">...</div>...
     </div>
@@ -65,7 +65,7 @@
     >>> len(comments)
     7
 
-    >>> print find_tag_by_id(user_browser.contents, 'add-comment-form').name
+    >>> print(find_tag_by_id(user_browser.contents, 'add-comment-form').name)
     div
 
 Anonymous users have a slightly different experience. If the comment
@@ -73,8 +73,8 @@
 is also removed.
 
     >>> anon_browser.open('http://launchpad.dev/bugs/11')
-    >>> print find_tag_by_id(
-    ...     anon_browser.contents, 'add-comment-login-first')
+    >>> print(find_tag_by_id(
+    ...     anon_browser.contents, 'add-comment-login-first'))
     None
 
 When an anonymous user views all comments, the "you must log in" note
@@ -83,9 +83,9 @@
     >>> anon_browser.getLink('View all 6 comments').click()
     >>> add_comment_link = find_tag_by_id(
     ...     anon_browser.contents, 'add-comment-login-first')
-    >>> print extract_text(add_comment_link)
+    >>> print(extract_text(add_comment_link))
     To post a comment you must log in.
-    >>> print add_comment_link.a.get('href')
+    >>> print(add_comment_link.a.get('href'))
     +login?comments=all
 
 Restore the configuration to its previous setting.

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-index.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-index.txt	2015-06-27 04:10:49 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-index.txt	2018-06-30 16:32:46 +0000
@@ -30,7 +30,7 @@
 highlighted.
 
     >>> anon_browser.open('http://launchpad.dev/firefox/+bug/1')
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     <!DOCTYPE...
     ...
     ...<tr class="highlight" id="...">
@@ -42,12 +42,12 @@
     ...<tr>
     ...mozilla-firefox (Ubuntu)...
     ...
-    >>> anon_browser.contents.count('<tr class="highlight"')
+    >>> anon_browser.contents.count(b'<tr class="highlight"')
     1
 
     >>> anon_browser.open(
     ...     'http://launchpad.dev/debian/+source/mozilla-firefox/+bug/1')
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     <!DOCTYPE...
     ...
     ...<tr>
@@ -59,17 +59,17 @@
     ...<tr>
     ...mozilla-firefox (Ubuntu)...
     ...
-    >>> anon_browser.contents.count('<tr class="highlight"')
+    >>> anon_browser.contents.count(b'<tr class="highlight"')
     1
 
 If the context is a distribution package, the package name has a
 tooltip containing the package details.
 
-    >>> print anon_browser.getLink('mozilla-firefox (Ubuntu)').attrs['title']
+    >>> print(anon_browser.getLink('mozilla-firefox (Ubuntu)').attrs['title'])
     Latest release: 0.9, uploaded to main on 2004-09-27 11:57:13+00:00...
     by Mark Shuttleworth (mark), maintained by Mark Shuttleworth (mark)
 
-    >>> print anon_browser.getLink('mozilla-firefox (Debian)').attrs['title']
+    >>> print(anon_browser.getLink('mozilla-firefox (Debian)').attrs['title'])
     No current release for this source package in Debian
 
 (XXX 20080623 mpt: Projects and distributions should similarly have a tooltip
@@ -80,7 +80,7 @@
 
     >>> user_browser.open(
     ...     "http://launchpad.dev/debian/+source/mozilla-firefox/+bug/2";)
-    >>> print user_browser.contents
+    >>> print(user_browser.contents)
     <!DOCTYPE...
     ...
     ...Tomcat...
@@ -100,7 +100,7 @@
 
     >>> user_browser.open(
     ...     "http://launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/1";)
-    >>> print extract_text(find_tag_by_id(user_browser.contents, 'task17'))
+    >>> print(extract_text(find_tag_by_id(user_browser.contents, 'task17')))
     Affecting: mozilla-firefox (Ubuntu)
     Filed here by: Foo Bar
     When: 2004-01-17
@@ -110,18 +110,18 @@
 The bug page includes a link to report another bug.
 
     >>> user_browser.open('http://bugs.launchpad.dev/firefox/+bug/1')
-    >>> print user_browser.getLink('Report a bug').url
+    >>> print(user_browser.getLink('Report a bug').url)
     http://bugs.launchpad.dev/firefox/+filebug
 
     >>> user_browser.open(
     ...     'http://launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/1')
-    >>> print user_browser.getLink('Report a bug').url
+    >>> print(user_browser.getLink('Report a bug').url)
     http://launchpad.dev/ubuntu/+source/mozilla-firefox/+filebug
 
 There's also a link on the page that will take the user to the "Add
 attachment or patch" page, for use when JavaScript isn't available.
 
-    >>> print user_browser.getLink('Add attachment or patch').url
+    >>> print(user_browser.getLink('Add attachment or patch').url)
     http://bugs.launchpad.dev/ubuntu/+source/.../+bug/1/+addcomment
 
 
@@ -132,7 +132,7 @@
 redirected to the default context.
 
     >>> browser.open('http://bugs.launchpad.dev/jokosher/+bug/10')
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/linux-source-2.6.15/+bug/10
 
 
@@ -154,8 +154,8 @@
 On the bug page, for every bug task there's one expander.
 
     >>> browser.open(bug_url)
-    >>> print len(find_tags_by_class(
-    ...     browser.contents, 'bugtask-expander'))
+    >>> print(len(find_tags_by_class(
+    ...     browser.contents, 'bugtask-expander')))
     1
 
 We add four more tasks.
@@ -168,8 +168,8 @@
 And the expander appears five times.
 
     >>> browser.open(bug_url)
-    >>> print len(find_tags_by_class(
-    ...     browser.contents, 'bugtask-expander'))
+    >>> print(len(find_tags_by_class(
+    ...     browser.contents, 'bugtask-expander')))
     5
 
 But on pages with more than ten bug tasks, we don't include the expander
@@ -181,7 +181,7 @@
     >>> logout()
 
     >>> browser.open(bug_url)
-    >>> print len(find_tags_by_class(
-    ...     browser.contents, 'bugtask-expander'))
+    >>> print(len(find_tags_by_class(
+    ...     browser.contents, 'bugtask-expander')))
     0
 

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt	2012-02-21 22:30:41 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt	2018-06-30 16:32:46 +0000
@@ -25,10 +25,10 @@
     >>> anon_browser.open(
     ...     'http://bugs.launchpad.dev'
     ...     '/debian/sarge/+source/mozilla-firefox/+bug/3')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Bug #3 ...
 
-    >>> 'user@xxxxxxxxxx' in anon_browser.contents
+    >>> b'user@xxxxxxxxxx' in anon_browser.contents
     False
 
     >>> description = find_tag_by_id(

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt	2012-10-09 10:28:02 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt	2018-06-30 16:32:46 +0000
@@ -15,7 +15,7 @@
     >>> bug_notification_level_control = user_browser.getControl(
     ...     name='field.bug_notification_level')
     >>> for control in bug_notification_level_control.controls:
-    ...     print control.optionValue
+    ...     print(control.optionValue)
     Discussion
     Details
     Lifecycle
@@ -24,10 +24,10 @@
 this case, they want to subscribe to just metadata updates:
 
     >>> bug_notification_level_control.getControl(
-    ...     'any change is made to this bug, other than a new comment '
-    ...     'being added').click()
+    ...     b'any change is made to this bug, other than a new comment '
+    ...     b'being added').click()
     >>> user_browser.getControl('Continue').click()
 
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     You have subscribed to this bug report.

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-single-comment-view.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-single-comment-view.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-single-comment-view.txt	2018-06-30 16:32:46 +0000
@@ -10,7 +10,7 @@
     ...     soup = find_main_content(browser.contents)
     ...     comment_details = soup('div', 'boardCommentDetails')
     ...     for details in comment_details:
-    ...         print details.find('strong').string
+    ...         print(details.find('strong').string)
 
     >>> print_comment_titles(browser.contents)
     Fantastic idea, I'd really like to see this

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt	2015-10-06 06:48:01 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt	2018-06-30 16:32:46 +0000
@@ -55,14 +55,14 @@
     >>> anon_browser.open('http://launchpad.dev/bugs/1/+text')
     >>> anon_browser.url
     'http://launchpad.dev/bugs/1/+text'
-    >>> print anon_browser.headers['content-type']
+    >>> print(anon_browser.headers['content-type'])
     text/plain;charset=utf-8 
 
 The textual description contains basic information about that bug, along with
 all tasks related to that bug, presented in an easy-to-digest format:
 
-    >>> text_bug = anon_browser.contents
-    >>> print text_bug
+    >>> text_bug = anon_browser.contents.decode('UTF-8')
+    >>> print(text_bug)
     bug: 1
     title: Firefox does not support SVG
     date-reported: Thu, 01 Jan 2004 20:58:04 -0000
@@ -151,7 +151,7 @@
     >>> attachments_text = text_bug[text_bug.find('attachments:'):]
     >>> attachment_2 = attachments_text.split('\n')[2]
     >>> attachment_2
-    ' http://bugs.launchpad.dev/.../file%20with%20space.txt text/plain;
+    u' http://bugs.launchpad.dev/.../file%20with%20space.txt text/plain;
     name="file with space.txt"'
 
 
@@ -169,14 +169,14 @@
     >>> anon_browser.url
     'http://launchpad.dev/firefox/+bug/1/+text'
 
-    >>> print anon_browser.headers['content-type']
+    >>> print(anon_browser.headers['content-type'])
     text/plain;charset=utf-8
 
 The textual report contains the same information as the report provided by the
 parent bug context:
 
-    >>> text_bug_task = anon_browser.contents
-    >>> print text_bug_task
+    >>> text_bug_task = anon_browser.contents.decode('UTF-8')
+    >>> print(text_bug_task)
     bug: 1
     title: Firefox does not support SVG
     ...
@@ -213,8 +213,8 @@
     ...         assert(len(bug_task_lines) == len(bug_lines))
     ...         for line_no in range(len(bug_task_lines)):
     ...             if bug_lines[line_no] != bug_task_lines[line_no]:
-    ...                 print bug_lines[line_no]
-    ...                 print bug_task_lines[line_no]
+    ...                 print(bug_lines[line_no])
+    ...                 print(bug_task_lines[line_no])
     http://bugs.launchpad.dev/bugs/1/+attachment/.../+files/file_a.txt text/html
     http://bugs.launchpad.dev/firefox/+bug/.../+files/file_a.txt text/html
     http://bugs.launchpad.dev/bugs/1/.../+files/file%20with%20space.txt...
@@ -230,10 +230,10 @@
     >>> anon_browser.open('http://launchpad.dev/bugs/6/+text')
     >>> anon_browser.url
     'http://launchpad.dev/bugs/6/+text'
-    >>> print anon_browser.headers['content-type']
+    >>> print(anon_browser.headers['content-type'])
     text/plain;charset=utf-8 
 
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     bug: 6
     ...
     duplicate-of: 5
@@ -245,10 +245,10 @@
     >>> anon_browser.open('http://launchpad.dev/bugs/5/+text')
     >>> anon_browser.url
     'http://launchpad.dev/bugs/5/+text'
-    >>> print anon_browser.headers['content-type']
+    >>> print(anon_browser.headers['content-type'])
     text/plain;charset=utf-8 
 
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     bug: 5
     ...
     duplicate-of: 
@@ -268,10 +268,10 @@
     >>> anon_browser.open('http://launchpad.dev/firefox/+bugs-text')
     >>> anon_browser.url
     'http://launchpad.dev/firefox/+bugs-text'
-    >>> print anon_browser.headers['content-type']
+    >>> print(anon_browser.headers['content-type'])
     text/plain;charset=utf-8 
 
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     5
     4
 
@@ -283,10 +283,10 @@
     >>> search_parameters = 'field.status:list=FIXRELEASED'
     >>> url = base_url + '?' + search_parameters
     >>> anon_browser.open(url)
-    >>> print anon_browser.headers['content-type']
+    >>> print(anon_browser.headers['content-type'])
     text/plain;charset=utf-8
 
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     1
 
 Searching for bugs in a component of a distribution works too.
@@ -295,16 +295,16 @@
     >>> search_parameters = 'field.component=1'
     >>> url = base_url + '?' + search_parameters
     >>> anon_browser.open(url)
-    >>> print anon_browser.headers['content-type']
+    >>> print(anon_browser.headers['content-type'])
     text/plain;charset=utf-8
 
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     10
 
 This page is also available for project groups.
 
     >>> anon_browser.open('http://launchpad.dev/mozilla/+bugs-text')
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     15
     5
     4
@@ -315,7 +315,7 @@
 When a bug is private, the textual description reflects this:
 
     >>> admin_browser.open('http://launchpad.dev/bugs/14/+text')
-    >>> print admin_browser.contents
+    >>> print(admin_browser.contents)
     bug: 14
     title: jokosher exposes personal details in its actions portlet
     date-reported: Thu, 09 Aug 2007 11:39:16 -0000

=== modified file 'lib/lp/bugs/stories/bugs/xx-bugs-advanced-search-upstream-status.txt'
--- lib/lp/bugs/stories/bugs/xx-bugs-advanced-search-upstream-status.txt	2012-10-02 06:36:44 +0000
+++ lib/lp/bugs/stories/bugs/xx-bugs-advanced-search-upstream-status.txt	2018-06-30 16:32:46 +0000
@@ -26,7 +26,7 @@
     >>> upstream_status = anon_browser.getControl(
     ...     name='field.status_upstream')
     >>> upstream_status.displayValue = [
-    ...     'Show bugs that need to be forwarded to an upstream bug tracker']
+    ...     b'Show bugs that need to be forwarded to an upstream bug tracker']
     >>> anon_browser.getControl('Search', index=0).click()
     >>> print_bugtasks(anon_browser.contents)
     2 Blackhole Trash folder Ubuntu Medium New
@@ -50,8 +50,8 @@
     ...             BugTaskStatus.INVALID, getUtility(ILaunchBag).user)
 
     >>> for bugtask in bug_two.bugtasks:
-    ...     print "%s, %s" % (
-    ...         bugtask.target.bugtargetdisplayname, bugtask.status.title)
+    ...     print("%s, %s" % (
+    ...         bugtask.target.bugtargetdisplayname, bugtask.status.title))
     Tomcat, Invalid
     Ubuntu, New
     Ubuntu Hoary, New
@@ -64,9 +64,9 @@
     >>> upstream_status = anon_browser.getControl(
     ...     name='field.status_upstream')
     >>> upstream_status.displayValue = [
-    ...     'Show bugs that need to be forwarded to an upstream bug tracker']
+    ...     b'Show bugs that need to be forwarded to an upstream bug tracker']
     >>> anon_browser.getControl('Search', index=0).click()
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     <!DOCTYPE...
     ...No results...
 
@@ -79,7 +79,7 @@
     >>> upstream_status = anon_browser.getControl(
     ...     name='field.status_upstream')
     >>> upstream_status.displayValue = [
-    ...     'Show bugs that are not known to affect upstream']
+    ...     b'Show bugs that are not known to affect upstream']
     >>> anon_browser.getControl('Search', index=0).click()
     >>> print_bugtasks(anon_browser.contents)
     10 another test bug linux-source-2.6.15 (Ubuntu) Medium New
@@ -110,7 +110,7 @@
     >>> upstream_status = anon_browser.getControl(
     ...     name='field.status_upstream')
     >>> upstream_status.displayValue = [
-    ...     'Show bugs that are resolved upstream']
+    ...     b'Show bugs that are resolved upstream']
     >>> anon_browser.getControl('Search', index=0).click()
     >>> print_bugtasks(anon_browser.contents)
     1 Firefox does not support SVG mozilla-firefox (Ubuntu) Medium New
@@ -124,8 +124,8 @@
     >>> upstream_status = anon_browser.getControl(
     ...     name='field.status_upstream')
     >>> upstream_status.displayValue = [
-    ...     'Show bugs that are resolved upstream',
-    ...     'Show bugs that are not known to affect upstream'
+    ...     b'Show bugs that are resolved upstream',
+    ...     b'Show bugs that are not known to affect upstream'
     ...     ]
 
     >>> anon_browser.getControl('Search', index=0).click()

=== modified file 'lib/lp/bugs/stories/bugs/xx-bugs.txt'
--- lib/lp/bugs/stories/bugs/xx-bugs.txt	2014-11-24 09:16:35 +0000
+++ lib/lp/bugs/stories/bugs/xx-bugs.txt	2018-06-30 16:32:46 +0000
@@ -2,14 +2,14 @@
 specifically Hoary.
 
     >>> browser.open('http://localhost/ubuntu/hoary/+bugs')
-    >>> print browser.title
+    >>> print(browser.title)
     Hoary (5.04) : Bugs : Ubuntu 
 
 This page checks that we can see a list of bugs on the distributions, in
 this case Ubuntu.
 
     >>> browser.open('http://localhost/ubuntu/+bugs')
-    >>> print browser.title
+    >>> print(browser.title)
     Bugs : Ubuntu...
 
 Comments are intended to be contributions to a bug report that further
@@ -33,7 +33,7 @@
   >>> user_browser.url
   'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/2'
 
-  >>> print user_browser.contents
+  >>> print(user_browser.contents)
   <...
   ...This is a test comment...
 

=== modified file 'lib/lp/bugs/stories/bugs/xx-bugtask-assignee-widget.txt'
--- lib/lp/bugs/stories/bugs/xx-bugtask-assignee-widget.txt	2013-04-11 01:27:33 +0000
+++ lib/lp/bugs/stories/bugs/xx-bugtask-assignee-widget.txt	2018-06-30 16:32:46 +0000
@@ -1,10 +1,10 @@
 The bug task edit page now features a new and improved assignee
 widget, which makes it easier to "take" a task.
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /ubuntu/+source/mozilla-firefox/+bug/1/+editstatus HTTP/1.1
   ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...
   ...Assigned to...
@@ -14,7 +14,7 @@
 So, taking the task is now as simple as selecting the "me" radio
 button:
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... POST /ubuntu/+source/mozilla-firefox/+bug/1/+editstatus HTTP/1.1
   ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
   ... Referer: https://launchpad.dev/
@@ -69,17 +69,17 @@
   ... 
   ... Save Changes
   ... -----------------------------19759086281403130373932339922--
-  ... """)
+  ... """))
   HTTP/1.1 303 See Other
   ...
 
 In this example, we were logged in as Foo Bar, so the task is now
 automagically assigned to Foo Bar.
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /ubuntu/+source/mozilla-firefox/+bug/1 HTTP/1.1
   ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...
   ...mozilla-firefox (Ubuntu)...Foo Bar...
@@ -87,7 +87,7 @@
 
 But, you can also assign the task to another person, of course:
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... POST /ubuntu/+source/mozilla-firefox/+bug/1/+editstatus HTTP/1.1
   ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
   ... Referer: https://launchpad.dev/
@@ -143,16 +143,16 @@
   ... 
   ... Save Changes
   ... -----------------------------19759086281403130373932339922--
-  ... """)
+  ... """))
   HTTP/1.1 303 See Other
   ...
 
 In this case, we assigned the task to Sample Person:
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /ubuntu/+source/mozilla-firefox/+bug/1 HTTP/1.1
   ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...
   ...mozilla-firefox (Ubuntu)...Sample Person...
@@ -161,7 +161,7 @@
 Lastly, the widget also allows you to simply assign the task to nobody
 (to, "give up" the task, you might say)
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... POST /ubuntu/+source/mozilla-firefox/+bug/1/+editstatus HTTP/1.1
   ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
   ... Referer: https://launchpad.dev/
@@ -217,16 +217,16 @@
   ... 
   ... Save Changes
   ... -----------------------------19759086281403130373932339922--
-  ... """)
+  ... """))
   HTTP/1.1 303 See Other
   ...
 
 And now the bug task is unassigned:
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /ubuntu/+source/mozilla-firefox/+bug/1 HTTP/1.1
   ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...
   ...mozilla-firefox (Ubuntu)...

=== modified file 'lib/lp/bugs/stories/bugs/xx-distribution-bugs-page.txt'
--- lib/lp/bugs/stories/bugs/xx-distribution-bugs-page.txt	2014-11-29 06:41:25 +0000
+++ lib/lp/bugs/stories/bugs/xx-distribution-bugs-page.txt	2018-06-30 16:32:46 +0000
@@ -22,7 +22,7 @@
 
     >>> user_browser.open('http://bugs.launchpad.dev/ubuntu/+bugs')
     >>> user_browser.getLink('Subscribe to bug mail').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/ubuntu/+subscribe
 
 
@@ -41,7 +41,7 @@
     >>> anon_browser.url
     'http://.../+bugs?field.status_upstream=resolved_upstream'
 
-    >>> print find_main_content(anon_browser.contents)
+    >>> print(find_main_content(anon_browser.contents))
     <...
     <p>There are currently no open bugs.</p>
     ...
@@ -78,7 +78,7 @@
 see which bugs will expire if they are not confirmed.
 
     >>> expirable_bugs_link.click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Expirable bugs : Bugs : Ubuntu
 
 Debian does not use Launchpad to track bugs; the anonymous user cannot

=== modified file 'lib/lp/bugs/stories/bugs/xx-distributionsourcepackage-bugs.txt'
--- lib/lp/bugs/stories/bugs/xx-distributionsourcepackage-bugs.txt	2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/stories/bugs/xx-distributionsourcepackage-bugs.txt	2018-06-30 16:32:46 +0000
@@ -12,6 +12,6 @@
     ...     'http://bugs.launchpad.dev/ubuntu/+source/alsa-utils/')
     >>> portlet = find_portlet(anon_browser.contents,
     ...     '"alsa-utils" versions published in Ubuntu')
-    >>> print portlet
+    >>> print(portlet)
     <div class="portlet" id="portlet-publishing-details">
     ...

=== modified file 'lib/lp/bugs/stories/bugs/xx-distrorelease-bugs-page.txt'
--- lib/lp/bugs/stories/bugs/xx-distrorelease-bugs-page.txt	2014-11-29 06:41:25 +0000
+++ lib/lp/bugs/stories/bugs/xx-distrorelease-bugs-page.txt	2018-06-30 16:32:46 +0000
@@ -22,7 +22,7 @@
 
     >>> user_browser.open('http://bugs.launchpad.dev/ubuntu/warty')
     >>> user_browser.getLink('Subscribe to bug mail').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/ubuntu/warty/+subscribe
 
 
@@ -40,7 +40,7 @@
     >>> anon_browser.url
     'http://.../+bugs?field.status_upstream=resolved_upstream'
 
-    >>> print find_main_content(anon_browser.contents)
+    >>> print(find_main_content(anon_browser.contents))
     <...
     <p>There are currently no open bugs.</p>
     ...
@@ -58,6 +58,6 @@
 see which bugs will expire if they are not confirmed.
 
     >>> expirable_bugs_link.click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Expirable bugs : Warty (4.10) : Bugs : Ubuntu
 

=== modified file 'lib/lp/bugs/stories/bugs/xx-duplicate-of-private-bug.txt'
--- lib/lp/bugs/stories/bugs/xx-duplicate-of-private-bug.txt	2012-07-31 09:41:51 +0000
+++ lib/lp/bugs/stories/bugs/xx-duplicate-of-private-bug.txt	2018-06-30 16:32:46 +0000
@@ -11,13 +11,13 @@
     ...         'debian/+source/mozilla-firefox/+bug/8/+secrecy')
     >>> admin_browser.getControl('Private', index=1).selected = True
     >>> admin_browser.getControl('Change').click()
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/8
 
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     Bug #8 ...Printing doesn't work...
 
-    >>> print extract_text(find_tag_by_id(admin_browser.contents, 'privacy'))
+    >>> print(extract_text(find_tag_by_id(admin_browser.contents, 'privacy')))
     This report contains Private information...
 
 Next we mark another bug as a duplicate of the private bug:
@@ -33,21 +33,21 @@
 
     >>> def print_messages(browser):
     ...     for tag in find_tags_by_class(browser.contents, 'message'):
-    ...         print tag.renderContents()
+    ...         print(tag.renderContents())
 
-    >>> print find_tag_by_id(
-    ...     admin_browser.contents, 'duplicate-of').renderContents()
+    >>> print(find_tag_by_id(
+    ...     admin_browser.contents, 'duplicate-of').renderContents())
     bug #8
 
 But when accessing it as an unprivileged user the title of the private
 bug cannot be found in the messages on the duplicate bug page:
 
     >>> user_browser.open(admin_browser.url)
-    >>> print find_tag_by_id(user_browser.contents, 'duplicate-of')
+    >>> print(find_tag_by_id(user_browser.contents, 'duplicate-of'))
     None
 
 The same is true when viewing the duplicate bug anonymously:
 
     >>> anon_browser.open(admin_browser.url)
-    >>> print find_tag_by_id(anon_browser.contents, 'duplicate-of')
+    >>> print(find_tag_by_id(anon_browser.contents, 'duplicate-of'))
     None

=== modified file 'lib/lp/bugs/stories/bugs/xx-front-page-bug-lists.txt'
--- lib/lp/bugs/stories/bugs/xx-front-page-bug-lists.txt	2012-08-08 11:48:29 +0000
+++ lib/lp/bugs/stories/bugs/xx-front-page-bug-lists.txt	2018-06-30 16:32:46 +0000
@@ -46,11 +46,11 @@
 to the bug target and to the bug reporter's page.
 
     >>> def print_bugs_links(bug_row):
-    ...     print "%s: %s" % (
+    ...     print("%s: %s" % (
     ...         bug_row.span.a.renderContents().strip(),
-    ...         bug_row.a.renderContents())
-    ...     print bug_row.a['href']
-    ...     print bug_row.span.a['href']
+    ...         bug_row.a.renderContents()))
+    ...     print(bug_row.a['href'])
+    ...     print(bug_row.span.a['href'])
     >>> for li in reported_bugs('li'):
     ...     print_bugs_links(li)
     Bigfixer: Bug #...: Summary for new bug ...

=== modified file 'lib/lp/bugs/stories/bugs/xx-front-page-info.txt'
--- lib/lp/bugs/stories/bugs/xx-front-page-info.txt	2015-06-15 08:35:10 +0000
+++ lib/lp/bugs/stories/bugs/xx-front-page-info.txt	2018-06-30 16:32:46 +0000
@@ -19,7 +19,7 @@
     >>> anon_browser.open(
     ...     'http://bugs.launchpad.dev/test-project/+bugs')
     >>> uses_malone_p = find_tag_by_id(anon_browser.contents, 'no-malone')
-    >>> print extract_text(uses_malone_p)
+    >>> print(extract_text(uses_malone_p))
     Test-project must be configured in order for Launchpad to forward bugs to
     the project's developers.
 
@@ -33,7 +33,7 @@
     ...   'http://bugs.launchpad.dev/test-project/+bugs')
     >>> enable_tracker = find_tag_by_id(
     ...     admin_browser.contents, 'no-malone-edit')
-    >>> print extract_text(enable_tracker)
+    >>> print(extract_text(enable_tracker))
     Configure Bugs
 
 The +bugs page for a project using Launchpad for bug tracking
@@ -49,13 +49,13 @@
     >>> anon_browser.open('http://bugs.launchpad.dev/uses-malone/+bugs')
     >>> bug_supervisor = find_tag_by_id(
     ...     anon_browser.contents, 'bug-supervisor')
-    >>> print extract_text(bug_supervisor)
+    >>> print(extract_text(bug_supervisor))
     Bug supervisor:
     None set
 
     >>> bug_list = find_tag_by_id(
     ...     anon_browser.contents, 'bugs-table-listing')
-    >>> print extract_text(bug_list)
+    >>> print(extract_text(bug_list))
     There are currently no open bugs.
 
 Projects that use an external bug tracker will list the tracker on a
@@ -69,7 +69,7 @@
     >>> anon_browser.open(
     ...   'http://bugs.launchpad.dev/test-project/+bugs')
     >>> tracker_text = find_tag_by_id(anon_browser.contents, 'bugtracker')
-    >>> print extract_text(tracker_text)
+    >>> print(extract_text(tracker_text))
     Bugs are tracked in tracker.example.com/.
 
 Projects that are linked to an Ubuntu distro source package and that
@@ -84,7 +84,7 @@
     >>> logout()
     >>> anon_browser.open(
     ...   'http://bugs.launchpad.dev/test-project/+bugs')
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'also-in-ubuntu'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'also-in-ubuntu')))
     Ubuntu also tracks bugs for packages derived from this project:
     test-project-package in Ubuntu.

=== modified file 'lib/lp/bugs/stories/bugs/xx-front-page-search.txt'
--- lib/lp/bugs/stories/bugs/xx-front-page-search.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bugs/xx-front-page-search.txt	2018-06-30 16:32:46 +0000
@@ -25,7 +25,7 @@
     >>> anon_browser.getControl('All projects').selected = True
     >>> anon_browser.getControl(name='field.searchtext').value = 'test bug'
     >>> anon_browser.getControl('Search Bug Reports').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Search all bug reports
     >>> print_bugtasks(anon_browser.contents)
     3 Bug Title Test
@@ -86,7 +86,7 @@
     'test bug'
 
     >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     Please enter a project name
 
 An error message will be displayed also if the project isn't registered
@@ -102,7 +102,7 @@
     >>> anon_browser.getControl(name='field.searchtext').value
     'test bug'
     >>> 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 user doesn't know what name to write, they can use the 'Choose'
@@ -123,7 +123,7 @@
     >>> anon_browser.getControl(name='field.scope.target').value = 'evolution'
     >>> anon_browser.getControl(name='field.searchtext').value = 'test bug'
     >>> anon_browser.getControl('Search Bug Reports').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Bugs : Evolution...
     >>> anon_browser.url
     'http://bugs.launchpad.dev/evolution/+bugs?field.searchtext=test+bug...'
@@ -138,7 +138,7 @@
     >>> anon_browser.getControl(name='field.scope.target').value = 'gnome'
     >>> anon_browser.getControl(name='field.searchtext').value = 'test bug'
     >>> anon_browser.getControl('Search Bug Reports').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Bugs : GNOME
     >>> anon_browser.url
     'http://bugs.launchpad.dev/gnome/+bugs?field.searchtext=test+bug...'
@@ -153,7 +153,7 @@
     >>> anon_browser.getControl(name='field.scope.target').value = 'ubuntu'
     >>> anon_browser.getControl(name='field.searchtext').value = 'test bug'
     >>> anon_browser.getControl('Search Bug Reports').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Bugs : Ubuntu...
     >>> anon_browser.url
     'http://bugs.launchpad.dev/ubuntu/+bugs?field.searchtext=test+bug...'

=== modified file 'lib/lp/bugs/stories/bugs/xx-front-page-statistics.txt'
--- lib/lp/bugs/stories/bugs/xx-front-page-statistics.txt	2012-05-11 12:53:36 +0000
+++ lib/lp/bugs/stories/bugs/xx-front-page-statistics.txt	2018-06-30 16:32:46 +0000
@@ -10,7 +10,7 @@
     >>> anon_browser.open('http://bugs.launchpad.dev/')
     >>> statistics = find_portlet(
     ...     anon_browser.contents, 'Statistics')
-    >>> print extract_text(statistics)
+    >>> print(extract_text(statistics))
     Statistics
     15 bugs reported across 7 projects
     including 12 links to 8 bug trackers

=== modified file 'lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt'
--- lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt	2018-06-30 16:32:46 +0000
@@ -9,8 +9,8 @@
     ...     'http://bugs.launchpad.dev/jokosher/+bug/11/+editstatus')
     >>> user_browser.getControl('Status').value = ['Incomplete']
     >>> user_browser.getControl('Save Changes').click()
-    >>> print extract_text(
-    ...     find_tags_by_class(user_browser.contents, 'statusINCOMPLETE')[0])
+    >>> print(extract_text(
+    ...     find_tags_by_class(user_browser.contents, 'statusINCOMPLETE')[0]))
     Incomplete
 
 No Privileges Person can now search for the bug using the advanced
@@ -40,8 +40,8 @@
 The bug No Privileges Person examined earlier does not have any new
 information, so they do not see it in the list.
 
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'bugs-table-listing'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'bugs-table-listing')))
     No results for search
 
 No Privileges Person can supply new information by posting a new
@@ -91,8 +91,8 @@
 stating that the bug report will be marked for expiration.
 
     >>> user_browser.open('http://bugs.launchpad.dev/jokosher/+bug/11')
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'can-expire'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'can-expire')))
     This bug report will be marked for expiration in 59 days if no further
     activity occurs.
     (find out why)
@@ -121,8 +121,8 @@
     >>> logout()
 
     >>> user_browser.open('http://bugs.launchpad.dev/jokosher/+bug/11')
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'can-expire'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'can-expire')))
     This bug report was marked for expiration 1 days ago.
     (find out why)
 
@@ -153,7 +153,7 @@
 the bug they set to Incomplete previously.
 
     >>> expirable_bugs_link.click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Expirable bugs : Bugs : Jokosher
 
     >>> contents = find_main_content(user_browser.contents)
@@ -171,7 +171,7 @@
     >>> user_browser.getControl('Search', index=0).type
     Traceback (most recent call last):
     ...
-    LookupError: label 'Search'
+    LookupError: label u'Search'
 
 The 'Report a bug' link is also not present.
 
@@ -201,7 +201,7 @@
     >>> user_browser.getLink('Make Jokosher use autoaudiosink').click()
     >>> user_browser.getControl('Status').value = ['Confirmed']
     >>> user_browser.getControl('Save Changes', index=0).click()
-    >>> print find_tag_by_id(user_browser.contents, 'can-expire')
+    >>> print(find_tag_by_id(user_browser.contents, 'can-expire'))
     None
 
     >>> user_browser.getLink('Bugs').click()
@@ -231,14 +231,14 @@
     ...     'http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/8')
     >>> user_browser.getControl('Status').value = ['Incomplete']
     >>> user_browser.getControl('Save Changes', index=0).click()
-    >>> print find_tag_by_id(user_browser.contents, 'can-expire')
+    >>> print(find_tag_by_id(user_browser.contents, 'can-expire'))
     None
 
 If No Privileges Person hacks the URL to see a listing of Debian's
 expirable bugs they read that Debian does not use bug expiration.
 
     >>> user_browser.open('http://bugs.launchpad.dev/debian/+expirable-bugs')
-    >>> print extract_text(find_main_content(user_browser.contents).p)
+    >>> print(extract_text(find_main_content(user_browser.contents).p))
     This project has not enabled bug expiration. No bugs can expire.
     Project administrator's may choose to enable bug expiration by
     updating the project's details. See Bugs/Expiry.
@@ -273,7 +273,7 @@
 
     >>> user_browser.open('http://bugs.launchpad.dev/jokosher')
     >>> user_browser.getControl('Search', index=0).click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/jokosher/+bugs?...&field.status%3Alist=INCOMPLETE_WITH_RESPONSE&field.status%3Alist=INCOMPLETE_WITHOUT_RESPONSE...
     >>> ('<a href="http://bugs.launchpad.dev/jokosher/+bug/11"; class="bugtitle">' in
     ...     str(find_tag_by_id(user_browser.contents, 'bugs-table-listing')))

=== modified file 'lib/lp/bugs/stories/bugs/xx-link-bug-to-branch.txt'
--- lib/lp/bugs/stories/bugs/xx-link-bug-to-branch.txt	2010-12-21 19:28:29 +0000
+++ lib/lp/bugs/stories/bugs/xx-link-bug-to-branch.txt	2018-06-30 16:32:46 +0000
@@ -7,10 +7,10 @@
     >>> user_browser.open(
     ...     "http://bugs.launchpad.dev/firefox/+bug/1";)
     >>> user_browser.getLink('Link a related branch').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/1/+addbranch
-    >>> print extract_text(find_tag_by_id(
-    ...    user_browser.contents, 'maincontent'))
+    >>> print(extract_text(find_tag_by_id(
+    ...    user_browser.contents, 'maincontent')))
     Add a branch to bug #1...
     Linking a Bazaar branch to a bug allows you to notify others of
     work to fix this bug.
@@ -26,9 +26,9 @@
 This takes us back to the main page where this branch is now listed as
 a related branch.
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/1
-    >>> print extract_text(user_browser.contents)
+    >>> print(extract_text(user_browser.contents))
     Bug #1...
     Successfully registered branch main for this bug.
     ...
@@ -42,8 +42,8 @@
     ...     'http://code.launchpad.dev/~name12/firefox/main/+bug/1/+delete')
     >>> link = user_browser.getLink(url=delete_branch_link_url)
     >>> link.click()
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'maincontent'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'maincontent')))
     Remove bug branch link...
     Are you sure you want to remove the link between Bug #1: Firefox does
     not support SVG and the branch lp://dev/~name12/firefox/main?

=== modified file 'lib/lp/bugs/stories/bugs/xx-malone-homepage.txt'
--- lib/lp/bugs/stories/bugs/xx-malone-homepage.txt	2010-07-27 12:42:00 +0000
+++ lib/lp/bugs/stories/bugs/xx-malone-homepage.txt	2018-06-30 16:32:46 +0000
@@ -4,14 +4,14 @@
     >>> browser.url
     'http://bugs.launchpad.dev/'
 
-    >>> print browser.title
+    >>> print(browser.title)
     Launchpad Bugs
 
 There are a few related pages linked in a portlet:
 
     >>> related_pages = find_portlet(browser.contents, 'Related pages')
     >>> for link in related_pages.findAll('a'):
-    ...     print "%s\n  --> %s" % (extract_text(link), link.get('href'))
+    ...     print("%s\n  --> %s" % (extract_text(link), link.get('href')))
     Bug trackers
     --> http://bugs.launchpad.dev/bugs/bugtrackers
     CVE tracker

=== modified file 'lib/lp/bugs/stories/bugs/xx-numbered-comments.txt'
--- lib/lp/bugs/stories/bugs/xx-numbered-comments.txt	2012-12-11 05:41:50 +0000
+++ lib/lp/bugs/stories/bugs/xx-numbered-comments.txt	2018-06-30 16:32:46 +0000
@@ -14,10 +14,10 @@
     ...     person_node = comment.find(
     ...         lambda node: 'person' in node.get('class', ''))
     ...     comment_node = comment.find(None, 'comment-text')
-    ...     print "%s: %s\n  %s" % (
+    ...     print("%s: %s\n  %s" % (
     ...         extract_text(number_node),
     ...         extract_text(person_node),
-    ...         extract_text(comment_node)[:50])
+    ...         extract_text(comment_node)[:50]))
     #1: Valentina Commissari (tsukimi)
       The solution to this is to make Jokosher use autoa
     #2: Diogo Matsubara (matsubara)

=== modified file 'lib/lp/bugs/stories/bugs/xx-portlets-bug-milestones.txt'
--- lib/lp/bugs/stories/bugs/xx-portlets-bug-milestones.txt	2012-07-10 01:13:08 +0000
+++ lib/lp/bugs/stories/bugs/xx-portlets-bug-milestones.txt	2018-06-30 16:32:46 +0000
@@ -12,7 +12,7 @@
     >>> anon_browser.open("http://bugs.launchpad.dev/firefox";)
     >>> portlet = find_portlet(
     ...     anon_browser.contents, "Milestone-targeted bugs")
-    >>> print portlet
+    >>> print(portlet)
     None
 
 To enable the portlet, a bugtask needs to have a milestone associated with it.
@@ -22,7 +22,7 @@
     >>> from zope.component import getUtility
     >>> from lp.bugs.interfaces.bugtask import IBugTaskSet
     >>> ff_bugtask = getUtility(IBugTaskSet).get(13)
-    >>> print ff_bugtask.bug.id
+    >>> print(ff_bugtask.bug.id)
     4
 
     >>> from lp.registry.interfaces.milestone import IMilestoneSet
@@ -30,7 +30,7 @@
     >>> firefox = getUtility(IProductSet).getByName('firefox')
     >>> ff_milestone = getUtility(IMilestoneSet).getByNameAndProduct(
     ...     "1.0", firefox)
-    >>> print ff_milestone.name
+    >>> print(ff_milestone.name)
     1.0
 
 The bugtask milestone is set to the Firefox 1.0 milestone.
@@ -44,7 +44,7 @@
     >>> anon_browser.open("http://bugs.launchpad.dev/firefox";)
     >>> portlet = find_portlet(
     ...     anon_browser.contents, "Milestone-targeted bugs")
-    >>> print extract_text(portlet)
+    >>> print(extract_text(portlet))
     Milestone-targeted bugs
     1
     1.0
@@ -61,7 +61,7 @@
     >>> anon_browser.open("http://bugs.launchpad.dev/debian/sarge/+bugs";)
     >>> portlet = find_portlet(
     ...     anon_browser.contents, "Milestone-targeted bugs")
-    >>> print extract_text(portlet)
+    >>> print(extract_text(portlet))
     Milestone-targeted bugs
     1
     3.1

=== modified file 'lib/lp/bugs/stories/bugs/xx-portlets-bug-series.txt'
--- lib/lp/bugs/stories/bugs/xx-portlets-bug-series.txt	2012-07-10 01:13:08 +0000
+++ lib/lp/bugs/stories/bugs/xx-portlets-bug-series.txt	2018-06-30 16:32:46 +0000
@@ -6,7 +6,7 @@
 
   >>> anon_browser.open("http://bugs.launchpad.dev/debian/+bugs";)
   >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
-  >>> print portlet
+  >>> print(portlet)
   None
 
 Change debian to track bugs in Launchpad and the portlet becomes visible.
@@ -16,7 +16,7 @@
 
   >>> anon_browser.open("http://bugs.launchpad.dev/debian/+bugs";)
   >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
-  >>> print extract_text(portlet)
+  >>> print(extract_text(portlet))
   Series-targeted bugs
   1
   sarge
@@ -25,7 +25,7 @@
 
   >>> anon_browser.open("http://bugs.launchpad.dev/debian/sarge/+bugs";)
   >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
-  >>> print extract_text(portlet)
+  >>> print(extract_text(portlet))
   Series-targeted bugs
   1
   sarge
@@ -34,14 +34,14 @@
 
   >>> anon_browser.open("http://bugs.launchpad.dev/ubuntu/+bugs";)
   >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
-  >>> print extract_text(portlet)
+  >>> print(extract_text(portlet))
   Series-targeted bugs
   1
   hoary
   1
   warty
 
-  >>> print anon_browser.getLink("hoary").url
+  >>> print(anon_browser.getLink("hoary").url)
   http://bugs.launchpad.dev/ubuntu/hoary/+bugs
 
 The same portlet is also available for project and project series
@@ -49,14 +49,14 @@
 
   >>> anon_browser.open("http://bugs.launchpad.dev/firefox/+bugs";)
   >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
-  >>> print extract_text(portlet)
+  >>> print(extract_text(portlet))
   Series-targeted bugs
   1
   1.0
 
   >>> anon_browser.open("http://bugs.launchpad.dev/firefox/1.0/+bugs";)
   >>> portlet = find_portlet(anon_browser.contents, "Series-targeted bugs")
-  >>> print extract_text(portlet)
+  >>> print(extract_text(portlet))
   Series-targeted bugs
   1
   1.0

=== modified file 'lib/lp/bugs/stories/bugs/xx-product-bugs-page.txt'
--- lib/lp/bugs/stories/bugs/xx-product-bugs-page.txt	2014-11-29 06:41:25 +0000
+++ lib/lp/bugs/stories/bugs/xx-product-bugs-page.txt	2018-06-30 16:32:46 +0000
@@ -61,7 +61,7 @@
     >>> anon_browser.url
     'http://.../+bugs?field.status_upstream=resolved_upstream'
 
-    >>> print find_main_content(anon_browser.contents)
+    >>> print(find_main_content(anon_browser.contents))
     <...
     <p>There are currently no open bugs.</p>
     ...
@@ -81,7 +81,7 @@
 see which bugs will expire if they are not confirmed.
 
     >>> expirable_bugs_link.click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Expirable bugs : Bugs : Jokosher
 
 Product series may also have a link to expirable bugs. Jokosher's trunk
@@ -90,7 +90,7 @@
     >>> anon_browser.open('http://bugs.launchpad.dev/jokosher/trunk')
     >>> expirable_bugs_link = anon_browser.getLink('Incomplete bugs')
     >>> expirable_bugs_link.click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Expirable bugs : Series trunk : Bugs : Jokosher
 
 Thunderbird has not enabled bug expiration; the anonymous user
@@ -112,7 +112,7 @@
     >>> anon_browser.open('http://bugs.launchpad.dev/firefox')
     >>> tags_portlet = find_tag_by_id(anon_browser.contents, 'portlet-tags')
     >>> anon_browser.getLink(id='tags-content-link').click()
-    >>> print extract_text(anon_browser.contents)
+    >>> print(extract_text(anon_browser.contents))
     Tags
     1
     layout-test

=== modified file 'lib/lp/bugs/stories/bugs/xx-project-bugs-page.txt'
--- lib/lp/bugs/stories/bugs/xx-project-bugs-page.txt	2011-01-20 20:41:30 +0000
+++ lib/lp/bugs/stories/bugs/xx-project-bugs-page.txt	2018-06-30 16:32:46 +0000
@@ -4,5 +4,5 @@
 for the ProjectGroup.
 
     >>> anon_browser.open('http://bugs.launchpad.dev/gnome')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Bugs : GNOME

=== modified file 'lib/lp/bugs/stories/bugs/xx-remote-bug-comments.txt'
--- lib/lp/bugs/stories/bugs/xx-remote-bug-comments.txt	2014-05-29 22:22:36 +0000
+++ lib/lp/bugs/stories/bugs/xx-remote-bug-comments.txt	2018-06-30 16:32:46 +0000
@@ -10,7 +10,7 @@
     ...     user_browser.contents, 'remoteBugComment', only_first=True)
     >>> details = remote_bug_comment.find(
     ...     attrs={'class': 'boardCommentDetails'})
-    >>> print extract_text(details)
+    >>> print(extract_text(details))
     In
     Debian Bug tracker #308994,
     josh (jbuhl-nospam)
@@ -21,7 +21,7 @@
 Remote comments are decorated with the bug watch icon, to distinguish
 them from comments posted directly by Launchpad users.
 
-    >>> print details.find('img')['src']
+    >>> print(details.find('img')['src'])
     /@@/bug-remote
 
 Since it's possible to reply to imported comments and have them
@@ -31,14 +31,14 @@
 
     >>> activity = remote_bug_comment.find(
     ...     attrs={'class': 'boardCommentActivity'})
-    >>> print extract_text(activity)
+    >>> print(extract_text(activity))
     Reply on Debian Bug tracker...
 
 When javascript is not available, the link simply takes us to the
 individual comment page, where the inline form is displayed.
 
     >>> user_browser.getLink('Reply on Debian Bug tracker').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/redfish/+bug/15/comments/1
 
 We enter a comment, and submit the form.
@@ -51,7 +51,7 @@
 
     >>> new_bug_comment = find_tags_by_class(
     ...     user_browser.contents, 'remoteBugComment')[-1]
-    >>> print extract_text(new_bug_comment)
+    >>> print(extract_text(new_bug_comment))
     In
     Debian Bug tracker #308994,
     ...
@@ -64,7 +64,7 @@
 to the remote bug tracker.
 
     >>> activity = new_bug_comment.find(attrs={'class': 'boardCommentActivity'})
-    >>> print extract_text(activity.findAll('td')[1])
+    >>> print(extract_text(activity.findAll('td')[1]))
     Awaiting synchronization
 
 When the comment is synchronized, it receives a remote comment id, and
@@ -78,7 +78,7 @@
     >>> login('foo.bar@xxxxxxxxxxxxx')
     >>> bug_15 = getUtility(IBugSet).get(15)
     >>> message = bug_15.messages[-1]
-    >>> print message.text_contents
+    >>> print(message.text_contents)
     A reply comment.
     >>> bug_message = getUtility(IBugMessageSet).getByBugAndMessage(
     ...     bug_15, message)
@@ -90,7 +90,7 @@
     >>> user_browser.open('http://bugs.launchpad.dev/redfish/+bug/15')
     >>> last_bug_comment = find_tags_by_class(
     ...     user_browser.contents, 'remoteBugComment')[-1]
-    >>> print extract_text(last_bug_comment)
+    >>> print(extract_text(last_bug_comment))
     In
     Debian Bug tracker #308994,
     ...

=== modified file 'lib/lp/bugs/stories/bugs/xx-switch-to-malone.txt'
--- lib/lp/bugs/stories/bugs/xx-switch-to-malone.txt	2011-09-20 01:33:04 +0000
+++ lib/lp/bugs/stories/bugs/xx-switch-to-malone.txt	2018-06-30 16:32:46 +0000
@@ -16,7 +16,7 @@
     ...     '/+source/evolution/+bug/7/+editstatus')
     >>> main = find_main_content(user_browser.contents)
     >>> read_only_icon = main.find('span', {'class': 'sprite read-only'})
-    >>> print extract_text(read_only_icon.parent)
+    >>> print(extract_text(read_only_icon.parent))
     Unknown
 
 If the project switches to use Launchpad as its bug tracker, the
@@ -34,5 +34,5 @@
     ...     '/debian/+source/evolution/+bug/7/+editstatus')
     >>> main = find_main_content(user_browser.contents)
     >>> read_only_icon = main.find('span', {'class': 'sprite read-only'})
-    >>> print extract_text(read_only_icon.parent)
+    >>> print(extract_text(read_only_icon.parent))
     Unknown

=== modified file 'lib/lp/bugs/stories/bugs/xx-unique-ids-on-bug-page.txt'
--- lib/lp/bugs/stories/bugs/xx-unique-ids-on-bug-page.txt	2015-07-21 09:04:01 +0000
+++ lib/lp/bugs/stories/bugs/xx-unique-ids-on-bug-page.txt	2018-06-30 16:32:46 +0000
@@ -12,7 +12,7 @@
     >>> user_browser.getControl('Package')
     Traceback (most recent call last):
     ...
-    AmbiguityError: label 'Package'
+    AmbiguityError: label u'Package'
 
 Still, the ids of the fields are unique.
 

=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-bug-importance-change.txt'
--- lib/lp/bugs/stories/bugtask-management/xx-bug-importance-change.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bugtask-management/xx-bug-importance-change.txt	2018-06-30 16:32:46 +0000
@@ -5,7 +5,7 @@
 
     >>> admin_browser.open('http://bugs.launchpad.dev/bugs/10')
     >>> importance_control = admin_browser.getControl('Importance')
-    >>> print '\n'.join(importance_control.displayOptions)
+    >>> print('\n'.join(importance_control.displayOptions))
     Undecided
     Critical
     High
@@ -63,7 +63,7 @@
 For a product owner.
 
     >>> login(ANONYMOUS)
-    >>> print firefox.owner.name
+    >>> print(firefox.owner.name)
     name12
 
     >>> login("foo.bar@xxxxxxxxxxxxx")

=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-bug-privileged-statuses.txt'
--- lib/lp/bugs/stories/bugtask-management/xx-bug-privileged-statuses.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bugtask-management/xx-bug-privileged-statuses.txt	2018-06-30 16:32:46 +0000
@@ -14,7 +14,7 @@
     ...     'mozilla-firefox/+bug/1/+editstatus')
 
     >>> status_control = user_browser.getControl('Status')
-    >>> print status_control.displayValue
+    >>> print(status_control.displayValue)
     ['New']
 
 An unprivileged user can confirm the bug:
@@ -22,7 +22,7 @@
     >>> status_control.displayValue = ['Confirmed']
     >>> user_browser.getControl('Save Changes').click()
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/1
     >>> print_highlighted_bugtask(user_browser)
     mozilla-firefox (Ubuntu) ... Confirmed  Medium Unassigned ...
@@ -35,7 +35,7 @@
     ...     'mozilla-firefox/+bug/1/+editstatus')
 
     >>> status_control = user_browser.getControl('Status')
-    >>> print status_control.displayValue
+    >>> print(status_control.displayValue)
     ['Confirmed']
 
     >>> status_control.displayValue = ["Won't Fix"]
@@ -57,7 +57,8 @@
     ...     'test@xxxxxxxxxxxxx')
     >>> admin_browser.getControl('Change').click()
 
-    >>> print extract_text(find_tag_by_id(admin_browser.contents, 'bug-supervisor'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'bug-supervisor')))
     Bug supervisor:
     Sample Person
 
@@ -71,13 +72,13 @@
     ...     'mozilla-firefox/+bug/1/+editstatus')
 
     >>> status_control = bug_supervisor_browser.getControl('Status')
-    >>> print status_control.displayValue
+    >>> print(status_control.displayValue)
     ['Confirmed']
 
     >>> status_control.displayValue = ["Won't Fix"]
     >>> bug_supervisor_browser.getControl('Save Changes').click()
 
-    >>> print bug_supervisor_browser.url
+    >>> print(bug_supervisor_browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/1
     >>> print_highlighted_bugtask(bug_supervisor_browser)
     mozilla-firefox (Ubuntu) ... Won't Fix  Medium Unassigned ...
@@ -90,20 +91,20 @@
     ...     'mozilla-firefox/+bug/1/+editstatus')
 
     >>> status_control = user_browser.getControl('Status')
-    >>> print status_control.displayValue
+    >>> print(status_control.displayValue)
     ["Won't Fix"]
 
 And a regular user can change other aspects of the bug:
 
     >>> package_control = user_browser.getControl(
     ...     name='ubuntu_mozilla-firefox.target.package')
-    >>> print package_control.value
+    >>> print(package_control.value)
     mozilla-firefox
 
     >>> package_control.value = 'iceweasel'
     >>> user_browser.getControl('Save Changes').click()
 
-    >>> print bug_supervisor_browser.url
+    >>> print(bug_supervisor_browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/1
     >>> print_highlighted_bugtask(bug_supervisor_browser)
     mozilla-firefox (Ubuntu) ... Won't Fix  Medium Unassigned ...
@@ -115,13 +116,13 @@
     ...     'iceweasel/+bug/1/+editstatus')
 
     >>> status_control = bug_supervisor_browser.getControl('Status')
-    >>> print status_control.displayValue
+    >>> print(status_control.displayValue)
     ["Won't Fix"]
 
     >>> status_control.displayValue = ["Triaged"]
     >>> bug_supervisor_browser.getControl('Save Changes').click()
 
-    >>> print bug_supervisor_browser.url
+    >>> print(bug_supervisor_browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/iceweasel/+bug/1
     >>> print_highlighted_bugtask(bug_supervisor_browser)
     iceweasel (Ubuntu) ... Triaged  Medium Unassigned ...

=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt'
--- lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bugtask-management/xx-bugtask-edit-forms.txt	2018-06-30 16:32:46 +0000
@@ -12,33 +12,33 @@
 respective bug task.
 
     >>> admin_browser.open('http://bugs.launchpad.dev/firefox/+bug/1')
-    >>> print extract_text(
-    ...     find_tag_by_id(admin_browser.contents, 'affected-software'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'affected-software')))
     Affects                     Status    Importance   Assigned to...
     Mozilla Firefox...          New       Low          Mark Shuttleworth...
     mozilla-firefox (Debian)... Confirmed Low          debbugs #304014...
     mozilla-firefox (Ubuntu)... New       Medium       Unassigned
     ...
 
-    >>> print admin_browser.getLink('New', index=0).url
+    >>> print(admin_browser.getLink('New', index=0).url)
     http://bugs.launchpad.dev/firefox/+bug/1/+editstatus
-    >>> print admin_browser.getLink('Low', index=0).url
+    >>> print(admin_browser.getLink('Low', index=0).url)
     http://bugs.launchpad.dev/firefox/+bug/1/+editstatus
 
-    >>> print admin_browser.getLink('Confirmed').url
+    >>> print(admin_browser.getLink('Confirmed').url)
     Traceback (most recent call last):
     ...
     LinkNotFoundError
 
-    >>> print admin_browser.getLink('Low', index=1).url
+    >>> print(admin_browser.getLink('Low', index=1).url)
     http://bugs.launchpad.dev/debian/+source/mozilla-firefox/+bug/1/+editstatus
-    >>> print admin_browser.getLink('New', index=1).url
+    >>> print(admin_browser.getLink('New', index=1).url)
     http://bugs...dev/ubuntu/+source/mozilla-firefox/+bug/1/+editstatus
-    >>> print admin_browser.getLink('Medium').url
+    >>> print(admin_browser.getLink('Medium').url)
     http://bugs...dev/ubuntu/+source/mozilla-firefox/+bug/1/+editstatus
 
     >>> admin_browser.getLink('New').click()
-    >>> print extract_text(admin_browser.contents)
+    >>> print(extract_text(admin_browser.contents))
     Edit status...
     ...
     Affecting: Mozilla Firefox

=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt'
--- lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt	2018-06-30 16:32:46 +0000
@@ -39,8 +39,8 @@
     ...     name="firefox.assignee", index=0)
     >>> assign_to_control.value = "cprov"
     >>> admin_browser.getControl("Save Changes", index=0).click()
-    >>> print extract_text(
-    ...     first_tag_by_class(admin_browser.contents, 'warning message'))
+    >>> print(extract_text(
+    ...     first_tag_by_class(admin_browser.contents, 'warning message')))
     Celso Providelo
     did not previously have any assigned bugs in
     Mozilla Firefox.
@@ -58,8 +58,8 @@
     ...     name="jokosher.assignee", index=0)
     >>> assign_to_control.value = "cprov"
     >>> admin_browser.getControl("Save Changes", index=0).click()
-    >>> print extract_text(
-    ...     first_tag_by_class(admin_browser.contents, 'warning message'))
+    >>> print(extract_text(
+    ...     first_tag_by_class(admin_browser.contents, 'warning message')))
     Celso Providelo
     did not previously have any assigned bugs in Jokosher.
     If this bug was assigned by mistake, you may change the assignment.
@@ -82,10 +82,10 @@
     ...     name="jokosher.assignee.option", index=0)
     >>> assignee_control.value = ["jokosher.assignee.assign_to_me"]
     >>> user_browser.getControl("Save Changes", index=0).click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/jokosher/+bug/11
-    >>> print first_tag_by_class(
-    ...     user_browser.contents, 'warning message')
+    >>> print(first_tag_by_class(
+    ...     user_browser.contents, 'warning message'))
     None
 
 
@@ -120,11 +120,11 @@
     Traceback (most recent call last):
     ...
     ItemNotFoundError: insufficient items with name
-    'jokosher.assignee.assign_to'
+    u'jokosher.assignee.assign_to'
     >>> user_browser.getControl(name="jokosher.assignee", index=0)
     Traceback (most recent call last):
     ...
-    LookupError: name 'jokosher.assignee'
+    LookupError: name u'jokosher.assignee'
 
 Once no_priv is a member of a team, the option is shown.
 

=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-change-milestone.txt'
--- lib/lp/bugs/stories/bugtask-management/xx-change-milestone.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bugtask-management/xx-change-milestone.txt	2018-06-30 16:32:46 +0000
@@ -14,7 +14,7 @@
 use, but the bug is not yet targeted to that milestone.
 
     >>> table = find_tag_by_id(owner_browser.contents, 'affected-software')
-    >>> print extract_text(table)
+    >>> print(extract_text(table))
     Affects             Status  Importance  Assigned to        Milestone
     ... Mozilla Firefox ... New     Low     Mark Shuttleworth
     ...
@@ -32,7 +32,7 @@
     >>> milestone_control.displayValue = ['Mozilla Firefox 1.0']
     >>> owner_browser.getControl('Save Changes', index=0).click()
     >>> table = find_tag_by_id(owner_browser.contents, 'affected-software')
-    >>> print extract_text(table)
+    >>> print(extract_text(table))
     Affects             Status  Importance  Assigned to           Milestone
     ... Mozilla Firefox ... New     Low     Mark Shuttleworth ... 1.0
     ...
@@ -84,7 +84,7 @@
 
     >>> admin_browser.open('http://launchpad.dev/firefox/+bug/1')
     >>> table = find_tag_by_id(admin_browser.contents, 'affected-software')
-    >>> print extract_text(table)
+    >>> print(extract_text(table))
     Affects                      Status  Importance  Assigned to           Milestone
     ... Mozilla Firefox          ... New     Low     Mark Shuttleworth ... 1.0
     ...
@@ -99,7 +99,7 @@
     >>> milestone_control.displayValue = ['Ubuntu 5.04.rc1']
     >>> admin_browser.getControl('Save Changes', index=3).click()
     >>> table = find_tag_by_id(admin_browser.contents, 'affected-software')
-    >>> print extract_text(table)
+    >>> print(extract_text(table))
     Affects                      Status  Importance  Assigned to           Milestone
     ... Mozilla Firefox      ... New     Low         Mark Shuttleworth ... 1.0
     ...

=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-edit-email-address-bugtask.txt'
--- lib/lp/bugs/stories/bugtask-management/xx-edit-email-address-bugtask.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/bugtask-management/xx-edit-email-address-bugtask.txt	2018-06-30 16:32:46 +0000
@@ -31,7 +31,7 @@
 
     >>> def print_widget_visibility(user, url):
     ...     status, importance = widget_visibility(user, url)
-    ...     print 'Status: %s\nImportance: %s' % (status, importance)
+    ...     print('Status: %s\nImportance: %s' % (status, importance))
 
 
 == "Normal" (not Email Address) bugtasks ==

=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-subscribe-while-editing.txt'
--- lib/lp/bugs/stories/bugtask-management/xx-subscribe-while-editing.txt	2015-07-21 09:04:01 +0000
+++ lib/lp/bugs/stories/bugtask-management/xx-subscribe-while-editing.txt	2018-06-30 16:32:46 +0000
@@ -12,7 +12,7 @@
 
     >>> browser.getControl("Save Changes").click()
 
-    >>> print browser.contents
+    >>> print(browser.contents)
     <!DOCTYPE...
     ...You have subscribed to this bug report...
 

=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-view-editable-bug-task.txt'
--- lib/lp/bugs/stories/bugtask-management/xx-view-editable-bug-task.txt	2012-10-03 04:52:37 +0000
+++ lib/lp/bugs/stories/bugtask-management/xx-view-editable-bug-task.txt	2018-06-30 16:32:46 +0000
@@ -31,5 +31,5 @@
 
     >>> browser = setupBrowser(auth="Basic test@xxxxxxxxxxxxx:test")
     >>> browser.open("http://launchpad.dev/firefox/+bug/6/+editstatus";)
-    >>> print browser.title
+    >>> print(browser.title)
     Edit status ...

=== modified file 'lib/lp/bugs/stories/bugtask-searches/xx-advanced-people-filters.txt'
--- lib/lp/bugs/stories/bugtask-searches/xx-advanced-people-filters.txt	2014-11-29 06:41:25 +0000
+++ lib/lp/bugs/stories/bugtask-searches/xx-advanced-people-filters.txt	2018-06-30 16:32:46 +0000
@@ -16,10 +16,10 @@
 
     >>> browser.open('http://launchpad.dev/firefox/+bugs?advanced=1')
     >>> assignee_widget = find_tag_by_id(browser.contents, 'field.assignee')
-    >>> print assignee_widget['onkeypress']
+    >>> print(assignee_widget['onkeypress'])
     selectWidget('assignee_option', event)
 
-    >>> print find_tag_by_id(browser.contents, 'assignee_option')
+    >>> print(find_tag_by_id(browser.contents, 'assignee_option'))
     <input...type="radio"...>
 
 
@@ -120,7 +120,7 @@
     >>> anon_browser.getControl('Commenter').value = 'non-existent'
     >>> anon_browser.getControl('Search', index=0).click()
     >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     There&#x27;s no person with the name or email address &#x27;non-existent&#x27;.
 
 Entering an existing person shows all bugs that person has commented on.
@@ -177,7 +177,7 @@
     >>> anon_browser.getControl('Subscriber').value = 'non-existent'
     >>> anon_browser.getControl('Search', index=0).click()
     >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     There&#x27;s no person with the name or email address &#x27;non-existent&#x27;.
 
 Entering an existing person shows all bugs for packages or products that
@@ -187,7 +187,7 @@
     >>> subscriber = 'no-priv@xxxxxxxxxxxxx'
     >>> anon_browser.getControl('Subscriber').value = subscriber
     >>> anon_browser.getControl('Search', index=0).click()
-    >>> print extract_text(find_main_content(anon_browser.contents))
+    >>> print(extract_text(find_main_content(anon_browser.contents)))
     Advanced search
     ...
     No results for search
@@ -199,7 +199,7 @@
     >>> browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
     >>> browser.open('http://bugs.launchpad.dev/firefox/')
     >>> browser.getLink('Report a bug').click()
-    >>> print extract_text(find_main_content(browser.contents))
+    >>> print(extract_text(find_main_content(browser.contents)))
     Report a bug...
 
     >>> report_bug_url = browser.url
@@ -228,7 +228,7 @@
 
     >>> browser.open(bug_1_url)
     >>> browser.getLink('Subscribe someone else').click()
-    >>> print extract_text(find_main_content(browser.contents))
+    >>> print(extract_text(find_main_content(browser.contents)))
     Subscribe someone else to bug #...
 
     >>> browser.getControl('Person').value = subscriber
@@ -244,14 +244,14 @@
     >>> anon_browser.getControl('Search', index=0).click()
     >>> from lp.bugs.tests.bug import extract_bugtasks
     >>> for bugtask in extract_bugtasks(anon_browser.contents):
-    ...     print 'Task:' + bugtask
+    ...     print('Task:' + bugtask)
     Task:...Test Bug 1...Undecided...New
 
 Next we'll subscribe our user to the second bug we've just registered:
 
     >>> browser.open(bug_2_url)
     >>> browser.getLink('Subscribe someone else').click()
-    >>> print extract_text(find_main_content(browser.contents))
+    >>> print(extract_text(find_main_content(browser.contents)))
     Subscribe someone else to bug #...
 
     >>> browser.getControl('Person').value = subscriber
@@ -266,7 +266,7 @@
     >>> anon_browser.getControl('Subscriber').value = subscriber
     >>> anon_browser.getControl('Search', index=0).click()
     >>> for bugtask in extract_bugtasks(anon_browser.contents):
-    ...     print 'Task:' + bugtask
+    ...     print('Task:' + bugtask)
     Task:...Test Bug 1...Undecided...New
     Task:...Test Bug 2...Undecided...New
 

=== modified file 'lib/lp/bugs/stories/bugtask-searches/xx-distribution-statistics-portlet.txt'
--- lib/lp/bugs/stories/bugtask-searches/xx-distribution-statistics-portlet.txt	2010-10-05 22:04:38 +0000
+++ lib/lp/bugs/stories/bugtask-searches/xx-distribution-statistics-portlet.txt	2018-06-30 16:32:46 +0000
@@ -11,45 +11,45 @@
     >>> from lp.testing.service_usage_helpers import set_service_usage
     >>> set_service_usage('debian', bug_tracking_usage='LAUNCHPAD')
 
-    >>> print http(r"""
+    >>> print(http(br"""
     ... GET /debian/+bugs?field.status%3Alist=New&field.status%3Alist=Confirmed&field.importance%3Alist=Critical&search=Search HTTP/1.1
     ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
-    ... """)
+    ... """))
     HTTP/1.1 200 Ok
     ...No results for search...
 
 Viewing bugs "assigned to me", as Sample Person:
 
-    >>> print http(r"""
+    >>> print(http(br"""
     ... GET /debian/+bugs?field.status%3Alist=New&field.status%3Alist=Confirmed&field.assignee=name12&search=Search HTTP/1.1
     ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
-    ... """)
+    ... """))
     HTTP/1.1 200 Ok
     ...1 result...
 
 Viewing untriaged bugs as Sample Person:
 
-    >>> print http(r"""
+    >>> print(http(br"""
     ... GET /debian/+bugs?field.status%3Alist=New&search=Search HTTP/1.1
     ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
-    ... """)
+    ... """))
     HTTP/1.1 200 Ok
     ...1 result...
 
 Viewing unassigned bugs as Sample Person:
 
-    >>> print http(r"""
+    >>> print(http(br"""
     ... GET /debian/+bugs?field.status%3Alist=New&field.status%3Alist=Confirmed&field.status-empty-marker=1&field.importance-empty-marker=1&field.assignee=&assignee_option=none&search=Search HTTP/1.1
     ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
-    ... """)
+    ... """))
     HTTP/1.1 200 Ok
     ...2 results...
 
 Viewing open reported bugs as Sample Person:
 
-    >>> print http(r"""
+    >>> print(http(br"""
     ... GET /debian/+bugs?search=Search HTTP/1.1
     ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
-    ... """)
+    ... """))
     HTTP/1.1 200 Ok
     ...3 results...

=== modified file 'lib/lp/bugs/stories/bugtask-searches/xx-listing-basics.txt'
--- lib/lp/bugs/stories/bugtask-searches/xx-listing-basics.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/bugs/stories/bugtask-searches/xx-listing-basics.txt	2018-06-30 16:32:46 +0000
@@ -183,7 +183,7 @@
 instead of displaying an empty table.
 
     >>> user_browser.open("http://launchpad.dev/firefox/+bugs?field.searchtext=fdsadsf&search=Search&advanced=&milestone=1&status=10&status=20&assignee=all";)
-    >>> print user_browser.contents
+    >>> print(user_browser.contents)
     <...
     ...No results for search...
     ...
@@ -193,7 +193,7 @@
 
     >>> set_service_usage('iso-codes', bug_tracking_usage='LAUNCHPAD')
     >>> user_browser.open("http://launchpad.dev/iso-codes/+bugs";)
-    >>> print user_browser.contents
+    >>> print(user_browser.contents)
     <...
     ...There are currently no open bugs...
     ...
@@ -209,7 +209,7 @@
     ...         badge_cell = row.find(None, {'class': 'bug-related-icons'})
     ...         spans = badge_cell.findAll('span')
     ...         for span in spans:
-    ...            print "  Badge:", span.get('alt')
+    ...            print("  Badge:", span.get('alt'))
 
 For instance, these are the badges on the firefox bug listing:
 
@@ -225,7 +225,7 @@
     >>> browser.open(
     ...     'http://bugs.launchpad.dev/debian/sarge/+source/mozilla-firefox')
     >>> milestone = find_tags_by_class(browser.contents, 'sprite milestone')
-    >>> print milestone[0]
+    >>> print(milestone[0])
     <a href="http://launchpad.dev/debian/+milestone/3.1"; alt="milestone 3.1"
     title="Linked to milestone 3.1" class="sprite milestone"></a>
 

=== modified file 'lib/lp/bugs/stories/bugtask-searches/xx-old-urls-still-work.txt'
--- lib/lp/bugs/stories/bugtask-searches/xx-old-urls-still-work.txt	2012-10-02 06:36:44 +0000
+++ lib/lp/bugs/stories/bugtask-searches/xx-old-urls-still-work.txt	2018-06-30 16:32:46 +0000
@@ -28,14 +28,14 @@
     ...         query_string_after, keep_blank_values=True)
     ...     for term_before, term_after in zip(query_before, query_after):
     ...         if term_before != term_after:
-    ...             print '%s --> %s' % (
+    ...             print('%s --> %s' % (
     ...                 '='.join(term_before),
-    ...                 '='.join(term_after))
+    ...                 '='.join(term_after)))
 
     >>> from lazr.uri import URI
     >>> uri = URI(anon_browser.url)
 
-    >>> print uri.path
+    >>> print(uri.path)
     /ubuntu/+bugs
 
     >>> print_query_changes(query, uri.query)
@@ -63,7 +63,7 @@
     >>> anon_browser.open(url)
 
     >>> uri = URI(anon_browser.url)
-    >>> print uri.path
+    >>> print(uri.path)
     /bugs/+bugs
 
     >>> print_query_changes(query, uri.query)
@@ -95,7 +95,7 @@
     >>> anon_browser.open(url)
 
     >>> uri = URI(anon_browser.url)
-    >>> print uri.path
+    >>> print(uri.path)
     /~mark/+assignedbugs
 
     >>> print_query_changes(query, uri.query)
@@ -112,7 +112,7 @@
     >>> anon_browser.open(url)
 
     >>> uri = URI(anon_browser.url)
-    >>> print uri.path
+    >>> print(uri.path)
     /~mark/+reportedbugs
 
     >>> print_query_changes(query, uri.query)
@@ -130,7 +130,7 @@
     >>> anon_browser.open(url)
 
     >>> uri = URI(anon_browser.url)
-    >>> print uri.path
+    >>> print(uri.path)
     /~mark/+subscribedbugs
 
     >>> print_query_changes(query, uri.query)

=== modified file 'lib/lp/bugs/stories/bugtask-searches/xx-person-bugs.txt'
--- lib/lp/bugs/stories/bugtask-searches/xx-person-bugs.txt	2012-02-22 22:17:46 +0000
+++ lib/lp/bugs/stories/bugtask-searches/xx-person-bugs.txt	2018-06-30 16:32:46 +0000
@@ -6,10 +6,10 @@
 
     >>> anon_browser.open('http://launchpad.dev/~name12')
     >>> anon_browser.getLink('Bugs').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Bugs : Sample Person
 
-    >>> print anon_browser.url
+    >>> print(anon_browser.url)
     http://bugs.launchpad.dev/~name12
 
 Note that we may see each bug more than once in case it's reported
@@ -48,7 +48,7 @@
 virtual host:
 
     >>> anon_browser.open('http://bugs.launchpad.dev/~name12')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Bugs : Sample Person
 
 
@@ -63,10 +63,10 @@
 .............
 
     >>> anon_browser.getLink('Assigned bugs').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Assigned bugs : Bugs : Sample Person
 
-    >>> print anon_browser.url
+    >>> print(anon_browser.url)
     http://bugs.launchpad.dev/~name12/+assignedbugs
 
     >>> print_bugtasks(anon_browser.contents)
@@ -82,10 +82,10 @@
 ..............
 
     >>> anon_browser.getLink('Commented bugs').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Commented bugs : Bugs : Sample Person
 
-    >>> print anon_browser.url
+    >>> print(anon_browser.url)
     http://bugs.launchpad.dev/~name12/+commentedbugs
 
 No Privileges Person has commented on two bugs, which will be returned
@@ -105,10 +105,10 @@
 .............
 
     >>> anon_browser.getLink('Reported bugs').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Reported bugs : Bugs : Sample Person
 
-    >>> print anon_browser.url
+    >>> print(anon_browser.url)
     http://bugs.launchpad.dev/~name12/+reportedbugs
 
     >>> print_bugtasks(anon_browser.contents)
@@ -136,10 +136,10 @@
 ...............
 
     >>> anon_browser.getLink('Subscribed bugs').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Subscribed bugs : Bugs : Sample Person
 
-    >>> print anon_browser.url
+    >>> print(anon_browser.url)
     http://bugs.launchpad.dev/~name12/+subscribedbugs
 
     >>> print_bugtasks(anon_browser.contents)
@@ -164,25 +164,25 @@
 all the menu links point to the bugs site.
 
     >>> anon_browser.open('http://launchpad.dev/~name12/+assignedbugs')
-    >>> print anon_browser.getLink('Commented bugs').url
+    >>> print(anon_browser.getLink('Commented bugs').url)
     http://bugs.launchpad.dev/~name12/+commentedbugs
 
-    >>> print anon_browser.getLink('Reported bugs').url
+    >>> print(anon_browser.getLink('Reported bugs').url)
     http://bugs.launchpad.dev/~name12/+reportedbugs
 
-    >>> print anon_browser.getLink('Subscribed bugs').url
+    >>> print(anon_browser.getLink('Subscribed bugs').url)
     http://bugs.launchpad.dev/~name12/+subscribedbugs
 
-    >>> print anon_browser.getLink('All related bugs').url
+    >>> print(anon_browser.getLink('All related bugs').url)
     http://bugs.launchpad.dev/~name12
 
-    >>> print anon_browser.getLink('Subscribed packages').url
+    >>> print(anon_browser.getLink('Subscribed packages').url)
     http://bugs.launchpad.dev/~name12/+packagebugs
 
     >>> anon_browser.open('http://launchpad.dev/~name12/+commentedbugs')
-    >>> print anon_browser.getLink('Assigned bugs').url
+    >>> print(anon_browser.getLink('Assigned bugs').url)
     http://bugs.launchpad.dev/~name12/+assignedbugs
 
-    >>> print anon_browser.getLink('Affecting bugs').url
+    >>> print(anon_browser.getLink('Affecting bugs').url)
     http://bugs.launchpad.dev/~name12/+affectingbugs
 

=== modified file 'lib/lp/bugs/stories/bugtask-searches/xx-unexpected-form-data.txt'
--- lib/lp/bugs/stories/bugtask-searches/xx-unexpected-form-data.txt	2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/stories/bugtask-searches/xx-unexpected-form-data.txt	2018-06-30 16:32:46 +0000
@@ -24,7 +24,7 @@
 When a UnexpectedFormData is raised, we display a custom error page to the
 user.
 
-    >>> output = str(http(r"""
+    >>> output = str(http(br"""
     ... GET /ubuntu/+bugs?search=Search&field.status=Fred HTTP/1.1
     ... """))
     >>> 'HTTP/1.1 500 Internal Server Error' in output

=== modified file 'lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt'
--- lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt	2012-12-11 05:41:50 +0000
+++ lib/lp/bugs/stories/bugtracker/bugtrackers-index.txt	2018-06-30 16:32:46 +0000
@@ -5,7 +5,7 @@
 page, with the addition of a breadcrumb itself.
 
     >>> user_browser.open('http://launchpad.dev/bugs/bugtrackers')
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Bug trackers registered in Launchpad
 
 The page presents a table with all bugtrackers currently registered:
@@ -14,14 +14,14 @@
     ...     table = find_tag_by_id(browser.contents, 'trackers')
     ...     for row in table.tbody.findAll('tr'):
     ...         title, location, linked, type, watches = row.findAll('td')
-    ...         print '------------------------'
-    ...         print '\n'.join([
+    ...         print('------------------------')
+    ...         print('\n'.join([
     ...             extract_text(title),
     ...             extract_text(location),
     ...             '  --> %s' % (location.a and location.a.get('href')),
     ...             ' '.join(extract_text(linked).split()),
     ...             extract_text(type),
-    ...             extract_text(watches)])
+    ...             extract_text(watches)]))
 
     >>> print_tracker_table(user_browser)
     ------------------------
@@ -76,7 +76,7 @@
     >>> user_browser.getLink("Debian Bug tracker").click()
     >>> nav = find_tags_by_class(user_browser.contents,
     ...     'batch-navigation-index')
-    >>> print extract_text(nav[0])
+    >>> print(extract_text(nav[0]))
     1 &rarr; 5 of 5 results
 
 The listing also displays projects and projects linked to bug trackers.

=== modified file 'lib/lp/bugs/stories/bugtracker/xx-bugtracker-remote-bug.txt'
--- lib/lp/bugs/stories/bugtracker/xx-bugtracker-remote-bug.txt	2012-07-17 14:29:17 +0000
+++ lib/lp/bugs/stories/bugtracker/xx-bugtracker-remote-bug.txt	2018-06-30 16:32:46 +0000
@@ -24,7 +24,7 @@
   * Answers - http://answers.launchpad.dev/
   Main heading: Remote Bug #42 in The Mozilla.org Bug Tracker
 
-  >>> print extract_text(find_tag_by_id(browser.contents, 'watchedbugs'))
+  >>> print(extract_text(find_tag_by_id(browser.contents, 'watchedbugs')))
   Bug #1: Firefox does not support SVG
   Bug #2: Blackhole Trash folder
 
@@ -32,7 +32,7 @@
 the list page and redirect the user directly to that bug's page:
 
   >>> browser.open('http://launchpad.dev/bugs/bugtrackers/mozilla.org/2000')
-  >>> print browser.url
+  >>> print(browser.url)
   http://bugs.launchpad.dev/firefox/+bug/1
 
 If there are no bug watches for a particular remote bug, then a Not
@@ -77,14 +77,14 @@
   * Answers - http://answers.launchpad.dev/
   Main heading: Remote Bug #42 in The Mozilla.org Bug Tracker
 
-  >>> print extract_text(find_tag_by_id(anon_browser.contents, 'watchedbugs'))
+  >>> print(extract_text(find_tag_by_id(anon_browser.contents, 'watchedbugs')))
   Bug #1: (Private)
   Bug #2: Blackhole Trash folder
 
 The bug title is still provided if the user can view the private bug:
 
   >>> browser.open('http://launchpad.dev/bugs/bugtrackers/mozilla.org/42')
-  >>> print extract_text(find_tag_by_id(browser.contents, 'watchedbugs'))
+  >>> print(extract_text(find_tag_by_id(browser.contents, 'watchedbugs')))
   Bug #1: Firefox does not support SVG
   Bug #2: Blackhole Trash folder
 

=== modified file 'lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt'
--- lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt	2016-07-04 17:11:29 +0000
+++ lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt	2018-06-30 16:32:46 +0000
@@ -15,7 +15,7 @@
     >>> user_browser.url
     'http://bugs.launchpad.dev/bugs/bugtrackers/+newbugtracker'
 
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Register an external bug tracker...
 
 In case the user gets cold feet, there is always a cancel link that
@@ -30,7 +30,7 @@
 requires manual set up of a bug archive mirror.
 
     >>> for control in user_browser.getControl('Bug Tracker Type').controls:
-    ...     print control.optionValue
+    ...     print(control.optionValue)
     Bugzilla
     Roundup
     Trac
@@ -60,7 +60,7 @@
     'http://bugs.launchpad.dev/bugs/bugtrackers/+newbugtracker'
 
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     There is 1 error.
     Invalid name 'testmantis!'.  Names must be at least two characters ...
 
@@ -76,7 +76,7 @@
     'http://bugs.launchpad.dev/bugs/bugtrackers/+newbugtracker'
 
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     There is 1 error.
     http://bugzilla.mozilla.org/ is already registered in Launchpad
     as &quot;The Mozilla.org Bug Tracker&quot; (mozilla.org).
@@ -103,7 +103,7 @@
     'http://bugs.launchpad.dev/bugs/bugtrackers/+newbugtracker'
 
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     There is 1 error.
     http://alias.example.com/ is already registered in Launchpad
     as &quot;GnomeGBug GTracker&quot; (gnome-bugzilla).
@@ -117,7 +117,7 @@
     >>> user_browser.url
     'http://bugs.launchpad.dev/bugs/bugtrackers/testmantis'
 
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Test Mantis Tracker : Bug trackers
 
     >>> 'Test Mantis Tracker' in user_browser.contents
@@ -136,10 +136,10 @@
     >>> anon_bugtracker_url_list = find_tag_by_id(
     ...     anon_browser.contents, 'bugtracker-urls')
 
-    >>> print extract_text(user_bugtracker_url_list)
+    >>> print(extract_text(user_bugtracker_url_list))
     mailto:bugs@xxxxxxxxxxx
 
-    >>> print extract_text(anon_bugtracker_url_list)
+    >>> print(extract_text(anon_bugtracker_url_list))
     mailto:&lt;email address hidden&gt;
 
 The `Summary` and `Contact Details` fields are optional - creating a
@@ -193,7 +193,7 @@
     >>> user_browser.url
     'http://bugs.launchpad.dev/bugs/bugtrackers/testmantis/+edit'
 
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Change details for the...
 
     >>> user_browser.getControl('Name').value = 'testbugzilla'
@@ -235,7 +235,7 @@
     &quot;what? my wife does this stuff&quot; is not a valid URI
 
     >>> user_browser.getControl('Location', index=0).value = (
-    ...     'http://ξνεr.been.fishing?')
+    ...     b'http://ξνεr.been.fishing?')
     >>> user_browser.getControl('Change').click()
 
     >>> print_feedback_messages(user_browser.contents)
@@ -272,8 +272,8 @@
 
     >>> user_browser.open(
     ...     'http://launchpad.dev/bugs/bugtrackers/testbugzilla')
-    >>> print extract_text(find_tag_by_id(
-    ...         user_browser.contents, 'bugtracker-urls'))
+    >>> print(extract_text(find_tag_by_id(
+    ...         user_browser.contents, 'bugtracker-urls')))
     http://trac.example.org/tickets
     http://mantis.testing.org/ (Alias)
 
@@ -282,8 +282,8 @@
     ...     'https://trac.example.org/tickets')
     >>> user_browser.getControl('Change').click()
 
-    >>> print extract_text(find_tag_by_id(
-    ...         user_browser.contents, 'bugtracker-urls'))
+    >>> print(extract_text(find_tag_by_id(
+    ...         user_browser.contents, 'bugtracker-urls')))
     https://trac.example.org/tickets
     http://mantis.testing.org/ (Alias)
 
@@ -308,7 +308,7 @@
 
     >>> bugtracker_url_list = find_tag_by_id(
     ...     user_browser.contents, 'bugtracker-urls')
-    >>> print extract_text(bugtracker_url_list)
+    >>> print(extract_text(bugtracker_url_list))
     https://trac.example.org/tickets
     http://pseudonym.example.com/ (Alias)
 
@@ -337,7 +337,7 @@
 
     >>> bugtracker_url_list = find_tag_by_id(
     ...     user_browser.contents, 'bugtracker-urls')
-    >>> print extract_text(bugtracker_url_list)
+    >>> print(extract_text(bugtracker_url_list))
     https://trac.example.org/tickets
     http://toadhall.example.com/ (Alias)
     http://wolverhampton.example.com/ (Alias)
@@ -350,7 +350,7 @@
     >>> user_browser.open(
     ...     'http://launchpad.dev/bugs/bugtrackers/testbugzilla/+edit')
     >>> user_browser.getControl('Location aliases').value = (
-    ...     'ξνεr been http://fishing?')
+    ...     b'ξνεr been http://fishing?')
     >>> user_browser.getControl('Change').click()
 
     >>> print_feedback_messages(user_browser.contents)
@@ -414,8 +414,8 @@
 
     >>> user_browser.open(
     ...     'http://launchpad.dev/bugs/bugtrackers/gnome-bugzilla/+edit')
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'bugtracker-delete-not-possible-reasons'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'bugtracker-delete-not-possible-reasons')))
     Please note, this bug tracker cannot be deleted because:
       This is the bug tracker for GNOME and GNOME Terminal.
       There are linked bug watches and only members of ...Launchpad
@@ -424,7 +424,7 @@
     >>> user_browser.getControl('Delete')
     Traceback (most recent call last):
     ...
-    LookupError: label 'Delete'
+    LookupError: label u'Delete'
 
 Note how we tell the user about _all_ the restrictions they face. In
 this instance the user would have the option of persuading the GNOME
@@ -439,8 +439,8 @@
     >>> user_browser.open(
     ...     'http://launchpad.dev/bugs/bugtrackers/debbugs/+edit')
 
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'bugtracker-delete-not-possible-reasons'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'bugtracker-delete-not-possible-reasons')))
     Please note, this bug tracker cannot be deleted because:
       There are linked bug watches and only members of ...Launchpad
         Administrators...
@@ -448,7 +448,7 @@
     >>> user_browser.getControl('Delete')
     Traceback (most recent call last):
     ...
-    LookupError: label 'Delete'
+    LookupError: label u'Delete'
 
 Again, we tell the user about all the restrictions they have stumbled
 on. A more privileged user would not stumble at the second hurdle,
@@ -456,8 +456,8 @@
 
     >>> admin_browser.open(
     ...     'http://launchpad.dev/bugs/bugtrackers/debbugs/+edit')
-    >>> print extract_text(find_tag_by_id(
-    ...     admin_browser.contents, 'bugtracker-delete-not-possible-reasons'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     admin_browser.contents, 'bugtracker-delete-not-possible-reasons')))
     Please note, this bug tracker cannot be deleted because:
       Bug comments have been imported via this bug tracker.
       This bug tracker is protected from deletion.
@@ -465,7 +465,7 @@
     >>> admin_browser.getControl('Delete')
     Traceback (most recent call last):
     ...
-    LookupError: label 'Delete'
+    LookupError: label u'Delete'
 
 
 Disabling a bug tracker
@@ -481,7 +481,7 @@
     >>> user_browser.getControl(name='field.active')
     Traceback (most recent call last):
       ...
-    LookupError: name 'field.active'
+    LookupError: name u'field.active'
 
 But admins can.
 
@@ -491,7 +491,7 @@
     >>> admin_browser.getControl('Change').click()
 
     >>> message = find_tag_by_id(admin_browser.contents, 'inactive-message')
-    >>> print extract_text(message)
+    >>> print(extract_text(message))
     Bug watch updates for Debian Bug tracker are disabled.
 
 If a user looks at a disabled bug tracker they'll see a message
@@ -499,7 +499,7 @@
 
     >>> user_browser.open('http://launchpad.dev/bugs/bugtrackers/debbugs')
     >>> message = find_tag_by_id(user_browser.contents, 'inactive-message')
-    >>> print extract_text(message)
+    >>> print(extract_text(message))
     Bug watch updates for Debian Bug tracker are disabled.
 
 And if the users views a bug with a watch against a disabled bug tracker
@@ -516,7 +516,7 @@
     >>> user_browser.open('http://launchpad.dev/bugs/bugtrackers')
     >>> inactive_trackers_table = find_tag_by_id(
     ...     user_browser.contents, 'inactive-trackers')
-    >>> print extract_text(inactive_trackers_table)
+    >>> print(extract_text(inactive_trackers_table))
     Title               Location...
     Debian Bug tracker  http://bugs.debian.org...
 
@@ -528,14 +528,14 @@
     >>> admin_browser.getControl('Change').click()
 
     >>> message = find_tag_by_id(user_browser.contents, 'inactive-message')
-    >>> print message
+    >>> print(message)
     None
 
 The user will no longer see any messages.
 
     >>> user_browser.open('http://launchpad.dev/bugs/bugtrackers/debbugs')
     >>> message = find_tag_by_id(user_browser.contents, 'inactive-message')
-    >>> print message
+    >>> print(message)
     None
 
 The message won't appear on the bug pages either.
@@ -548,7 +548,7 @@
 
     >>> inactive_trackers_table = find_tag_by_id(
     ...     user_browser.contents, 'inactive-trackers')
-    >>> print inactive_trackers_table
+    >>> print(inactive_trackers_table)
     None
 
 
@@ -559,8 +559,8 @@
 
     >>> anon_browser.open(
     ...     'http://launchpad.dev/bugs/bugtrackers/debbugs')
-    >>> print extract_text(find_tag_by_id(
-    ...     anon_browser.contents, 'latestwatches'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     anon_browser.contents, 'latestwatches')))
     Launchpad bug  Remote bug  Status  Last check  Next check
     #15: Nonse...  308994      open...
     #3:  Bug T...  327549
@@ -583,8 +583,8 @@
 
     >>> anon_browser.open(
     ...     'http://launchpad.dev/bugs/bugtrackers/debbugs')
-    >>> print extract_text(find_tag_by_id(
-    ...     anon_browser.contents, 'latestwatches'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     anon_browser.contents, 'latestwatches')))
     Launchpad bug  Remote bug  Status  Last check  Next check
     #15: Nonse...  308994      open... 2007-12-18    2010-04-09 09:50:00 UTC
     #3:  Bug T...  327549
@@ -607,8 +607,8 @@
 
     >>> anon_browser.open(
     ...     'http://launchpad.dev/bugs/bugtrackers/debbugs')
-    >>> print extract_text(find_tag_by_id(
-    ...     anon_browser.contents, 'latestwatches'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     anon_browser.contents, 'latestwatches')))
     Launchpad bug                      Remote bug  Status...
     #15: Nonse...                      308994...
     #3:  (Private)                     -
@@ -663,10 +663,10 @@
     ...     for watch in watches:
     ...         bug, remote_bug, status, last_checked, next_check = (
     ...             watch.findAll('td'))
-    ...         print extract_text(bug)
-    ...         print '  --> %s: %s' % (
+    ...         print(extract_text(bug))
+    ...         print('  --> %s: %s' % (
     ...             extract_text(remote_bug),
-    ...             (remote_bug.a and remote_bug.a.get('href')))
+    ...             (remote_bug.a and remote_bug.a.get('href'))))
 
     >>> user_browser.open(
     ...     'http://launchpad.dev/bugs/bugtrackers/email')
@@ -689,8 +689,8 @@
 bug tracker page.
 
     >>> user_browser.open('http://launchpad.dev/bugs/bugtrackers/email')
-    >>> print extract_text(find_portlet(
-    ...     user_browser.contents, 'Details'))
+    >>> print(extract_text(find_portlet(
+    ...     user_browser.contents, 'Details')))
     Details
     Location:
     mailto:bugs@xxxxxxxxxxx
@@ -703,8 +703,8 @@
 above are obfuscated:
 
     >>> anon_browser.open('http://launchpad.dev/bugs/bugtrackers/email')
-    >>> print extract_text(find_portlet(
-    ...     anon_browser.contents, 'Details'))
+    >>> print(extract_text(find_portlet(
+    ...     anon_browser.contents, 'Details')))
     Details
     Location:
     mailto:&lt;email address hidden&gt;
@@ -714,8 +714,8 @@
 
     >>> anon_browser.open(
     ...     'http://bugs.launchpad.dev/bugs/bugtrackers/gnome-bugzilla')
-    >>> print extract_text(find_portlet(
-    ...     anon_browser.contents, 'Details'))
+    >>> print(extract_text(find_portlet(
+    ...     anon_browser.contents, 'Details')))
     Details
     Location:
     http://bugzilla.gnome.org/bugs

=== modified file 'lib/lp/bugs/stories/bugtracker/xx-reschedule-all-watches.txt'
--- lib/lp/bugs/stories/bugtracker/xx-reschedule-all-watches.txt	2012-01-15 13:32:27 +0000
+++ lib/lp/bugs/stories/bugtracker/xx-reschedule-all-watches.txt	2018-06-30 16:32:46 +0000
@@ -25,7 +25,7 @@
     >>> user_browser.getControl('Reschedule all watches')
     Traceback (most recent call last):
       ...
-    LookupError: label 'Reschedule all watches'
+    LookupError: label u'Reschedule all watches'
 
 However, the reschedule button will appear to administrators.
 
@@ -60,12 +60,12 @@
 checking at some future date.
 
     >>> reschedule_button.click()
-    >>> print lp_dev_browser.url
+    >>> print(lp_dev_browser.url)
     http://bugs.launchpad.dev/bugs/bugtrackers/our-bugtracker
 
     >>> for message in find_tags_by_class(
     ...     lp_dev_browser.contents, 'informational message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     All bug watches on Our BugTracker have been rescheduled.
 
 If we look at the bug watch on our bugtracker we can see that it has
@@ -75,7 +75,7 @@
     >>> from pytz import utc
 
     >>> login(ADMIN_EMAIL)
-    >>> print bug_watch.next_check >= datetime.now(utc)
+    >>> print(bug_watch.next_check >= datetime.now(utc))
     True
 
 Should the bug watch be deleted the reschedule button will no longer
@@ -89,4 +89,4 @@
     ...     'Reschedule all watches')
     Traceback (most recent call last):
       ...
-    LookupError: label 'Reschedule all watches'
+    LookupError: label u'Reschedule all watches'

=== modified file 'lib/lp/bugs/stories/bugwatches/xx-bugtask-bugwatch-linkage.txt'
--- lib/lp/bugs/stories/bugwatches/xx-bugtask-bugwatch-linkage.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/bugs/stories/bugwatches/xx-bugtask-bugwatch-linkage.txt	2018-06-30 16:32:46 +0000
@@ -18,7 +18,7 @@
 
 This means that we only display the status, it's not possible to edit it:
 
-    >>> print browser.contents
+    >>> print(browser.contents)
     <!DOCTYPE...
 
     <label for="debian_mozilla-firefox.status">Status</label>
@@ -29,19 +29,19 @@
     >>> status_control = browser.getControl(name='debian_mozilla-firefox.status')
     Traceback (most recent call last):
     ...
-    LookupError: name 'debian_mozilla-firefox.status'
+    LookupError: name u'debian_mozilla-firefox.status'
 
 Of course we can't edit the importance or assignee either.
 
     >>> browser.getControl(name='debian_mozilla-firefox.importance')
     Traceback (most recent call last):
     ...
-    LookupError: name 'debian_mozilla-firefox.importance'
+    LookupError: name u'debian_mozilla-firefox.importance'
 
     >>> browser.getControl(name='debian_mozilla-firefox.assignee')
     Traceback (most recent call last):
     ...
-    LookupError: name 'debian_mozilla-firefox.assignee'
+    LookupError: name u'debian_mozilla-firefox.assignee'
 
 If we remove the bug watch, we'll be able to edit the status, which has
 been reset to New.
@@ -117,7 +117,7 @@
 
     >>> bugwatch_portlet = find_portlet(browser.contents, 'Remote bug watches')
     >>> for li_tag in bugwatch_portlet.findAll('li'):
-    ...     print li_tag.findAll('a')[0].string
+    ...     print(li_tag.findAll('a')[0].string)
     mozilla.org #123543
     mozilla.org #2000
     mozilla.org #42

=== modified file 'lib/lp/bugs/stories/bugwatches/xx-bugwatch-errors.txt'
--- lib/lp/bugs/stories/bugwatches/xx-bugwatch-errors.txt	2013-05-09 08:53:01 +0000
+++ lib/lp/bugs/stories/bugwatches/xx-bugwatch-errors.txt	2018-06-30 16:32:46 +0000
@@ -55,19 +55,19 @@
     >>> user_browser.open('http://bugs.launchpad.dev/thunderbird/+bug/12')
     >>> for tag in find_tags_by_class(user_browser.contents,
     ...     'error message'):
-    ...     print extract_text(tag.renderContents())
+    ...     print(extract_text(tag.renderContents()))
     The Mozilla.org Bug Tracker bug #900 appears not to exist. Check
     that the bug number is correct. (what does this mean?)
 
     >>> help_link = user_browser.getLink('(what does this mean?)')
-    >>> print help_link.url
+    >>> print(help_link.url)
     http://bugs.launchpad.dev/.../+error-help#BUG_NOT_FOUND
 
 It's also shown in the tooltip of the warning icon next to the bug watch
 in the bugtask table.
 
     >>> icon = find_tag_by_id(user_browser.contents, 'bugwatch-error-sprite')
-    >>> print icon.get('title')
+    >>> print(icon.get('title'))
     The Mozilla.org Bug Tracker bug #900 appears not to exist. Check
     that the bug number is correct.
 
@@ -86,7 +86,7 @@
     ...     user_browser.open('http://bugs.launchpad.dev/thunderbird/+bug/12')
     ...     for tag in find_tags_by_class(user_browser.contents,
     ...         'error message'):
-    ...         print extract_text(tag.renderContents())
+    ...         print(extract_text(tag.renderContents()))
     Launchpad couldn't import bug #900 from The Mozilla.org Bug
     Tracker...
     The Mozilla.org Bug Tracker bug #900 appears not to exist. Check

=== modified file 'lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt'
--- lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt	2016-02-05 16:51:12 +0000
+++ lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt	2018-06-30 16:32:46 +0000
@@ -10,7 +10,7 @@
     >>> admin_browser.getControl('Change').click()
     >>> admin_browser.url
     'http://bugs.launchpad.dev/firefox/+bug/1'
-    >>> 'https://bugzilla.mozilla.org/show_bug.cgi?id=1000' in (
+    >>> b'https://bugzilla.mozilla.org/show_bug.cgi?id=1000' in (
     ...     admin_browser.contents)
     True
 
@@ -24,7 +24,7 @@
     >>> admin_browser.url
     'http://bugs.launchpad.dev/bugs/1/+watch/2/+edit'
     >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     There is 1 error.
     Invalid bug tracker URL.
 
@@ -35,7 +35,7 @@
     >>> admin_browser.url
     'http://bugs.launchpad.dev/bugs/1/+watch/2/+edit'
     >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     There is 1 error.
     &quot;GELLER&quot; is not a valid URI
 
@@ -47,7 +47,7 @@
 
     >>> for data_tag in find_tags_by_class(
     ...     admin_browser.contents, 'bugwatch-data'):
-    ...     print extract_text(data_tag.renderContents())
+    ...     print(extract_text(data_tag.renderContents()))
     Tracker: The Mozilla.org Bug Tracker
     Remote bug ID: 1000
     Last status: None recorded
@@ -77,7 +77,7 @@
     >>> admin_browser.open('http://bugs.launchpad.dev/bugs/1/+watch/2')
     >>> data_tag = find_tag_by_id(
     ...     admin_browser.contents, 'bugwatch-next_check')
-    >>> print extract_text(data_tag.renderContents())
+    >>> print(extract_text(data_tag.renderContents()))
     Next check: 2010-04-08...
 
 
@@ -91,7 +91,7 @@
     >>> user_browser.open('http://bugs.launchpad.dev/bugs/1/+watch/2')
     >>> recent_activity_list = find_tag_by_id(
     ...     user_browser.contents, 'recent-watch-activity')
-    >>> print recent_activity_list
+    >>> print(recent_activity_list)
     None
 
 Adding some activity to the watch will cause it to show up in the recent
@@ -105,7 +105,7 @@
     >>> user_browser.open('http://bugs.launchpad.dev/bugs/1/+watch/2')
     >>> recent_activity_list = find_tag_by_id(
     ...     user_browser.contents, 'recent-watch-activity')
-    >>> print extract_text(recent_activity_list)
+    >>> print(extract_text(recent_activity_list))
     Update completed successfully ... ago
 
 If an update fails, that too will be reflected in the list.
@@ -119,7 +119,7 @@
     >>> user_browser.open('http://bugs.launchpad.dev/bugs/1/+watch/2')
     >>> recent_activity_list = find_tag_by_id(
     ...     user_browser.contents, 'recent-watch-activity')
-    >>> print extract_text(recent_activity_list)
+    >>> print(extract_text(recent_activity_list))
     Update failed with error 'Bug Not Found' ... ago
     Update completed successfully ... ago
 
@@ -136,7 +136,7 @@
     >>> user_browser.open('http://bugs.launchpad.dev/bugs/1/+watch/2')
     >>> recent_activity_list = find_tag_by_id(
     ...     user_browser.contents, 'recent-watch-activity')
-    >>> print extract_text(recent_activity_list)
+    >>> print(extract_text(recent_activity_list))
     Update failed with error 'Unable to import...' (OOPS-12345TEST) ... ago
     Update failed with error 'Bug Not Found' ... ago
     Update completed successfully ... ago
@@ -145,7 +145,7 @@
 
     >>> admin_browser.open('http://bugs.launchpad.dev/bugs/1/+watch/2')
     >>> oops_link = admin_browser.getLink('OOPS-12345TEST')
-    >>> print oops_link.url
+    >>> print(oops_link.url)
     http...OOPS-12345TEST
 
 
@@ -169,7 +169,7 @@
     >>> user_browser.getControl('Update Now')
     Traceback (most recent call last):
       ...
-    LookupError: label 'Update Now'
+    LookupError: label u'Update Now'
 
 If the watch has been checked but has never failed, the button will
 remain hidden.
@@ -182,7 +182,7 @@
     >>> user_browser.getControl('Update Now')
     Traceback (most recent call last):
       ...
-    LookupError: label 'Update Now'
+    LookupError: label u'Update Now'
 
 If the watch has failed less than 60% of its recent checks, the button
 will appear on the page.
@@ -196,7 +196,7 @@
 
     >>> data_tag = find_tag_by_id(
     ...     user_browser.contents, 'bugwatch-next_check')
-    >>> print extract_text(data_tag.renderContents())
+    >>> print(extract_text(data_tag.renderContents()))
     Next check: Not yet scheduled
 
 Clicking the Update Now button will schedule it to be checked
@@ -206,7 +206,7 @@
 
     >>> for message in find_tags_by_class(
     ...     user_browser.contents, 'informational message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     The ... bug watch has been scheduled for immediate checking.
 
 Looking at the watch +edit page again, we can see that the watch has
@@ -215,7 +215,7 @@
     >>> user_browser.open(watch_url)
     >>> data_tag = find_tag_by_id(
     ...     user_browser.contents, 'bugwatch-next_check')
-    >>> print extract_text(data_tag.renderContents())
+    >>> print(extract_text(data_tag.renderContents()))
     Next check: 2...
 
 The button will no longer be shown on the page.
@@ -223,7 +223,7 @@
     >>> reschedule_button = user_browser.getControl('Update Now')
     Traceback (most recent call last):
       ...
-    LookupError: label 'Update Now'
+    LookupError: label u'Update Now'
 
 If a watch has run once and failed once, the reschedule button will be
 shown.
@@ -243,7 +243,7 @@
 
     >>> for message in find_tags_by_class(
     ...     user_browser.contents, 'informational message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     The ... bug watch has been scheduled for immediate checking.
 
 However, once the watch succeeds the button will disappear, even though
@@ -263,7 +263,7 @@
     >>> user_browser.getControl('Update Now')
     Traceback (most recent call last):
       ...
-    LookupError: label 'Update Now'
+    LookupError: label u'Update Now'
 
 
 Resetting a watch
@@ -313,12 +313,12 @@
     >>> reset_button.click()
     >>> for message in find_tags_by_class(
     ...     lp_dev_browser.contents, 'informational message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     The ... bug watch has been reset.
 
     >>> data_tag = find_tag_by_id(
     ...     user_browser.contents, 'bugwatch-lastchecked')
-    >>> print extract_text(data_tag.renderContents())
+    >>> print(extract_text(data_tag.renderContents()))
     Checked:
 
 Should a non-admin, non-Launchpad-developer user visit the page, the
@@ -328,4 +328,4 @@
     >>> user_browser.getControl('Reset this watch')
     Traceback (most recent call last):
       ...
-    LookupError: label 'Reset this watch'
+    LookupError: label u'Reset this watch'

=== modified file 'lib/lp/bugs/stories/cve/cve-linking.txt'
--- lib/lp/bugs/stories/cve/cve-linking.txt	2017-10-23 00:16:39 +0000
+++ lib/lp/bugs/stories/cve/cve-linking.txt	2018-06-30 16:32:46 +0000
@@ -32,11 +32,11 @@
 title is linked
 
     >>> content = find_tag_by_id(user_browser.contents, 'related-bugs')
-    >>> print extract_text(content)
+    >>> print(extract_text(content))
     Related bugs and status ...
     Bug #5:...Firefox install instructions should be complete ...
 
-    >>> print content.a
+    >>> print(content.a)
     <a href=".../bugs/5" class="sprite bug">Bug #5: ...
 
 It is also possible to link a bug using its nickname. For example, bug
@@ -49,7 +49,7 @@
 An error message is displayed when user enters a non-existent nickname:
 
     >>> for tag in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     There is 1 error.
     <BLANKLINE>
     Not a valid bug number or nickname.
@@ -75,7 +75,7 @@
     >>> admin_browser.open('http://launchpad.dev/bugs/cve/1999-8979')
     >>> for form in find_main_content(
     ...     admin_browser.contents).findAll('form'):
-    ...     print form.renderContents()
+    ...     print(form.renderContents())
 
 Similarly, there should be no links allowing the user to mark the bug as
 affecting another product or distribution.
@@ -139,8 +139,8 @@
 
 The user will see that linked private bug:
 
-    >>> print extract_text(
-    ...     find_tag_by_id(admin_browser.contents, 'related-bugs'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'related-bugs')))
     Related bugs and status ...
     Bug #6: Firefox crashes when Save As dialog
     for a nonexistent window is closed ...
@@ -149,5 +149,5 @@
 bug) will not see the bug reference at all:
 
     >>> anon_browser.open('http://launchpad.dev/bugs/cve/2005-2737')
-    >>> print find_tag_by_id(anon_browser.contents, 'related-bugs')
+    >>> print(find_tag_by_id(anon_browser.contents, 'related-bugs'))
     None

=== modified file 'lib/lp/bugs/stories/cve/cve-pages.txt'
--- lib/lp/bugs/stories/cve/cve-pages.txt	2013-04-11 01:27:33 +0000
+++ lib/lp/bugs/stories/cve/cve-pages.txt	2018-06-30 16:32:46 +0000
@@ -8,7 +8,7 @@
     >>> browser.open('http://bugs.launchpad.dev/')
     >>> browser.url
     'http://bugs.launchpad.dev/'
-    >>> print find_tag_by_id(browser.contents, 'malone-stats')
+    >>> print(find_tag_by_id(browser.contents, 'malone-stats'))
     <...2...bugs are related to...CVE entries...
 
 "CVE entries" links to the main CVEs page, which has the same
@@ -16,11 +16,11 @@
 created or modified.
 
     >>> browser.getLink('CVE entries').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/bugs/cve
-    >>> print browser.title
+    >>> print(browser.title)
     Launchpad CVE tracker
-    >>> print browser.contents
+    >>> print(browser.contents)
     <...
     ...CVE-2005-2737 (Candidate)...
     ...CVE-2005-2736 (Candidate)...
@@ -28,22 +28,22 @@
 The main CVEs page links to a list of all registered CVEs.
 
     >>> browser.getLink('Show all registered CVEs').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.dev/bugs/cve/+all
-    >>> print browser.title
+    >>> print(browser.title)
     Launchpad CVE tracker
 
 Now, we will test the search functionality of the CVE tracker. First we
 will search for the word "loss" in the database and see how many show
 up. We expect to see CVE-1999-2345.
 
-    >>> print http(r"""
+    >>> print(http(br"""
     ... POST /bugs/cve/ HTTP/1.1
     ... Content-Length: 9
     ... Content-Type: application/x-www-form-urlencoded
     ... Referer: https://launchpad.dev/
     ...
-    ... text=loss""")
+    ... text=loss"""))
     HTTP/1.1 200 Ok
     ...Matches: 1...
     ...CVE-1999-2345 (Candidate)...
@@ -52,13 +52,13 @@
 Finally, let's put a CVE number in there. We expect to jump straight to that
 CVE.
 
-    >>> print http(r"""
+    >>> print(http(br"""
     ... POST /bugs/cve/ HTTP/1.1
     ... Content-Length: 14
     ... Content-Type: application/x-www-form-urlencoded
     ... Referer: https://launchpad.dev/
     ...
-    ... text=2005-2737""")
+    ... text=2005-2737"""))
     HTTP/1.1 303 See Other
     ...
     Location: http://.../bugs/cve/2005-2737
@@ -67,10 +67,10 @@
 A CVE page includes a link back to the main CVE page.
 
     >>> anon_browser.open('http://launchpad.dev/bugs/cve/2005-2737')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     CVE-2005-2737
     >>> back_link = anon_browser.getLink('Launchpad CVE tracker')
-    >>> print back_link.url
+    >>> print(back_link.url)
     http://launchpad.dev/bugs/cve
 
 The CVE page links to the related bugs.
@@ -81,7 +81,7 @@
     True
     >>> for tag in find_tags_by_class(
     ...     anon_browser.contents, 'menu-link-linkbug'):
-    ...     print tag
+    ...     print(tag)
     <a href="+linkbug" class="menu-link-linkbug sprite add">Link to bug</a>
     >>> 'Candidate' in anon_browser.contents
     True

=== modified file 'lib/lp/bugs/stories/cve/xx-cve-link-xss.txt'
--- lib/lp/bugs/stories/cve/xx-cve-link-xss.txt	2012-12-10 13:43:47 +0000
+++ lib/lp/bugs/stories/cve/xx-cve-link-xss.txt	2018-06-30 16:32:46 +0000
@@ -17,8 +17,8 @@
 
 Indeed, the markup is valid and correctly escaped:
 
-    >>> print find_tag_by_id(
-    ...     user_browser.contents, 'field.sequence').prettify()
+    >>> print(find_tag_by_id(
+    ...     user_browser.contents, 'field.sequence').prettify())
     <input class="textType" id="field.sequence"
            name="field.sequence" size="20" type="text"
            value='&lt;script&gt;alert("cheezburger");&lt;/script&gt;' />
@@ -26,7 +26,7 @@
 The error message is also valid and correctly escaped:
 
     >>> for tag in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print tag.prettify()
+    ...     print(tag.prettify())
     <p class="error message">
     There is 1 error.
     </p>

=== modified file 'lib/lp/bugs/stories/feeds/xx-bug-atom.txt'
--- lib/lp/bugs/stories/feeds/xx-bug-atom.txt	2017-05-17 06:45:49 +0000
+++ lib/lp/bugs/stories/feeds/xx-bug-atom.txt	2018-06-30 16:32:46 +0000
@@ -32,41 +32,41 @@
     'http://feeds.launchpad.dev/jokosher/latest-bugs.atom'
 
     >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
-    >>> print extract_text(soup.find('id'))
+    >>> print(extract_text(soup.find('id')))
     tag:launchpad.net,2007-03-15:/bugs/jokosher
     >>> alternate_links = parse_links(browser.contents, 'alternate')
     >>> for link in alternate_links:
-    ...     print link
+    ...     print(link)
     <link rel="alternate" href="http://bugs.launchpad.dev/jokosher"; />
     <link rel="alternate" href="http://bugs.launchpad.dev/bugs/12"; />
     <link rel="alternate" href="http://bugs.launchpad.dev/bugs/11"; />
 
     >>> self_links = parse_links(browser.contents, 'self')
     >>> for link in self_links:
-    ...     print link
+    ...     print(link)
     <link rel="self" href="http://feeds.launchpad.dev/jokosher/latest-bugs.atom"; />
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     2
     >>> entry = entries[0]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [12] Copy, Cut and Delete operations should work on selections
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Foo Bar
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name16
-    >>> print extract_text(entry.id)
+    >>> print(extract_text(entry.id))
     tag:launchpad.net,2007-03-15:/bugs/12
 
     >>> entry = entries[1]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [11] Make Jokosher use autoaudiosink
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Foo Bar
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name16
-    >>> print extract_text(entry.id)
+    >>> print(extract_text(entry.id))
     tag:launchpad.net,2007-03-15:/bugs/11
 
 The Atom feed must have the content-type of "application/atom+xml".
@@ -89,40 +89,40 @@
     'http://feeds.launchpad.dev/mozilla/latest-bugs.atom'
 
     >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
-    >>> print extract_text(soup.find('id'))
+    >>> print(extract_text(soup.find('id')))
     tag:launchpad.net,2004-09-24:/bugs/mozilla
 
     >>> self_links = parse_links(browser.contents, 'self')
     >>> for link in self_links:
-    ...     print link
+    ...     print(link)
     <link rel="self" href="http://feeds.launchpad.dev/mozilla/latest-bugs.atom"; />
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     5
 
     >>> entry = entries[0]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [15] Nonsensical bugs are useless
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Foo Bar
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name16
 
     >>> entry = entries[1]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [9] Thunderbird crashes
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Foo Bar
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name16
 
     >>> entry = entries[2]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [5] Firefox install instructions should be complete
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Sample Person
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name12
 
 Ensure the entries are in reverse chronological order by published date.
@@ -150,24 +150,24 @@
     'http://feeds.launchpad.dev/ubuntu/latest-bugs.atom'
 
     >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
-    >>> print extract_text(soup.find('id'))
+    >>> print(extract_text(soup.find('id')))
     tag:launchpad.net,2006-10-16:/bugs/ubuntu
 
     >>> self_links = parse_links(browser.contents, 'self')
     >>> for link in self_links:
-    ...     print link
+    ...     print(link)
     <link rel="self" href="http://feeds.launchpad.dev/ubuntu/latest-bugs.atom"; />
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     4
 
     >>> entry = entries[1]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [9] Thunderbird crashes
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Foo Bar
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name16
 
     >>> assert check_entries_order(entries), (
@@ -184,13 +184,13 @@
     >>> login('foo.bar@xxxxxxxxxxxxx')
     >>> priv_team = factory.makeTeam(visibility=PersonVisibility.PRIVATE)
     >>> bug = getUtility(IBugSet).get(1)
-    >>> print bug.title
+    >>> print(bug.title)
     Firefox does not support SVG
-    >>> print len(bug.bugtasks)
+    >>> print(len(bug.bugtasks))
     3
     >>> from zope.security.proxy import removeSecurityProxy
     >>> bugtask = removeSecurityProxy(bug.bugtasks[1])
-    >>> print bugtask.distribution
+    >>> print(bugtask.distribution)
     <Distribution 'Ubuntu' (ubuntu)>
     >>> bugtask.assignee = priv_team
     >>> logout()
@@ -203,13 +203,13 @@
     No Errors
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     4
 
 The bug should be included in the feed.
 
     >>> entry = entries[3]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [1] Firefox does not support SVG
 
 Private teams should show as '-'.
@@ -218,7 +218,7 @@
     ...     entry.find('content').text,
     ...     convertEntities=BSS.HTML_ENTITIES)
     >>> soup = BSS(entry_content.text)
-    >>> print [tr.findAll('td')[4].text for tr in soup.findAll('tr')[1:4]]
+    >>> print([tr.findAll('td')[4].text for tr in soup.findAll('tr')[1:4]])
     [u'Mark Shuttleworth', u'-', u'-']
 
 == Latest bugs for a source package ==
@@ -237,17 +237,17 @@
     >>> browser.url
     'http://feeds.launchpad.dev/ubuntu/+source/thunderbird/latest-bugs.atom'
     >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
-    >>> print extract_text(soup.find('id'))
+    >>> print(extract_text(soup.find('id')))
     tag:launchpad.net,2008:/bugs/ubuntu/+source/thunderbird
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     1
     >>> entry = entries[0]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [9] Thunderbird crashes
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Foo Bar
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name16
 
     >>> assert check_entries_order(entries), (
@@ -270,24 +270,24 @@
     'http://feeds.launchpad.dev/ubuntu/hoary/latest-bugs.atom'
 
     >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
-    >>> print extract_text(soup.find('id'))
+    >>> print(extract_text(soup.find('id')))
     tag:launchpad.net,2006-10-16:/bugs/ubuntu/hoary
 
     >>> self_links = parse_links(browser.contents, 'self')
     >>> for link in self_links:
-    ...     print link
+    ...     print(link)
     <link rel="self" href="http://feeds.launchpad.dev/ubuntu/hoary/latest-bugs.atom"; />
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     1
 
     >>> entry = entries[0]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [2] Blackhole Trash folder
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Sample Person
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name12
 
     >>> assert check_entries_order(entries), (
@@ -310,24 +310,24 @@
     'http://feeds.launchpad.dev/firefox/1.0/latest-bugs.atom'
 
     >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
-    >>> print extract_text(soup.find('id'))
+    >>> print(extract_text(soup.find('id')))
     tag:launchpad.net,2005-06-06:/bugs/firefox/1.0
 
     >>> self_links = parse_links(browser.contents, 'self')
     >>> for link in self_links:
-    ...     print link
+    ...     print(link)
     <link rel="self" href="http://feeds.launchpad.dev/firefox/1.0/latest-bugs.atom"; />
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     1
 
     >>> entry = entries[0]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [5] Firefox install instructions should be complete
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Sample Person
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name12
 
     >>> assert check_entries_order(entries), (
@@ -348,32 +348,32 @@
     'http://feeds.launchpad.dev/~name16/latest-bugs.atom'
 
     >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
-    >>> print extract_text(soup.find('id'))
+    >>> print(extract_text(soup.find('id')))
     tag:launchpad.net,2005-06-06:/bugs/~name16
 
     >>> self_links = parse_links(browser.contents, 'self')
     >>> for link in self_links:
-    ...     print link
+    ...     print(link)
     <link rel="self" href="http://feeds.launchpad.dev/~name16/latest-bugs.atom"; />
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     7
 
     >>> entry = entries[0]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [15] Nonsensical bugs are useless
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Foo Bar
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name16
 
     >>> entry = entries[1]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [12] Copy, Cut and Delete operations should work on selections
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Foo Bar
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name16
 
     >>> assert check_entries_order(entries), (
@@ -421,16 +421,16 @@
     [u'Bugs for Simple Team']
 
     >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
-    >>> print extract_text(soup.find('id'))
+    >>> print(extract_text(soup.find('id')))
     tag:launchpad.net,2007-02-21:/bugs/~simple-team
 
     >>> self_links = parse_links(browser.contents, 'self')
     >>> for link in self_links:
-    ...     print link
+    ...     print(link)
     <link rel="self" href="http://feeds.launchpad.dev/~simple-team/latest-bugs.atom"; />
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     3
 
     >>> assert check_entries_order(entries), (
@@ -451,24 +451,24 @@
     'http://feeds.launchpad.dev/bugs/latest-bugs.atom'
 
     >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
-    >>> print extract_text(soup.find('id'))
+    >>> print(extract_text(soup.find('id')))
     tag:launchpad.net,2008:/bugs
 
     >>> self_links = parse_links(browser.contents, 'self')
     >>> for link in self_links:
-    ...     print link
+    ...     print(link)
     <link rel="self" href="http://feeds.launchpad.dev/bugs/latest-bugs.atom"; />
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     13
 
     >>> entry = entries[0]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [15] Nonsensical bugs are useless
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Foo Bar
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name16
 
     >>> assert check_entries_order(entries), (
@@ -513,36 +513,36 @@
 
     >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
     >>> feed_id = extract_text(soup.find('id'))
-    >>> print feed_id
+    >>> print(feed_id)
     tag:launchpad.net,2008:/+bugs.atom?field.scope.target=&amp;field.scope=all&amp;field.searchtext=&amp;search=Search+Bug+Reports
 
     >>> from lp.services.webapp.escaping import html_escape
-    >>> print html_escape(browser.url)
+    >>> print(html_escape(browser.url))
     http://feeds.launchpad.dev/bugs/+bugs.atom?field.scope.target=&amp;field.scope=all&amp;field.searchtext=&amp;search=Search+Bug+Reports
 
     >>> self_links = parse_links(browser.contents, 'self')
     >>> for link in self_links:
-    ...     print link
+    ...     print(link)
     <link rel="self" href="http://feeds.launchpad.dev/bugs/+bugs.atom?field.scope.target=&amp;field.scope=all&amp;field.searchtext=&amp;search=Search+Bug+Reports"; />
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     12
 
     >>> entry = entries[0]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [15] Nonsensical bugs are useless
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Foo Bar
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name16
 
     >>> entry = entries[1]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [13] Launchpad CSS and JS is not testible
-    >>> print extract_text(entry.author('name')[0])
+    >>> print(extract_text(entry.author('name')[0]))
     Sample Person
-    >>> print extract_text(entry.author('uri')[0])
+    >>> print(extract_text(entry.author('uri')[0]))
     http://bugs.launchpad.dev/~name12
 
 
@@ -557,14 +557,14 @@
     >>> BSS(browser.contents).title.contents
     [u'Bug 1']
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     1
     >>> entry = entries[0]
-    >>> print extract_text(entry.title)
+    >>> print(extract_text(entry.title))
     [1] Firefox does not support SVG
     >>> self_links = parse_links(browser.contents, 'self')
     >>> for link in self_links:
-    ...     print link
+    ...     print(link)
     <link rel="self" href="http://feeds.launchpad.dev/bugs/1/bug.atom"; />
 
 == Feeds Configuration Options ==

=== modified file 'lib/lp/bugs/stories/feeds/xx-bug-html.txt'
--- lib/lp/bugs/stories/feeds/xx-bug-html.txt	2017-10-21 18:14:14 +0000
+++ lib/lp/bugs/stories/feeds/xx-bug-html.txt	2018-06-30 16:32:46 +0000
@@ -20,16 +20,16 @@
 
     >>> def print_entry(entry):
     ...     tds = entry('td')
-    ...     print "number:", extract_text(tds[1])
-    ...     print "href:", tds[1].a['href']
-    ...     print "title:", extract_text(tds[2])
+    ...     print("number:", extract_text(tds[1]))
+    ...     print("href:", tds[1].a['href'])
+    ...     print("title:", extract_text(tds[2]))
     ...     if len(tds) < 6:
-    ...         print "importance:", extract_text(tds[3])
-    ...         print "status:", extract_text(tds[4])
+    ...         print("importance:", extract_text(tds[3]))
+    ...         print("status:", extract_text(tds[4]))
     ...     else:
-    ...         print "project:", extract_text(tds[3])
-    ...         print "importance:", extract_text(tds[4])
-    ...         print "status:", extract_text(tds[5])
+    ...         print("project:", extract_text(tds[3]))
+    ...         print("importance:", extract_text(tds[4]))
+    ...         print("status:", extract_text(tds[5]))
 
     >>> def get_bug_numbers(entries):
     ...     bug_numbers = []
@@ -53,7 +53,7 @@
     'http://feeds.launchpad.dev/jokosher/latest-bugs.html?show_column=bugtargetdisplayname'
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     3
 
     >>> print_entry(entries[1])
@@ -79,7 +79,7 @@
     'http://feeds.launchpad.dev/mozilla/latest-bugs.html?show_column=bugtargetdisplayname'
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     12
 
     >>> print_entry(entries[1])
@@ -103,9 +103,9 @@
     >>> login('foo.bar@xxxxxxxxxxxxx')
     >>> priv_team = factory.makeTeam(visibility=PersonVisibility.PRIVATE)
     >>> bug = getUtility(IBugSet).get(1)
-    >>> print bug.title
+    >>> print(bug.title)
     Firefox does not support SVG
-    >>> print len(bug.bugtasks)
+    >>> print(len(bug.bugtasks))
     3
     >>> from zope.security.proxy import removeSecurityProxy
     >>> bugtask = removeSecurityProxy(bug.bugtasks[1])
@@ -131,7 +131,7 @@
     'http://feeds.launchpad.dev/~name16/latest-bugs.html'
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     13
 
     >>> print_entry(entries[1])
@@ -158,7 +158,7 @@
     'http://feeds.launchpad.dev/bugs/latest-bugs.html?show_column=bugtargetdisplayname'
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     27
 
     >>> print_entry(entries[1])
@@ -210,7 +210,7 @@
     'Bugs from custom search'
 
     >>> entries = parse_entries(browser.contents)
-    >>> print len(entries)
+    >>> print(len(entries))
     26
 
     >>> print_entry(entries[1])

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-guidelines.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-guidelines.txt	2012-12-11 05:41:50 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-guidelines.txt	2018-06-30 16:32:46 +0000
@@ -33,14 +33,14 @@
 page, because that page does not include a bug description field.
 
     >>> def print_guidelines(name, browser):
-    ...     print '*'
-    ...     print name
-    ...     print '  <%s>' % user_browser.url
-    ...     print extract_text(find_tag_by_id(
-    ...         user_browser.contents, 'bug-reporting-guidelines'))
+    ...     print('*')
+    ...     print(name)
+    ...     print('  <%s>' % user_browser.url)
+    ...     print(extract_text(find_tag_by_id(
+    ...         user_browser.contents, 'bug-reporting-guidelines')))
     >>> def print_acknowledgement_message(browser):
-    ...     print extract_text(find_tags_by_class(
-    ...         user_browser.contents, 'informational message')[0])
+    ...     print(extract_text(find_tags_by_class(
+    ...         user_browser.contents, 'informational message')[0]))
 
     >>> def print_visible_guidelines(context_path, guidelines):
     ...     result = guidelines.findParents(id='filebug-form-container')
@@ -50,7 +50,7 @@
     ...         style_attrs = [item.strip()
     ...             for item in filebug_form_container['style'].split(";")]
     ...     if (result and not 'display: none' in style_attrs):
-    ...         print 'Found %s guidelines: %s' % (context_path, guidelines)
+    ...         print('Found %s guidelines: %s' % (context_path, guidelines))
 
     >>> for context_name, context_path, view in contexts:
     ...     filebug_url = (
@@ -111,8 +111,8 @@
 
 URLs are linkified.
 
-    >>> print find_tags_by_class(
-    ...     user_browser.contents, 'informational message')[0]
+    >>> print(find_tags_by_class(
+    ...     user_browser.contents, 'informational message')[0])
     <div ...><p class="last">Thank you for filing a bug for
     <a...https://launchpad.dev/ubuntu/+source/alsa-utils.../a></p></div>
 
@@ -126,8 +126,8 @@
     ...     'http://launchpad.dev/ubuntu/warty/+filebug')
     >>> user_browser.getControl('Summary', index=0).value = "It doesn't work"
     >>> user_browser.getControl('Continue').click()
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'bug-reporting-guidelines'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'bug-reporting-guidelines')))
     Ubuntu bug reporting guidelines:
     The version of Ubuntu you&#x27;re using.
     See http://example.com for more details.
@@ -137,8 +137,8 @@
 page. This prevents the user being taken away from the bug filing
 process by clicking on the links.
 
-    >>> print find_tag_by_id(
-    ...     user_browser.contents, 'bug-reporting-guidelines')
+    >>> print(find_tag_by_id(
+    ...     user_browser.contents, 'bug-reporting-guidelines'))
     <td...
     See <a ... target="_new">...</a> for more details...
 
@@ -164,8 +164,8 @@
     ...     'http://launchpad.dev/ubuntu/+filebug')
     >>> user_browser.getControl('Summary', index=0).value = "It doesn't work"
     >>> user_browser.getControl('Continue').click()
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'bug-reporting-guidelines'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'bug-reporting-guidelines')))
     Ubuntu bug reporting guidelines:
     The version of Ubuntu you&#x27;re using.
     See http://example.com for more details.
@@ -177,8 +177,8 @@
     ...     name='packagename_option').value = ['choose']
     >>> user_browser.getControl(
     ...     name='field.packagename').value = "alsa-utils"
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'bug-reporting-guidelines'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'bug-reporting-guidelines')))
     Ubuntu bug reporting guidelines:
     The version of Ubuntu you&#x27;re using.
     See http://example.com for more details.
@@ -203,18 +203,18 @@
     >>> edit_url_re = re.compile('/[+]edit$')
     >>> for context_name, context_path, view in contexts:
     ...     overview_url = 'http://launchpad.dev/%s' % (context_path,)
-    ...     print '* ' + context_name
-    ...     print '  - User:',
+    ...     print('* ' + context_name)
+    ...     print('  - User:', end=' ')
     ...     user_browser.open(overview_url)
     ...     try:
     ...         user_browser.getLink(url=edit_url_re)
     ...     except (SystemExit, KeyboardInterrupt):
     ...         raise
     ...     except:
-    ...         print sys.exc_info()[0].__name__
-    ...     print '  - Admin:',
+    ...         print(sys.exc_info()[0].__name__)
+    ...     print('  - Admin:', end=' ')
     ...     admin_browser.open(overview_url)
-    ...     print bool(admin_browser.getLink(url=edit_url_re))
+    ...     print(bool(admin_browser.getLink(url=edit_url_re)))
     * Ubuntu
       - User: LinkNotFoundError
       - Admin: True
@@ -233,13 +233,13 @@
 
     >>> for context_name, context_path, view in contexts:
     ...     edit_url = 'http://launchpad.dev/%s/%s' % (context_path, view)
-    ...     print '* ' + context_name
+    ...     print('* ' + context_name)
     ...     try:
     ...         user_browser.open(edit_url)
     ...     except (SystemExit, KeyboardInterrupt):
     ...         raise
     ...     except:
-    ...         print sys.exc_info()[0].__name__
+    ...         print(sys.exc_info()[0].__name__)
     * Ubuntu
       Unauthorized
     * Mozilla

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-tools.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-tools.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-tools.txt	2018-06-30 16:32:46 +0000
@@ -29,7 +29,7 @@
 to give the data to the +filebug page.
 
     >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     Your ticket is &quot;...&quot;
 
 To avoid having the tool from parsing the HTML page, the token is
@@ -66,15 +66,15 @@
     ...    '/ubuntu/+source/mozilla-firefox/+filebug/%s' % blob_token)
     >>> filebug_url = 'http://%s%s' % (filebug_host, filebug_path)
     >>> contents = str(http(
-    ...     "GET %s HTTP/1.1\nHostname: %s\n"
-    ...     "Authorization: Basic test@xxxxxxxxxxxxx:test\n\n"
+    ...     b"GET %s HTTP/1.1\nHostname: %s\n"
+    ...     b"Authorization: Basic test@xxxxxxxxxxxxx:test\n\n"
     ...     % (filebug_path, filebug_host)))
 
 At first, the user will be shown a message telling them that the extra
 data is being processed.
 
     >>> for message in find_tags_by_class(contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     Please wait while bug data is processed. This page will refresh
     every 10 seconds until processing is complete.
 
@@ -93,7 +93,7 @@
 information will be added to the bug automatically.
 
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     Extra debug information will be added to the bug report automatically.
 
 After the user fills in the summary and click on the button, we'll still
@@ -113,7 +113,7 @@
     >>> user_browser.getControl('Submit Bug Report').click()
     >>> for error in find_tags_by_class(
     ...     user_browser.contents, 'message error'):
-    ...     print error.renderContents()
+    ...     print(error.renderContents())
     There is 1 error.
 
     >>> user_browser.url == filebug_url
@@ -133,7 +133,7 @@
     >>> attachment_portlet = find_portlet(
     ...     user_browser.contents, 'Bug attachments')
     >>> for li in attachment_portlet('li', 'download-attachment'):
-    ...     print li.a.renderContents()
+    ...     print(li.a.renderContents())
     attachment1
     Attachment description.
 
@@ -214,7 +214,7 @@
     'http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/...'
 
     >>> tags = find_tag_by_id(user_browser.contents, 'bug-tags')
-    >>> print extract_text(tags)
+    >>> print(extract_text(tags))
     Tags: bar baz...
 
 The normal +filebug page has a hidden tags widget, so bugs filed via
@@ -233,7 +233,7 @@
     'http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/...'
 
     >>> tags = find_tag_by_id(user_browser.contents, 'bug-tags')
-    >>> print extract_text(tags)
+    >>> print(extract_text(tags))
     Tags: bar foo...
 
 
@@ -250,8 +250,8 @@
 
     >>> extra_filebug_data_with_hwdb_submission = open(
     ...     os.path.join(testfiles, 'extra_filebug_data_hwdb_submission.msg'))
-    >>> extra_filebug_data_with_hwdb_submission.read().split('\n')[2]
-    'HWDB-Submission: sample-submission, non-existing-submission-key'
+    >>> print(extra_filebug_data_with_hwdb_submission.read().split('\n')[2])
+    HWDB-Submission: sample-submission, non-existing-submission-key
 
 Once we submit the bug report...
 
@@ -281,5 +281,5 @@
     >>> linked_submissions = webservice.named_get(
     ...     bug_url, 'getHWSubmissions').jsonBody()
     >>> for submission in linked_submissions['entries']:
-    ...     print submission['submission_key']
+    ...     print(submission['submission_key'])
     sample-submission

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-displaying-similar-bugs.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-displaying-similar-bugs.txt	2012-11-14 19:18:05 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-displaying-similar-bugs.txt	2018-06-30 16:32:46 +0000
@@ -18,8 +18,8 @@
     ...         status = ' '.join(text_lines[2:])
     ...         # All this trouble is worth it when you see ndiff output
     ...         # from a failing test, and it *makes sense* :)
-    ...         print '(icon class=%s)\n  %s\n  %s' % (
-    ...             label_class, summary, status)
+    ...         print('(icon class=%s)\n  %s\n  %s' % (
+    ...             label_class, summary, status))
 
 
 Products
@@ -87,7 +87,7 @@
 the list of potentially similar bugs.
 
     >>> query = find_tag_by_id(user_browser.contents, 'filebug-query-heading')
-    >>> print extract_text(query)
+    >>> print(extract_text(query))
     Is "reflow" one of these bugs?
 
 

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-distro-guided-filebug-tags.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-distro-guided-filebug-tags.txt	2011-04-20 14:56:23 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-distro-guided-filebug-tags.txt	2018-06-30 16:32:46 +0000
@@ -16,7 +16,7 @@
 On the next page, possible duplicates are displayed as ususal. No
 candidates were found for this summary, though.
 
-    >>> print find_main_content(user_browser.contents).renderContents()
+    >>> print(find_main_content(user_browser.contents).renderContents())
     <...
     No similar bug reports were found...
 

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-distro-guided-filebug.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-distro-guided-filebug.txt	2015-07-21 09:04:01 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-distro-guided-filebug.txt	2018-06-30 16:32:46 +0000
@@ -28,7 +28,7 @@
     >>> user_browser.getControl(
     ...     "Yes, this is the bug I'm trying to report").click()
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/thunderbird/+bug/9
 
     >>> print_feedback_messages(user_browser.contents)
@@ -62,14 +62,14 @@
     >>> field_labels = page_soup.findAll(
     ...     'label', text=re.compile('Further information'))
     >>> for field_label in field_labels:
-    ...     print extract_text(field_label.parent)
+    ...     print(extract_text(field_label.parent))
     Further information:
 
 Finally, let's submit the bug.
 
     >>> user_browser.getControl("Submit Bug Report").click()
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/...
 
 
@@ -91,7 +91,7 @@
 
     >>> similar_bugs_list = find_tag_by_id(
     ...     user_browser.contents, "similar-bugs")
-    >>> print similar_bugs_list
+    >>> print(similar_bugs_list)
     None
 
 But the bug can be filed as before.
@@ -102,5 +102,5 @@
     ...     "Frobnobulator is a Firefox add-on, ...")
     >>> user_browser.getControl("Submit Bug Report").click()
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/...

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-distro-sourcepackage-guided-filebug.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-distro-sourcepackage-guided-filebug.txt	2015-07-21 09:04:01 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-distro-sourcepackage-guided-filebug.txt	2018-06-30 16:32:46 +0000
@@ -18,7 +18,7 @@
     >>> similar_bugs_table is None
     True
 
-    >>> print find_main_content(user_browser.contents).renderContents()
+    >>> print(find_main_content(user_browser.contents).renderContents())
     <...
     No similar bug reports were found...
 
@@ -36,5 +36,5 @@
     >>> user_browser.getControl(name="field.comment").value = "test"
     >>> user_browser.getControl("Submit Bug Report").click()
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/...

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-filebug-attachments.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-filebug-attachments.txt	2015-10-06 06:48:01 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-filebug-attachments.txt	2018-06-30 16:32:46 +0000
@@ -43,7 +43,7 @@
 
     >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
     >>> for li_tag in attachments.findAll('li', 'download-attachment'):
-    ...     print li_tag.a.renderContents()
+    ...     print(li_tag.a.renderContents())
     A description of the attachment
 
     >>> user_browser.getLink('A description of the attachment').url
@@ -56,7 +56,7 @@
 treated as non-empty by the receiving view. The attachment form will
 treat all empty-equivalent values equally.
 
-    >>> print http(r"""
+    >>> print(http(br"""
     ... POST /firefox/+filebug HTTP/1.1
     ... Authorization: Basic test@xxxxxxxxxxxxx:test
     ... Referer: https://launchpad.dev/
@@ -99,6 +99,6 @@
     ... 
     ... Submit Bug Report
     ... -----------------------------2051078912280543729816242321--
-    ... """)
+    ... """))
     HTTP/1.1 303 See Other...
     Location: http://bugs.launchpad.dev/firefox/+bug/...

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-no-launchpadder.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-no-launchpadder.txt	2010-09-27 19:39:21 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-no-launchpadder.txt	2018-06-30 16:32:46 +0000
@@ -15,7 +15,7 @@
     ...     'http://bugs.launchpad.dev/alsa-utils/+filebug')
     >>> for message in find_tags_by_class(
     ...     user_browser.contents, 'highlight-message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     alsa-utils does not use Launchpad as its bug tracker.
 
 But it's packaged in Ubuntu and Debian, and we suggest those packages
@@ -39,7 +39,7 @@
     ...     'http://bugs.launchpad.dev/gnomebaker/+filebug')
     >>> for message in find_tags_by_class(
     ...     user_browser.contents, 'highlight-message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     gnomebaker does not use Launchpad as its bug tracker.
 
     >>> user_browser.getLink("linking them for us.") is not None
@@ -47,8 +47,8 @@
 
 But we are advised to file bugs in the upstream bug tracker:
 
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'bugtarget-upstream-bugtracker-info'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'bugtarget-upstream-bugtracker-info')))
     Bugs in upstream gnomebaker should be reported in its official bug
     tracker, GnomeGBug GTracker
 
@@ -69,8 +69,8 @@
 
     >>> user_browser.open('http://bugs.launchpad.dev/jokosher/+filebug')
 
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'bugtarget-upstream-bugtracker-info'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'bugtarget-upstream-bugtracker-info')))
     Bugs in upstream Jokosher should be sent to
     mailto:puff@xxxxxxxxxxxxxxxxxxxxxxx
 
@@ -87,7 +87,7 @@
     ...     "http://launchpad.dev/products/alsa-utils/+filebug";)
     >>> for message in find_tags_by_class(
     ...     user_browser.contents, 'highlight-message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     alsa-utils does not use Launchpad as its bug tracker.
 
 
@@ -100,7 +100,7 @@
     ...     'http://bugs.launchpad.dev/debian/+filebug')
     >>> for message in find_tags_by_class(
     ...     user_browser.contents, 'highlight-message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     Debian does not use Launchpad as its bug tracker.
 
 They get the same messages in the advanced filebug page:
@@ -109,7 +109,7 @@
     ...     "http://launchpad.dev/distros/debian/+filebug";)
     >>> for message in find_tags_by_class(
     ...     user_browser.contents, 'highlight-message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     Debian does not use Launchpad as its bug tracker.
 
 
@@ -124,7 +124,7 @@
     ...     "+filebug")
     >>> for message in find_tags_by_class(
     ...     user_browser.contents, 'highlight-message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     Debian does not use Launchpad as its bug tracker.
 
 Not even using the advanced filebug page:
@@ -134,7 +134,7 @@
     ...     "+filebug")
     >>> for message in find_tags_by_class(
     ...     user_browser.contents, 'highlight-message'):
-    ...     print extract_text(message)
+    ...     print(extract_text(message))
     Debian does not use Launchpad as its bug tracker.
 
 

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-options-for-bug-supervisors.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-options-for-bug-supervisors.txt	2011-04-20 14:56:23 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-options-for-bug-supervisors.txt	2018-06-30 16:32:46 +0000
@@ -13,22 +13,22 @@
     >>> user_browser.getControl('Status')
     Traceback (most recent call last):
     ...
-    LookupError: label 'Status'
+    LookupError: label u'Status'
 
     >>> user_browser.getControl('Importance')
     Traceback (most recent call last):
     ...
-    LookupError: label 'Importance'
+    LookupError: label u'Importance'
 
     >>> user_browser.getControl('Milestone')
     Traceback (most recent call last):
     ...
-    LookupError: label 'Milestone'
+    LookupError: label u'Milestone'
 
     >>> user_browser.getControl('Assign to')
     Traceback (most recent call last):
     ...
-    LookupError: label 'Assign to'
+    LookupError: label u'Assign to'
 
 Users who are bug supervisors can see these options:
 

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-product-guided-filebug.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-product-guided-filebug.txt	2013-09-27 04:13:23 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-product-guided-filebug.txt	2018-06-30 16:32:46 +0000
@@ -20,10 +20,10 @@
     >>> top_portlet = first_tag_by_class(
     ...     user_browser.contents, 'top-portlet')
     >>> for message in top_portlet.findAll(attrs={'class': 'error message'}):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     There is 1 error.
     >>> for message in top_portlet.findAll(attrs={'class': 'message'}):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     Required input is missing.
 
 The user fills in some keywords, and clicks a button to search existing
@@ -36,7 +36,7 @@
 The form is self-posting, so the user is still at +filebug. This makes
 it difficult to bypass the search-for-dupes bit.
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+filebug
 
 After searching, the user is presented with a list of similar bugs in
@@ -58,15 +58,15 @@
     >>> user_browser.getControl("Further information").value = "not empty"
     >>> user_browser.getControl("Summary", index=0).value = ''
     >>> user_browser.getControl("Submit Bug Report").click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+filebug
 
-    >>> print find_main_content(user_browser.contents).renderContents()
+    >>> print(find_main_content(user_browser.contents).renderContents())
     <...
     No similar bug reports were found...
 
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     There is 1 error.
     Required input is missing.
 
@@ -76,7 +76,7 @@
     >>> user_browser.getControl("Further information").value = "test"
     >>> user_browser.getControl("Submit Bug Report").click()
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/...
 
 
@@ -104,7 +104,7 @@
     >>> user_browser.getControl(
     ...     "Yes, this is the bug I'm trying to report").click()
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/firefox/+bug/1
 
     >>> print_feedback_messages(user_browser.contents)
@@ -151,7 +151,7 @@
 
     >>> similar_bugs_list = find_tag_by_id(
     ...     user_browser.contents, "similar-bugs")
-    >>> print similar_bugs_list
+    >>> print(similar_bugs_list)
     None
 
 But, as before, entering a description and submitting the bug takes the
@@ -163,5 +163,5 @@
     >>> user_browser.url
     'http://bugs.launchpad.dev/firefox/+bug/...'
 
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Bug #...Frankenzombulon reanimated... : Bugs : Mozilla Firefox

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-productseries-guided-filebug.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-productseries-guided-filebug.txt	2014-11-29 06:41:25 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-productseries-guided-filebug.txt	2018-06-30 16:32:46 +0000
@@ -13,5 +13,5 @@
     >>> report_bug.click()
     >>> user_browser.url
     'http://bugs.launchpad.dev/firefox/+filebug'
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Report a bug : Bugs : Mozilla Firefox

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt	2012-11-14 19:37:04 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt	2018-06-30 16:32:46 +0000
@@ -26,7 +26,7 @@
 
     >>> user_browser.url
     'http://bugs...?field.title=Evolution+crashes&field.tags='
-    >>> print find_main_content(user_browser.contents).renderContents()
+    >>> print(find_main_content(user_browser.contents).renderContents())
     <...
     <input type="submit" id="field.actions.search"
     name="field.actions.search" value="Continue" class="button" /> ...
@@ -60,6 +60,6 @@
 
     >>> for message in find_tags_by_class(user_browser.contents,
     ...     'informational message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     There are no projects registered for Test Group that either use Launchpad
     to track bugs or allow new bugs to be filed.

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-ubuntu-filebug.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-ubuntu-filebug.txt	2014-11-29 06:41:25 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-ubuntu-filebug.txt	2018-06-30 16:32:46 +0000
@@ -19,14 +19,14 @@
 
     >>> user_browser.open('http://launchpad.dev/ubuntu')
     >>> user_browser.getLink('Report a bug').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://launchpad.dev/+tour/index
 
 We can override this behaviour by adding the `no-redirect` query parameter.
 
     >>> user_browser.open(
     ...    'http://bugs.launchpad.dev/ubuntu/+filebug?no-redirect')
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Report a bug : Bugs : Ubuntu
 
 The no-redirect parameter is retained when we redirect a user to the bug
@@ -34,14 +34,14 @@
 
     >>> user_browser.open(
     ...    'http://bugs.launchpad.dev/ubuntu/hoary/+filebug?no-redirect')
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/ubuntu/+filebug?no-redirect
 
 When filing bugs directly on source packages we are also not redirected.
 
     >>> admin_browser.open(
     ...     'http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+filebug')
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+filebug
 
 Ubuntu's bug supervisor doesn't get automatically redirected either.
@@ -57,7 +57,7 @@
     >>> logout()
 
     >>> admin_browser.open('http://bugs.launchpad.dev/ubuntu/+filebug')
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     Report a bug : Bugs : Ubuntu
 
 Filing bugs with Apport also allows us to get to the bug filing interface.
@@ -85,7 +85,7 @@
     >>> user_browser.open(
     ...     'http://launchpad.dev/ubuntu/+source/mozilla-firefox/+filebug/%s'
     ...     % blob_token)
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://launchpad.dev/ubuntu/+source/mozilla-firefox/+filebug/...
 
     >>> _ = config.pop('malone')
@@ -94,7 +94,7 @@
 
     >>> user_browser.open(
     ...     'http://bugs.launchpad.dev/ubuntu/+filebug-inline-form')
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/ubuntu/+filebug-inline-form
 
 Neither does the show-similar-bugs view.
@@ -102,5 +102,5 @@
     >>> user_browser.open(
     ...     'http://bugs.launchpad.dev/ubuntu/'
     ...     '+filebug-show-similar?title=testing')
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://bugs.launchpad.dev/ubuntu/+filebug-show-similar?title=testing

=== modified file 'lib/lp/bugs/stories/patches-view/patches-view.txt'
--- lib/lp/bugs/stories/patches-view/patches-view.txt	2015-10-05 06:34:17 +0000
+++ lib/lp/bugs/stories/patches-view/patches-view.txt	2018-06-30 16:32:46 +0000
@@ -20,10 +20,10 @@
 
     >>> def show_patches_view(contents):
     ...     for tag in find_tags_by_class(contents, 'listing'):
-    ...         print extract_text(tag)
+    ...         print(extract_text(tag))
     ...     messages = find_tags_by_class(contents, 'informational')
     ...     if len(messages) > 0:
-    ...         print extract_text(messages[0])
+    ...         print(extract_text(messages[0]))
 
     >>> anon_browser.open(
     ...     'http://bugs.launchpad.dev/patchy-product-1/+patches')

=== modified file 'lib/lp/bugs/stories/standalone/xx-filebug-package-chooser-radio-buttons.txt'
--- lib/lp/bugs/stories/standalone/xx-filebug-package-chooser-radio-buttons.txt	2011-04-20 14:56:23 +0000
+++ lib/lp/bugs/stories/standalone/xx-filebug-package-chooser-radio-buttons.txt	2018-06-30 16:32:46 +0000
@@ -9,12 +9,12 @@
     >>> user_browser.getControl('Summary', index=0).value = 'Bug Summary'
     >>> user_browser.getControl('Continue').click()
 
-    >>> print user_browser.getControl(name="packagename_option").value
+    >>> print(user_browser.getControl(name="packagename_option").value)
     ['none']
 
 ("I don't know" remains selected.)
 
-    >>> print user_browser.getControl(name="packagename_option").value
+    >>> print(user_browser.getControl(name="packagename_option").value)
     ['none']
 
 If you enter a package name that doesn't exist in the distribution,
@@ -27,7 +27,7 @@
     >>> user_browser.url
     'http://launchpad.dev/ubuntu/+filebug'
 
-    >>> print user_browser.getControl(name="packagename_option").value
+    >>> print(user_browser.getControl(name="packagename_option").value)
     ['choose']
 
 On the package +filebug page, the package name is populated by default.
@@ -37,8 +37,8 @@
     >>> user_browser.getControl('Summary', index=0).value = 'Bug Summary'
     >>> user_browser.getControl('Continue').click()
 
-    >>> print user_browser.getControl(name="packagename_option").value
+    >>> print(user_browser.getControl(name="packagename_option").value)
     ['choose']
 
-    >>> print user_browser.getControl(name="field.packagename").value
+    >>> print(user_browser.getControl(name="field.packagename").value)
     mozilla-firefox

=== modified file 'lib/lp/bugs/stories/standalone/xx-nonexistent-bugid-raises-404.txt'
--- lib/lp/bugs/stories/standalone/xx-nonexistent-bugid-raises-404.txt	2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/stories/standalone/xx-nonexistent-bugid-raises-404.txt	2018-06-30 16:32:46 +0000
@@ -1,14 +1,14 @@
 When the user attempts to access a bug that doesn't exist, a 404 is
 raised.
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET http://localhost:8085/bugs/123456 HTTP/1.1
-  ... """)
+  ... """))
   HTTP/1.1 404 Not Found
   ...
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET http://localhost:8085/bugs/doesntexist HTTP/1.1
-  ... """)
+  ... """))
   HTTP/1.1 404 Not Found
   ...

=== modified file 'lib/lp/bugs/stories/standalone/xx-obsolete-bug-and-task-urls.txt'
--- lib/lp/bugs/stories/standalone/xx-obsolete-bug-and-task-urls.txt	2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/stories/standalone/xx-obsolete-bug-and-task-urls.txt	2018-06-30 16:32:46 +0000
@@ -2,10 +2,10 @@
 
 1. The Anorak bug listing URL:
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET http://localhost:8085/bugs/bugs HTTP/1.1
   ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3
-  ... """)
+  ... """))
   HTTP/1.1 404 Not Found
   ...
 
@@ -14,10 +14,10 @@
 
 2. The tasks namespace:
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET http://localhost:8085/malone/tasks HTTP/1.1
   ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3
-  ... """)
+  ... """))
   HTTP/1.1 404 Not Found
   ...
 

=== modified file 'lib/lp/bugs/stories/standalone/xx-show-distribution-cve-report.txt'
--- lib/lp/bugs/stories/standalone/xx-show-distribution-cve-report.txt	2009-09-07 10:07:46 +0000
+++ lib/lp/bugs/stories/standalone/xx-show-distribution-cve-report.txt	2018-06-30 16:32:46 +0000
@@ -3,7 +3,7 @@
 
     >>> browser.open('http://launchpad.dev/gentoo/+cve')
     >>> main = find_main_content(browser.contents)
-    >>> print extract_text(main.h1)
+    >>> print(extract_text(main.h1))
     CVEs related to bugs in Gentoo
 
 The report lists the open and closed bugs.
@@ -22,7 +22,7 @@
 
 Instead, the links for the specific series reports are shown.
 
-    >>> print extract_text(main.h1)
+    >>> print(extract_text(main.h1))
     CVE reports in releases of Ubuntu
 
     >>> series_list = main.find('div', 'top-portlet').ul
@@ -37,5 +37,5 @@
     'http://launchpad.dev/ubuntu/breezy-autotest/+cve'
 
     >>> main = find_main_content(browser.contents)
-    >>> print extract_text(main.h1)
+    >>> print(extract_text(main.h1))
     CVEs related to bugs in Ubuntu Breezy-autotest

=== modified file 'lib/lp/bugs/stories/standalone/xx-show-distrorelease-cve-report.txt'
--- lib/lp/bugs/stories/standalone/xx-show-distrorelease-cve-report.txt	2012-05-22 12:05:51 +0000
+++ lib/lp/bugs/stories/standalone/xx-show-distrorelease-cve-report.txt	2018-06-30 16:32:46 +0000
@@ -1,7 +1,7 @@
 Let's look at all CVE issues in Debian and Debian Woody
 
     >>> browser.open('http://launchpad.dev/debian/woody/+cve')
-    >>> print '\n'+browser.contents
+    >>> print('\n'+browser.contents)
     <BLANKLINE>
     ...
     ...CVEs related to bugs in Debian Woody...
@@ -39,6 +39,6 @@
     >>> browser.open('http://launchpad.dev/debian/woody/+cve')
     >>> main = find_main_content(browser.contents)
     >>> for tr in main.table.tbody.findAll('tr'):
-    ...     print extract_text(tr)
+    ...     print(extract_text(tr))
     Bug #2: Blackhole Trash folder CVE-1999-2345
     Debian Woody New (unassigned)

=== modified file 'lib/lp/bugs/stories/standalone/xx-slash-malone-slash-assigned.txt'
--- lib/lp/bugs/stories/standalone/xx-slash-malone-slash-assigned.txt	2009-06-12 16:36:02 +0000
+++ lib/lp/bugs/stories/standalone/xx-slash-malone-slash-assigned.txt	2018-06-30 16:32:46 +0000
@@ -1,8 +1,8 @@
 To access /malone/assigned we have to be logged in.
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /bugs/assigned HTTP/1.1
-  ... """)
+  ... """))
   HTTP/1.1 303 See Other
   ...
   Location: http://localhost/bugs/assigned/+login
@@ -13,10 +13,10 @@
 /malone/assigned has been deprecated, in favour of the equivalent
 report (at least by intent, if not by design) in FOAF.
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /bugs/assigned HTTP/1.1
   ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """)
+  ... """))
   HTTP/1.1 303 See Other
   ...
   Location: http://localhost/~name16/+assignedbugs

=== modified file 'lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt'
--- lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt	2018-06-30 16:32:46 +0000
@@ -17,7 +17,7 @@
 
     >>> browser.open(
     ... 'http://bugs.launchpad.dev/ubuntu/+subscribe')
-    >>> print extract_text(find_portlet(browser.contents, 'Subscribers'))
+    >>> print(extract_text(find_portlet(browser.contents, 'Subscribers')))
     Subscribers
       To all Ubuntu bugs:
         Landscape Developers
@@ -42,9 +42,9 @@
 
     >>> browser.open(
     ... 'http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+subscribe')
-    >>> print extract_text(find_portlet(
+    >>> print(extract_text(find_portlet(
     ...     browser.contents, 'Subscribers')).encode(
-    ...     'ascii', 'backslashreplace')
+    ...     'ascii', 'backslashreplace'))
     Subscribers
       To all bugs in mozilla-firefox in Ubuntu:
         Foo Bar
@@ -63,7 +63,7 @@
     >>> browser.getControl('Save these changes').click()
     >>> browser.open(
     ... 'http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+subscribe')
-    >>> print extract_text(find_portlet(browser.contents, 'Subscribers'))
+    >>> print(extract_text(find_portlet(browser.contents, 'Subscribers')))
     Subscribers
       To all bugs in mozilla-firefox in Ubuntu:
         Foo Bar
@@ -104,13 +104,13 @@
 No Privileges Person is now subscribed...
 
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     No Privileges Person will now receive an email each time someone reports
     or changes a public bug in &quot;mozilla-firefox in Ubuntu&quot;.
 
     >>> browser.open(
     ... 'http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+subscribe')
-    >>> print extract_text(find_portlet(browser.contents, 'Subscribers'))
+    >>> print(extract_text(find_portlet(browser.contents, 'Subscribers')))
     Subscribers
       To all bugs in mozilla-firefox in Ubuntu:
         Foo Bar
@@ -126,13 +126,13 @@
 
     >>> remove_other.selected = True
     >>> browser.getControl('Save these changes').click()
-    >>> print find_tags_by_class(
-    ...    browser.contents, 'informational message')[0].contents[0]
+    >>> print(find_tags_by_class(
+    ...    browser.contents, 'informational message')[0].contents[0])
     No Privileges Person will no longer automatically receive email about
     public bugs in &quot;mozilla-firefox in Ubuntu&quot;.
     >>> browser.open(
     ... 'http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+subscribe')
-    >>> print extract_text(find_portlet(browser.contents, 'Subscribers'))
+    >>> print(extract_text(find_portlet(browser.contents, 'Subscribers')))
     Subscribers
       To all bugs in mozilla-firefox in Ubuntu:
         Foo Bar
@@ -145,7 +145,7 @@
     >>> remove_other = browser.getControl('\xa0No Privileges Person')
     Traceback (most recent call last):
     ...
-    LookupError: label '\xa0No Privileges Person'
+    LookupError: label u'\xa0No Privileges Person'
 
 We clean up by removing Sample Person as the distribution driver.
 
@@ -158,7 +158,7 @@
 and to add Sample Person is now silently ignored, because LaunchpadFormView
 purges the submitted form data from now unexpected values.
 
-    >>> print extract_text(find_portlet(browser.contents, 'Subscribers'))
+    >>> print(extract_text(find_portlet(browser.contents, 'Subscribers')))
     Subscribers
       To all bugs in mozilla-firefox in Ubuntu:
         Foo Bar
@@ -172,7 +172,7 @@
     >>> browser.getControl('Save these changes').click()
     >>> browser.open(
     ... 'http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+subscribe')
-    >>> print extract_text(find_portlet(browser.contents, 'Subscribers'))
+    >>> print(extract_text(find_portlet(browser.contents, 'Subscribers')))
     Subscribers
       To all bugs in mozilla-firefox in Ubuntu:
         Foo Bar
@@ -187,12 +187,12 @@
     >>> browser.getControl('Subscribe someone else:')
     Traceback (most recent call last):
     ...
-    LookupError: label 'Subscribe someone else:'
+    LookupError: label u'Subscribe someone else:'
 
-    >>> print browser.getControl('\xa0Foo Bar')
+    >>> print(browser.getControl('\xa0Foo Bar'))
     Traceback (most recent call last):
     ...
-    LookupError: label '\xa0Foo Bar'
+    LookupError: label u'\xa0Foo Bar'
 
 
 Distribution with a bug supervisor

=== modified file 'lib/lp/bugs/stories/upstream-bugprivacy/xx-upstream-bug-privacy.txt'
--- lib/lp/bugs/stories/upstream-bugprivacy/xx-upstream-bug-privacy.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/upstream-bugprivacy/xx-upstream-bug-privacy.txt	2018-06-30 16:32:46 +0000
@@ -15,7 +15,7 @@
     >>> browser.getControl("Submit Bug Report").click()
 
     >>> bug_id = browser.url.split("/")[-1]
-    >>> print browser.url.replace(bug_id, "BUG-ID")
+    >>> print(browser.url.replace(bug_id, "BUG-ID"))
     http://bugs.launchpad.dev/firefox/+bug/BUG-ID
 
 
@@ -39,7 +39,7 @@
 bug listing.
 
     >>> browser.open("http://localhost:9000/firefox/+bugs";)
-    >>> print browser.contents.replace(bug_id, "BUG-ID")
+    >>> print(browser.contents.replace(bug_id, "BUG-ID"))
     <!DOCTYPE...
     ...
     ...Mozilla Firefox...
@@ -85,14 +85,14 @@
 
     >>> browser = setupBrowser(auth="Basic foo.bar@xxxxxxxxxxxxx:test")
     >>> browser.open("http://launchpad.dev/firefox/+bug/%s"; % bug_id)
-    >>> print browser.headers["Status"]
+    >>> print(browser.headers["Status"])
     200 Ok
 
 They now access the task page of a task on a private bug; also permitted.
 
     >>> browser = setupBrowser(auth="Basic foo.bar@xxxxxxxxxxxxx:test")
     >>> browser.open("http://launchpad.dev/firefox/+bug/%s/+editstatus"; % bug_id)
-    >>> print browser.headers["Status"]
+    >>> print(browser.headers["Status"])
     200 Ok
 
 
@@ -100,10 +100,10 @@
 View the bug task listing page as an anonymous user. Note that the
 private bug just filed by Sample Person is not visible.
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /firefox/+bugs HTTP/1.1
   ... Accept-Language: en-ca,en-us;q=0.8,en;q=0.5,fr-ca;q=0.3
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...3 results...
   ...5...
@@ -114,15 +114,15 @@
 Trying to access a private upstream bug as an anonymous user results
 in a page not found error.
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /firefox/+bug/6 HTTP/1.1
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /firefox/+bug/14 HTTP/1.1
-  ... """)
+  ... """))
   HTTP/1.1 404 Not Found
   ...
 
@@ -130,10 +130,10 @@
 Bar cannot see in this listing the private bug that Sample Person
 submitted earlier.
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /firefox/+bugs HTTP/1.1
   ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...Mozilla Firefox...
   ...5...Firefox install instructions should be complete...
@@ -144,10 +144,10 @@
 
 View bugs on Mozilla Firefox as the no-privs user:
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /firefox/+bugs HTTP/1.1
   ... Authorization: Basic bm8tcHJpdkBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...
       Mozilla Firefox
@@ -155,29 +155,29 @@
 
 Note that the no-privs user doesn't have the permissions to see bug #13.
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /firefox/+bug/14 HTTP/1.1
   ... Authorization: Basic bm8tcHJpdkBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """)
+  ... """))
   HTTP/1.1 404 Not Found
   ...
 
 This is also true if no-privs tries to access the bug from another
 context.
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /tomcat/+bug/14 HTTP/1.1
   ... Authorization: Basic bm8tcHJpdkBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """)
+  ... """))
   HTTP/1.1 404 Not Found
   ...
 
 Sample Person views a bug, which they're about to set private:
 
-  >>> print http(r"""
+  >>> print(http(br"""
   ... GET /firefox/+bug/4/+edit HTTP/1.1
   ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...
   ...Reflow problems with complex page layouts...

=== modified file 'lib/lp/bugs/stories/webservice/xx-bug-target.txt'
--- lib/lp/bugs/stories/webservice/xx-bug-target.txt	2012-08-21 04:04:47 +0000
+++ lib/lp/bugs/stories/webservice/xx-bug-target.txt	2018-06-30 16:32:46 +0000
@@ -11,7 +11,7 @@
     >>> product_url = '/firefox'
 
     >>> product = webservice.get(product_url).jsonBody()
-    >>> print product['bug_reporting_guidelines']
+    >>> print(product['bug_reporting_guidelines'])
     None
 
     >>> from simplejson import dumps
@@ -21,7 +21,7 @@
     ...     product['self_link'], 'application/json', dumps(patch))
 
     >>> product = webservice.get(product_url).jsonBody()
-    >>> print product['bug_reporting_guidelines']
+    >>> print(product['bug_reporting_guidelines'])
     Please run `ubuntu-bug -p firefox`.
 
 Not everyone can modify it however:
@@ -30,7 +30,7 @@
     ...          u'Include your credit-card details, mwuh'}
     >>> response = user_webservice.patch(
     ...     product['self_link'], 'application/json', dumps(patch))
-    >>> print response
+    >>> print(response)
     HTTP/1.1 401 Unauthorized...
     Content-Length: ...
     Content-Type: text/plain...
@@ -54,9 +54,9 @@
 The webservice client is logged in as salgado, so we can add a new official
 tag.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     '/tags-test-product', 'addOfficialBugTag',
-    ...     tag='test-bug-tag')
+    ...     tag='test-bug-tag'))
     HTTP/1.1 200 Ok
     ...
     <BLANKLINE>
@@ -64,9 +64,9 @@
 
 And we can remove it.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     '/tags-test-product', 'removeOfficialBugTag',
-    ...     tag='test-bug-tag')
+    ...     tag='test-bug-tag'))
     HTTP/1.1 200 Ok
     ...
     <BLANKLINE>
@@ -74,9 +74,9 @@
 
 But a different user can't.
 
-    >>> print user_webservice.named_post(
+    >>> print(user_webservice.named_post(
     ...     '/tags-test-product', 'addOfficialBugTag',
-    ...     tag='test-bug-tag')
+    ...     tag='test-bug-tag'))
     HTTP/1.1 401 Unauthorized
     ...
     <BLANKLINE>
@@ -88,17 +88,17 @@
     >>> product = factory.makeProduct(name='tags-test-product2')
     >>> logout()
     >>> ws_salgado = webservice.get('/~salgado').jsonBody()
-    >>> print webservice.patch(
+    >>> print(webservice.patch(
     ...     '/tags-test-product2', 'application/json',
-    ...     dumps({'bug_supervisor_link': ws_salgado['self_link']}))
+    ...     dumps({'bug_supervisor_link': ws_salgado['self_link']})))
     HTTP/1.1 209 Content Returned...
 
 The webservice client is logged in as salgado and he can add a new official
 tag.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     '/tags-test-product2', 'addOfficialBugTag',
-    ...     tag='test-bug-tag2')
+    ...     tag='test-bug-tag2'))
     HTTP/1.1 200 Ok
     ...
     <BLANKLINE>
@@ -106,9 +106,9 @@
 
 Official tags must conform to the same format as ordinary tags.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     '/tags-test-product', 'addOfficialBugTag',
-    ...     tag='an invalid tag !!!')
+    ...     tag='an invalid tag !!!'))
     HTTP/1.1 400 Bad Request
     ...
     tag: an invalid tag !!!
@@ -119,9 +119,9 @@
     >>> tags_test_product = webservice.get('/tags-test-product').jsonBody()
     >>> tags_test_product['official_bug_tags']
     []
-    >>> print webservice.patch(
+    >>> print(webservice.patch(
     ...     '/tags-test-product', 'application/json',
-    ...     dumps({'official_bug_tags': [u'foo', u'bar']}))
+    ...     dumps({'official_bug_tags': [u'foo', u'bar']})))
     HTTP/1.1 209 Content Returned...
 
     >>> tags_test_product = webservice.get('/tags-test-product').jsonBody()
@@ -131,9 +131,9 @@
     >>> login('foo.bar@xxxxxxxxxxxxx')
     >>> distribution = factory.makeDistribution(name='testix')
     >>> logout()
-    >>> print webservice.patch(
+    >>> print(webservice.patch(
     ...     '/testix', 'application/json',
-    ...     dumps({'official_bug_tags': [u'foo', u'bar']}))
+    ...     dumps({'official_bug_tags': [u'foo', u'bar']})))
     HTTP/1.1 209 Content Returned...
 
 == bug_supervisor ==
@@ -141,38 +141,38 @@
 We can retrieve or set a person or team as the bug supervisor for projects.
 
     >>> firefox_project = webservice.get('/firefox').jsonBody()
-    >>> print firefox_project['bug_supervisor_link']
+    >>> print(firefox_project['bug_supervisor_link'])
     None
 
-    >>> print webservice.patch(
+    >>> print(webservice.patch(
     ...     '/firefox', 'application/json',
-    ...     dumps({'bug_supervisor_link': firefox_project['owner_link']}))
+    ...     dumps({'bug_supervisor_link': firefox_project['owner_link']})))
     HTTP/1.1 209 Content Returned...
 
     >>> firefox_project = webservice.get('/firefox').jsonBody()
-    >>> print firefox_project['bug_supervisor_link']
+    >>> print(firefox_project['bug_supervisor_link'])
     http://api.launchpad.dev/beta/~name12
 
 We can also do this for distributions.
 
     >>> ubuntutest_dist = webservice.get('/ubuntutest').jsonBody()
-    >>> print ubuntutest_dist['bug_supervisor_link']
+    >>> print(ubuntutest_dist['bug_supervisor_link'])
     None
 
-    >>> print webservice.patch(
+    >>> print(webservice.patch(
     ...     '/ubuntutest', 'application/json',
-    ...     dumps({'bug_supervisor_link': ubuntutest_dist['owner_link']}))
+    ...     dumps({'bug_supervisor_link': ubuntutest_dist['owner_link']})))
     HTTP/1.1 209 Content Returned...
 
     >>> ubuntutest_dist = webservice.get('/ubuntutest').jsonBody()
-    >>> print ubuntutest_dist['bug_supervisor_link']
+    >>> print(ubuntutest_dist['bug_supervisor_link'])
     http://api.launchpad.dev/beta/~ubuntu-team
 
 Setting the bug supervisor is restricted to owners and launchpad admins.
 
-    >>> print user_webservice.patch(
+    >>> print(user_webservice.patch(
     ...     '/ubuntutest', 'application/json',
-    ...     dumps({'bug_supervisor_link': None}))
+    ...     dumps({'bug_supervisor_link': None})))
     HTTP/1.1 401 Unauthorized
     ...
     <BLANKLINE>

=== modified file 'lib/lp/bugs/stories/webservice/xx-bug-tracker.txt'
--- lib/lp/bugs/stories/webservice/xx-bug-tracker.txt	2012-02-21 22:46:28 +0000
+++ lib/lp/bugs/stories/webservice/xx-bug-tracker.txt	2018-06-30 16:32:46 +0000
@@ -37,7 +37,7 @@
     >>> bug_tracker = anon_webservice.named_get(
     ...     '/bugs/bugtrackers', 'getByName',
     ...     name='gnome-bugzilla').jsonBody()
-    >>> print bug_tracker['name']
+    >>> print(bug_tracker['name'])
     gnome-bugzilla
 
 A bug tracker can be retrieved using the bug tracker collection's
@@ -46,7 +46,7 @@
     >>> bug_tracker = anon_webservice.named_get(
     ...     '/bugs/bugtrackers', 'queryByBaseURL',
     ...     base_url='https://bugzilla.mozilla.org/').jsonBody()
-    >>> print bug_tracker['name']
+    >>> print(bug_tracker['name'])
     mozilla.org
 
 The bug tracker set provides the ensureBugTracker named operation that a
@@ -56,8 +56,8 @@
     ...     base_url='http://wombat.zz/', bug_tracker_type='Bugzilla',
     ...     name='wombat', title='Wombat title', summary='Wombat summary',
     ...     contact_details='big-nose@xxxxxxxxx')
-    >>> print webservice.named_post(
-    ...     '/bugs/bugtrackers', 'ensureBugTracker', **params)
+    >>> print(webservice.named_post(
+    ...     '/bugs/bugtrackers', 'ensureBugTracker', **params))
     HTTP/1.1 201 Created ...
     Location: http://.../bugs/bugtrackers/wombat ...
 

=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
--- lib/lp/bugs/stories/webservice/xx-bug.txt	2017-05-31 17:31:58 +0000
+++ lib/lp/bugs/stories/webservice/xx-bug.txt	2018-06-30 16:32:46 +0000
@@ -79,17 +79,17 @@
     ...     '/bugs', 'createBug',
     ...     title='Test bug', description='Test bug',
     ...     target=firefox['self_link'])
-    >>> print response
+    >>> print(response)
     HTTP/1.1 201 Created
     ...
     Location: http://.../bugs/...
     ...
     >>> new_bug_id = int(response.getHeader("Location").rsplit('/', 1)[-1])
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     '/bugs', 'createBug',
     ...     title='Test bug', description='Test bug',
-    ...     target=webservice.getAbsoluteUrl('/ubuntu'))
+    ...     target=webservice.getAbsoluteUrl('/ubuntu')))
     HTTP/1.1 201 Created
     ...
     Location: http://.../bugs/...
@@ -99,7 +99,7 @@
     ...     '/bugs', 'createBug',
     ...     title='Test bug', description='Test bug',
     ...     target=webservice.getAbsoluteUrl('/ubuntu/+source/evolution'))
-    >>> print response
+    >>> print(response)
     HTTP/1.1 201 Created
     ...
     Location: http://.../bugs/...
@@ -120,25 +120,25 @@
     >>> bug = getUtility(IBugSet).get(new_bug['id'])
 
     >>> for activity in bug.activity:
-    ...     print '%s, %s, %s' % (
+    ...     print('%s, %s, %s' % (
     ...         activity.whatchanged, activity.message,
-    ...         activity.person.name)
+    ...         activity.person.name))
     bug, added bug, salgado
 
     >>> for notification in BugNotification.selectBy(bug=bug, orderBy='id'):
-    ...     print '%s, %s, %s' % (
+    ...     print('%s, %s, %s' % (
     ...         notification.message.owner.name, notification.is_comment,
-    ...         notification.message.text_contents)
+    ...         notification.message.text_contents))
     salgado, True, Test bug
 
     >>> logout()
 
 A ProductSeries can't be the target of a new bug.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     '/bugs', 'createBug',
     ...     title='Test bug', description='Test bug',
-    ...     target=webservice.getAbsoluteUrl('/firefox/1.0'))
+    ...     target=webservice.getAbsoluteUrl('/firefox/1.0')))
     HTTP/1.1 400 Bad Request
     ...
     Can't create a bug on a series. Create it with a non-series
@@ -147,9 +147,9 @@
 That operation will fail if the client doesn't specify the product or
 distribution in which the bug exists.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     '/bugs', 'createBug',
-    ...     title='Test bug', description='Test bug')
+    ...     title='Test bug', description='Test bug'))
     HTTP/1.1 400 Bad Request
     ...
     target: Required input is missing.
@@ -160,37 +160,39 @@
     >>> bug_twelve = webservice.get("/bugs/12").jsonBody()
     >>> bug_twelve['private']
     False
-    >>> print webservice.patch(
-    ...     bug_twelve['self_link'], 'application/json', dumps(dict(private=True)))
+    >>> print(webservice.patch(
+    ...     bug_twelve['self_link'], 'application/json',
+    ...     dumps(dict(private=True))))
     HTTP/1.1 209 Content Returned...
     >>> bug_twelve = webservice.get("/bugs/12").jsonBody()
     >>> bug_twelve['private']
     True
-    >>> print webservice.patch(
-    ...     bug_twelve['self_link'], 'application/json', dumps(dict(private=False)))
+    >>> print(webservice.patch(
+    ...     bug_twelve['self_link'], 'application/json',
+    ...     dumps(dict(private=False))))
     HTTP/1.1 209 Content Returned...
 
 Similarly, to mark a bug as a duplicate, we patch the `duplicate_of_link`
 attribute of the bug.
 
-    >>> print bug_twelve['duplicate_of_link']
+    >>> print(bug_twelve['duplicate_of_link'])
     None
-    >>> print webservice.patch(
+    >>> print(webservice.patch(
     ...       bug_twelve['self_link'], 'application/json',
-    ...       dumps(dict(duplicate_of_link=bug_one['self_link'])))
+    ...       dumps(dict(duplicate_of_link=bug_one['self_link']))))
     HTTP/1.1 209 Content Returned...
     >>> bug_twelve = webservice.get("/bugs/12").jsonBody()
-    >>> print bug_twelve['duplicate_of_link']
+    >>> print(bug_twelve['duplicate_of_link'])
     http://api.launchpad.dev/beta/bugs/1
 
 Now set it back to none:
 
-    >>> print webservice.patch(
+    >>> print(webservice.patch(
     ...       bug_twelve['self_link'], 'application/json',
-    ...       dumps(dict(duplicate_of_link=None)))
+    ...       dumps(dict(duplicate_of_link=None))))
     HTTP/1.1 209 Content Returned...
     >>> bug_twelve = webservice.get("/bugs/12").jsonBody()
-    >>> print bug_twelve['duplicate_of_link']
+    >>> print(bug_twelve['duplicate_of_link'])
     None
 
 Marking a bug as duplicate follows the same validation rules as available in
@@ -198,24 +200,24 @@
 Due to bug #1088358 the error is escaped as if it was HTML.
 
     >>> dupe_url = webservice.getAbsoluteUrl('/bugs/%d' % new_bug_id)
-    >>> print webservice.patch(
+    >>> print(webservice.patch(
     ...       dupe_url, 'application/json',
     ...       dumps(dict(
-    ...           duplicate_of_link=webservice.getAbsoluteUrl('/bugs/5'))))
+    ...           duplicate_of_link=webservice.getAbsoluteUrl('/bugs/5')))))
     HTTP/1.1 209 Content Returned...
 
-    >>> print webservice.patch(
+    >>> print(webservice.patch(
     ...       webservice.getAbsoluteUrl('/bugs/5'), 'application/json',
-    ...       dumps(dict(duplicate_of_link=dupe_url)))
+    ...       dumps(dict(duplicate_of_link=dupe_url))))
     HTTP/1.1 400 Bad Request
     ...
     Bug ... is already a duplicate of bug 5. You
     can only mark a bug report as duplicate of one that
     isn&#x27;t a duplicate itself...
 
-    >>> print webservice.patch(
+    >>> print(webservice.patch(
     ...       dupe_url, 'application/json',
-    ...       dumps(dict(duplicate_of_link=None)))
+    ...       dumps(dict(duplicate_of_link=None))))
     HTTP/1.1 209 Content Returned...
 
 
@@ -258,15 +260,15 @@
 Bug messages can be accessed anonymously.
 
     >>> messages = anon_webservice.get("/bugs/5/messages").jsonBody()['entries']
-    >>> print messages[0]['self_link']
+    >>> print(messages[0]['self_link'])
     http://.../firefox/+bug/5/comments/0
 
 We can add a new message to a bug by calling the newMessage method.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     "/bugs/5", 'newMessage',
     ...     subject='A new message',
-    ...     content='This is a new message added through the webservice API.')
+    ...     content='This is a new message added through the webservice API.'))
     HTTP/1.1 201 Created...
     Content-Length: 0
     ...
@@ -284,9 +286,9 @@
 
 We don't have to submit a subject when we add a new message.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     "/bugs/5", 'newMessage',
-    ...     content='This is a new message with no subject.')
+    ...     content='This is a new message with no subject.'))
     HTTP/1.1 201 Created...
     Content-Length: 0
     ...
@@ -353,10 +355,10 @@
 
     >>> patch = {u'assignee_link': webservice.getAbsoluteUrl('/~cprov')}
     >>> bugtask_path = bug_one_bugtasks[0]['self_link']
-    >>> print webservice.patch(bugtask_path, 'application/json', dumps(patch))
+    >>> print(webservice.patch(bugtask_path, 'application/json', dumps(patch)))
     HTTP/1.1 209 Content Returned...
 
-    >>> print webservice.get(bugtask_path).jsonBody()['assignee_link']
+    >>> print(webservice.get(bugtask_path).jsonBody()['assignee_link'])
     http://.../~cprov
 
 
@@ -367,7 +369,7 @@
     u'Low'
 
     >>> patch = {u'importance': u'High'}
-    >>> print webservice.patch(bugtask_path, 'application/json', dumps(patch))
+    >>> print(webservice.patch(bugtask_path, 'application/json', dumps(patch)))
     HTTP/1.1 209 Content Returned...
 
     >>> body = webservice.get(bugtask_path).jsonBody()
@@ -377,8 +379,8 @@
 Only bug supervisors or people who can otherwise edit the bugtask's
 pillar are authorised to edit the importance.
 
-    >>> print user_webservice.named_post(
-    ...     bugtask_path, 'transitionToImportance', importance='Low')
+    >>> print(user_webservice.named_post(
+    ...     bugtask_path, 'transitionToImportance', importance='Low'))
     HTTP/1.1 401 Unauthorized...
 
     >>> body = webservice.get(bugtask_path).jsonBody()
@@ -387,46 +389,46 @@
 
 The task's status can also be modified directly.
 
-    >>> print webservice.get(bugtask_path).jsonBody()['status']
+    >>> print(webservice.get(bugtask_path).jsonBody()['status'])
     Confirmed
 
     >>> patch = {u'status': u'Fix Committed'}
-    >>> print webservice.patch(bugtask_path, 'application/json', dumps(patch))
+    >>> print(webservice.patch(bugtask_path, 'application/json', dumps(patch)))
     HTTP/1.1 209 Content Returned...
 
-    >>> print webservice.get(bugtask_path).jsonBody()['status']
+    >>> print(webservice.get(bugtask_path).jsonBody()['status'])
     Fix Committed
 
 If an error occurs during a request that sets both 'status' and
 'importance', neither one will be set.
 
     >>> task = webservice.get(bugtask_path).jsonBody()
-    >>> print task['status']
+    >>> print(task['status'])
     Fix Committed
-    >>> print task['importance']
+    >>> print(task['importance'])
     High
 
     >>> patch = {u'importance': 'High', u'status': u'No Such Status'}
-    >>> print webservice.patch(bugtask_path, 'application/json', dumps(patch))
+    >>> print(webservice.patch(bugtask_path, 'application/json', dumps(patch)))
     HTTP/1.1 400 Bad Request...
 
     >>> task = webservice.get(bugtask_path).jsonBody()
-    >>> print task['status']
+    >>> print(task['status'])
     Fix Committed
-    >>> print task['importance']
+    >>> print(task['importance'])
     High
 
 The milestone can only be set by appropriately privileged users.
 
-    >>> print webservice.get(bugtask_path).jsonBody()['milestone_link']
+    >>> print(webservice.get(bugtask_path).jsonBody()['milestone_link'])
     None
 
     >>> patch = {u'milestone_link': webservice.getAbsoluteUrl(
     ...                                 '/debian/+milestone/3.1')}
-    >>> print webservice.patch(bugtask_path, 'application/json', dumps(patch))
+    >>> print(webservice.patch(bugtask_path, 'application/json', dumps(patch)))
     HTTP/1.1 209 Content Returned...
 
-    >>> print webservice.get(bugtask_path).jsonBody()['milestone_link']
+    >>> print(webservice.get(bugtask_path).jsonBody()['milestone_link'])
     http://.../debian/+milestone/3.1
 
 We need to ensure the milestone we try and set is different to the current
@@ -435,11 +437,11 @@
 
     >>> patch = {u'milestone_link': webservice.getAbsoluteUrl(
     ...                                 '/debian/+milestone/3.1-rc1')}
-    >>> print user_webservice.patch(
-    ...     bugtask_path, 'application/json', dumps(patch))
+    >>> print(user_webservice.patch(
+    ...     bugtask_path, 'application/json', dumps(patch)))
     HTTP/1.1 401 Unauthorized...
 
-    >>> print webservice.get(bugtask_path).jsonBody()['milestone_link']
+    >>> print(webservice.get(bugtask_path).jsonBody()['milestone_link'])
     http://.../debian/+milestone/3.1
 
 We can change the task's target. Here we change the task's target from
@@ -452,9 +454,9 @@
     >>> ignored = factory.makeSourcePackagePublishingHistory(
     ...     distroseries=debian.currentseries, sourcepackagename='evolution')
     >>> logout()
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     task['self_link'], 'transitionToTarget',
-    ...     target=webservice.getAbsoluteUrl('/debian/+source/evolution'))
+    ...     target=webservice.getAbsoluteUrl('/debian/+source/evolution')))
     HTTP/1.1 301 Moved Permanently
     ...
     Location: http://api.launchpad.dev/beta/debian/+source/evolution/+bug/1
@@ -462,11 +464,11 @@
 
 We can also PATCH the target attribute to accomplish the same thing.
 
-    >>> print webservice.patch(
+    >>> print(webservice.patch(
     ...     task['self_link'].replace('mozilla-firefox', 'evolution'),
     ...     'application/json',
     ...     dumps({'target_link': webservice.getAbsoluteUrl(
-    ...         '/debian/+source/alsa-utils')}))
+    ...         '/debian/+source/alsa-utils')})))
     HTTP/1.1 301 Moved Permanently
     ...
     Location: http://api.launchpad.dev/beta/debian/+source/alsa-utils/+bug/1
@@ -478,16 +480,16 @@
     >>> task = webservice.get(
     ...     task['self_link'].replace(
     ...         'mozilla-firefox', 'alsa-utils')).jsonBody()
-    >>> print task['target_link']
+    >>> print(task['target_link'])
     http://api.../debian/+source/alsa-utils
 
 We can change an upstream task to target a different project.
 
     >>> product_bugtask = webservice.get(
     ...     webservice.getAbsoluteUrl('/jokosher/+bug/14')).jsonBody()
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     product_bugtask['self_link'], 'transitionToTarget',
-    ...     target=webservice.getAbsoluteUrl('/bzr'))
+    ...     target=webservice.getAbsoluteUrl('/bzr')))
     HTTP/1.1 301 Moved Permanently
     ...
     Location: http://api.launchpad.dev/beta/bzr/+bug/14
@@ -500,19 +502,19 @@
     ...     webservice.getAbsoluteUrl('/firefox/+bug/1')).jsonBody()
     >>> patch = {u'milestone_link':
     ...     webservice.getAbsoluteUrl('/firefox/+milestone/1.0')}
-    >>> print webservice.patch(
-    ...     firefox_bugtask['self_link'], 'application/json', dumps(patch))
+    >>> print(webservice.patch(
+    ...     firefox_bugtask['self_link'], 'application/json', dumps(patch)))
     HTTP/1.1 209 Content Returned
     ...
     <BLANKLINE>
     >>> firefox_bugtask = webservice.get(
     ...     webservice.getAbsoluteUrl('/firefox/+bug/1')).jsonBody()
-    >>> print firefox_bugtask['milestone_link']
+    >>> print(firefox_bugtask['milestone_link'])
     http://api.../firefox/+milestone/1.0
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     firefox_bugtask['self_link'],
     ...     'transitionToTarget',
-    ...     target=webservice.getAbsoluteUrl('/jokosher'))
+    ...     target=webservice.getAbsoluteUrl('/jokosher')))
     HTTP/1.1 301 Moved Permanently
     ...
     Location: http://api.launchpad.dev/beta/jokosher/+bug/1
@@ -521,13 +523,13 @@
     >>> jokosher_bugtask = webservice.get(
     ...     firefox_bugtask['self_link'].replace(
     ...         'firefox', 'jokosher')).jsonBody()
-    >>> print jokosher_bugtask['milestone_link']
+    >>> print(jokosher_bugtask['milestone_link'])
     None
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     jokosher_bugtask['self_link'],
     ...     'transitionToTarget',
-    ...     target=webservice.getAbsoluteUrl('/firefox'))
+    ...     target=webservice.getAbsoluteUrl('/firefox')))
     HTTP/1.1 301 Moved Permanently
     ...
     Location: http://api.launchpad.dev/beta/firefox/+bug/1
@@ -545,9 +547,9 @@
     >>> logout()
 
     >>> distro_bugtask = webservice.get(distro_bugtask_path)
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     distro_bugtask_path, 'transitionToTarget',
-    ...     target=webservice.getAbsoluteUrl('/ubuntu/+source/alsa-utils'))
+    ...     target=webservice.getAbsoluteUrl('/ubuntu/+source/alsa-utils')))
     HTTP/1.1 301 Moved Permanently
     ...
     Location: http://api.launchpad.dev/beta/ubuntu/+source/alsa-utils/+bug/...
@@ -557,9 +559,9 @@
 its findSimilarBugs() method. As it happens, there aren't any bugs
 similar to bug 1 for Firefox.
 
-    >>> print anon_webservice.named_get(
+    >>> print(anon_webservice.named_get(
     ...     firefox_bugtask['self_link'],
-    ...     'findSimilarBugs')
+    ...     'findSimilarBugs'))
     HTTP/1.1 200 Ok...
     {"total_size": 0, "start": 0, "entries": []}
 
@@ -576,9 +578,9 @@
     ...     webservice.getAbsoluteUrl('/firefox/+bug/%s' % new_bug['id'])
     ...     ).jsonBody()
 
-    >>> print anon_webservice.named_get(
+    >>> print(anon_webservice.named_get(
     ...     new_bug_task['self_link'],
-    ...     'findSimilarBugs')
+    ...     'findSimilarBugs'))
     HTTP/1.1 200 Ok...
     {"total_size": 4, "start": 0, "entries":...
     "id": 1... "title": "Firefox does not support SVG"...
@@ -633,9 +635,9 @@
 
 But John, an unprivileged user, wants it fixed in Fooix 0.1.1.
 
-    >>> print john_webservice.named_post(
+    >>> print(john_webservice.named_post(
     ...     '/bugs/%d' % bug.id, 'addNomination',
-    ...     target=john_webservice.getAbsoluteUrl('/fooix/0.1'))
+    ...     target=john_webservice.getAbsoluteUrl('/fooix/0.1')))
     HTTP/1.1 201 Created
     ...
     Location: http://.../bugs/.../nominations/...
@@ -665,13 +667,13 @@
 
     >>> nom_url = nominations['entries'][0]['self_link']
 
-    >>> print john_webservice.named_get(nom_url, 'canApprove').jsonBody()
+    >>> print(john_webservice.named_get(nom_url, 'canApprove').jsonBody())
     False
 
-    >>> print john_webservice.named_post(nom_url, 'approve')
+    >>> print(john_webservice.named_post(nom_url, 'approve'))
     HTTP/1.1 401 Unauthorized...
 
-    >>> print john_webservice.named_post(nom_url, 'decline')
+    >>> print(john_webservice.named_post(nom_url, 'decline'))
     HTTP/1.1 401 Unauthorized...
 
     >>> login('foo.bar@xxxxxxxxxxxxx')
@@ -683,10 +685,10 @@
 
     >>> eric_webservice = webservice_for_person(
     ...     eric, permission=OAuthPermission.WRITE_PRIVATE)
-    >>> print eric_webservice.named_post(nom_url, 'decline')
+    >>> print(eric_webservice.named_post(nom_url, 'decline'))
     HTTP/1.1 200 Ok...
 
-    >>> print eric_webservice.named_get(nom_url, 'canApprove').jsonBody()
+    >>> print(eric_webservice.named_get(nom_url, 'canApprove').jsonBody())
     True
 
     >>> login('foo.bar@xxxxxxxxxxxxx')
@@ -717,7 +719,7 @@
 
 Eric changes his mind, and approves the nomination.
 
-    >>> print eric_webservice.named_post(nom_url, 'approve')
+    >>> print(eric_webservice.named_post(nom_url, 'approve'))
     HTTP/1.1 200 Ok...
 
 This marks the nomination as Approved, and creates a new task.
@@ -748,7 +750,7 @@
 
 Eric cannot change his mind and decline the approved task.
 
-    >>> print eric_webservice.named_post(nom_url, 'decline')
+    >>> print(eric_webservice.named_post(nom_url, 'decline'))
     HTTP/1.1 400 Bad Request
     ...
     Cannot decline an approved nomination.
@@ -760,7 +762,7 @@
 
 While he can approve it again, it's a no-op.
 
-    >>> print eric_webservice.named_post(nom_url, 'approve')
+    >>> print(eric_webservice.named_post(nom_url, 'approve'))
     HTTP/1.1 200 Ok...
 
     >>> login('foo.bar@xxxxxxxxxxxxx')
@@ -770,14 +772,14 @@
 
 A bug cannot be nominated for a non-series.
 
-    >>> print john_webservice.named_get(
+    >>> print(john_webservice.named_get(
     ...     '/bugs/%d' % bug.id, 'canBeNominatedFor',
-    ...     target=john_webservice.getAbsoluteUrl('/fooix')).jsonBody()
+    ...     target=john_webservice.getAbsoluteUrl('/fooix')).jsonBody())
     False
 
-    >>> print john_webservice.named_post(
+    >>> print(john_webservice.named_post(
     ...     '/bugs/%d' % bug.id, 'addNomination',
-    ...     target=john_webservice.getAbsoluteUrl('/fooix'))
+    ...     target=john_webservice.getAbsoluteUrl('/fooix')))
     HTTP/1.1 400 Bad Request
     ...
     This bug cannot be nominated for Fooix.
@@ -785,14 +787,14 @@
 The bug also can't be nominated for Debuntu 5.0, as it has no
 Debuntu tasks.
 
-    >>> print john_webservice.named_get(
+    >>> print(john_webservice.named_get(
     ...     '/bugs/%d' % bug.id, 'canBeNominatedFor',
-    ...     target=john_webservice.getAbsoluteUrl('/debuntu/5.0')).jsonBody()
+    ...     target=john_webservice.getAbsoluteUrl('/debuntu/5.0')).jsonBody())
     False
 
-    >>> print john_webservice.named_post(
+    >>> print(john_webservice.named_post(
     ...     '/bugs/%d' % bug.id, 'addNomination',
-    ...     target=john_webservice.getAbsoluteUrl('/debuntu/5.0'))
+    ...     target=john_webservice.getAbsoluteUrl('/debuntu/5.0')))
     HTTP/1.1 400 Bad Request
     ...
     This bug cannot be nominated for Debuntu 5.0.
@@ -809,7 +811,7 @@
     ...     subscriptions['entries'], key=itemgetter('self_link'))
     >>> for entry in subscription_entries:
     ...     pprint_entry(entry)
-    ...     print
+    ...     print()
     bug_link: u'http://.../bugs/1'
     date_created: u'2006-10-16T18:31:43.156104+00:00'
     person_link: u'http://.../~name12'
@@ -840,7 +842,7 @@
 Subscriptions can also be accessed anonymously.
 
     >>> subscriptions = anon_webservice.get(bug_one_subscriptions_url).jsonBody()
-    >>> print subscriptions['entries'][0]['self_link']
+    >>> print(subscriptions['entries'][0]['self_link'])
     http://.../bugs/1/+subscription/stevea
 
 We can also create new subscriptions.
@@ -856,8 +858,8 @@
 An individual can only unsubscribe themselves.  If the person argument is
 not provided, the web service uses the calling user.
 
-    >>> print webservice.named_post(
-    ...     bug_one['self_link'], 'unsubscribe')
+    >>> print(webservice.named_post(
+    ...     bug_one['self_link'], 'unsubscribe'))
     HTTP/1.1 200 Ok...
 
 Using the devel api, an individual can subscribe themself at a given
@@ -891,9 +893,9 @@
 If one person tries to unsubscribe another individual, the web
 service will return an unauthorized error.
 
-    >>> print user_webservice.named_post(
+    >>> print(user_webservice.named_post(
     ...     bug_one['self_link'], 'unsubscribe',
-    ...     person=webservice.getAbsoluteUrl('/~mark'))
+    ...     person=webservice.getAbsoluteUrl('/~mark')))
     HTTP/1.1 401 Unauthorized...
 
 An individual can, however, unsubscribe a team to which they belong.
@@ -914,23 +916,23 @@
     >>> member_webservice = webservice_for_person(
     ...     ubuntu_team_member, permission=OAuthPermission.WRITE_PRIVATE)
 
-    >>> print member_webservice.named_post(
+    >>> print(member_webservice.named_post(
     ...     bug_one['self_link'], 'unsubscribe',
-    ...     person=webservice.getAbsoluteUrl('/~ubuntu-team'))
+    ...     person=webservice.getAbsoluteUrl('/~ubuntu-team')))
     HTTP/1.1 200 Ok...
 
 If someone who is not a member tries to unsubscribe the group,
 the web service will raise an unauthorized error.  To demonstrate
 this, the group must first be re-subscribed.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     bug_one['self_link'], 'subscribe',
-    ...     person=webservice.getAbsoluteUrl('/~ubuntu-team'))
+    ...     person=webservice.getAbsoluteUrl('/~ubuntu-team')))
     HTTP/1.1 200 Ok...
 
-    >>> print user_webservice.named_post(
+    >>> print(user_webservice.named_post(
     ...     bug_one['self_link'], 'unsubscribe',
-    ...     person=webservice.getAbsoluteUrl('/~ubuntu-team'))
+    ...     person=webservice.getAbsoluteUrl('/~ubuntu-team')))
     HTTP/1.1 401 Unauthorized...
 
 To determine if a user can unsubscribe a person or team,
@@ -940,14 +942,14 @@
 
 This example uses a subscription of SteveA.
 
-    >>> print subscription['person_link']
+    >>> print(subscription['person_link'])
     http://.../~stevea
 
 Salgado is the webservice user who performed the original subscription and so
 can unsubscribe SteveA.
 
-    >>> print webservice.named_get(
-    ...     subscription['self_link'], 'canBeUnsubscribedByUser').jsonBody()
+    >>> print(webservice.named_get(
+    ...     subscription['self_link'], 'canBeUnsubscribedByUser').jsonBody())
     True
 
 
@@ -967,9 +969,9 @@
 web service user himself (Salgado) so he has permission to unsubscribe
 himself.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     bug_six['self_link'], 'subscribe',
-    ...     person=webservice.getAbsoluteUrl('/~salgado'))
+    ...     person=webservice.getAbsoluteUrl('/~salgado')))
     HTTP/1.1 200 Ok...
 
 bug_six now has one subscriber, Salgado.
@@ -977,50 +979,50 @@
     >>> bug_six_subscriptions = webservice.get(
     ...     bug_six['subscriptions_collection_link']).jsonBody()
     >>> for entry in bug_six_subscriptions['entries']:
-    ...     print entry['person_link']
+    ...     print(entry['person_link'])
     http://.../~salgado
 
 Unsubscribe from bug_five, the primary bug, to unsubscribe from both
 it and its duplicate, bug_six.
 
-    >>> print webservice.named_post(
-    ...     bug_five['self_link'], 'unsubscribeFromDupes')
+    >>> print(webservice.named_post(
+    ...     bug_five['self_link'], 'unsubscribeFromDupes'))
     HTTP/1.1 200 Ok...
 
 Now bug_six has no subscibers.
 
     >>> bug_six_subscriptions = webservice.get(
     ...     bug_six['subscriptions_collection_link']).jsonBody()
-    >>> print bug_six_subscriptions['total_size']
+    >>> print(bug_six_subscriptions['total_size'])
     0
 
 Unsubscribing from duplicates is also supported for teams.
 To demonstrate, first subscribe Ubuntu Team to bug_six, the duplicate.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     bug_six['self_link'], 'subscribe',
-    ...     person=webservice.getAbsoluteUrl('/~ubuntu-team'))
+    ...     person=webservice.getAbsoluteUrl('/~ubuntu-team')))
     HTTP/1.1 200 Ok...
     >>> bug_six_subscriptions = webservice.get(
     ...     bug_six['subscriptions_collection_link']).jsonBody()
     >>> for entry in bug_six_subscriptions['entries']:
-    ...     print entry['person_link']
+    ...     print(entry['person_link'])
     http://.../~ubuntu-team
 
 Now, a team member can unsubscribe from bug_five to be unsubscribed
 from both it and the duplicate (bug_six).  Use the previously created
 member_webservice, which is for an Ubuntu Team member.
 
-    >>> print member_webservice.named_post(
+    >>> print(member_webservice.named_post(
     ...     bug_five['self_link'], 'unsubscribeFromDupes',
-    ...     person=webservice.getAbsoluteUrl('/~ubuntu-team'))
+    ...     person=webservice.getAbsoluteUrl('/~ubuntu-team')))
     HTTP/1.1 200 Ok...
 
 Now again, bug_six has no subscibers.
 
     >>> bug_six_subscriptions = webservice.get(
     ...     bug_six['subscriptions_collection_link']).jsonBody()
-    >>> print bug_six_subscriptions['total_size']
+    >>> print(bug_six_subscriptions['total_size'])
     0
 
 
@@ -1087,7 +1089,7 @@
     >>> patch = {u'url': u'http://www.example.com/'}
     >>> response = webservice.patch(
     ...     bug_watch_2000['self_link'], 'application/json', dumps(patch))
-    >>> print response
+    >>> print(response)
     HTTP/1.1 400 Bad Request...
     Content-Length: 47
     ...
@@ -1102,7 +1104,7 @@
     ...     bug_tracker=webservice.getAbsoluteUrl(
     ...                      '/bugs/bugtrackers/mozilla.org'),
     ...     remote_bug='9876')
-    >>> print response
+    >>> print(response)
     HTTP/1.1 201 Created...
     Content-Length: 0
     ...
@@ -1166,7 +1168,7 @@
     ...     }
     >>> response = webservice.patch(
     ...     bug_tracker['self_link'], 'application/json', dumps(patch))
-    >>> print response
+    >>> print(response)
     HTTP/1.1 301 Moved Permanently...
     Content-Length: 0
     ...
@@ -1179,7 +1181,7 @@
 Now notice that bug trackers (and bugs too) that are not found generate
 a 404 error, but do not generate an OOPS.
 
-    >>> print webservice.get(bug_tracker['self_link'])
+    >>> print(webservice.get(bug_tracker['self_link']))
     HTTP/1.1 404 Not Found...
     Content-Length: ...
     ...
@@ -1209,9 +1211,9 @@
 
 Non-admins can't disable a bugtracker through the API.
 
-    >>> print public_webservice.patch(
+    >>> print(public_webservice.patch(
     ...     bug_tracker_path, 'application/json',
-    ...     dumps(dict(active=False)))
+    ...     dumps(dict(active=False))))
     HTTP/1.1 401 Unauthorized
     ...
     (<BugTracker at ...>, 'active', 'launchpad.Admin')
@@ -1244,7 +1246,7 @@
     ...     bug_one['self_link'], 'addAttachment',
     ...     data="12345", filename="numbers.txt", content_type='foo/bar',
     ...     comment="The numbers you asked for.")
-    >>> print response
+    >>> print(response)
     HTTP/1.1 201 Created...
     Content-Length: 0
     ...
@@ -1287,7 +1289,7 @@
 we must follow to download the data.
 
     >>> data_response = webservice.get(attachment['data_link'])
-    >>> print data_response
+    >>> print(data_response)
     HTTP/1.1 303 See Other...
     Content-Length: 0
     Content-Type: text/plain
@@ -1343,7 +1345,7 @@
 
     >>> response = webservice.put(
     ...     attachment['data_link'], 'text/text', 'abcdefg')
-    >>> print response
+    >>> print(response)
     HTTP/1.1 405 Method Not Allowed
     ...
 
@@ -1361,7 +1363,7 @@
 
     >>> response = webservice.named_post(
     ...     attachment['self_link'], 'removeFromBug')
-    >>> print response
+    >>> print(response)
     HTTP/1.1 200 Ok
     ...
 
@@ -1569,7 +1571,7 @@
     ...     hardware_vendor_id='0x10de', hardware_product_id='0x045d',
     ...     hardware_owner_is_bug_reporter=True).jsonBody()['entries']
     >>> for bugtask in bugtasks:
-    ...     print bugtask['self_link']
+    ...     print(bugtask['self_link'])
     http://api.launchpad.dev/beta/ubuntu/+source/mozilla-firefox/+bug/1
     http://api.launchpad.dev/beta/ubuntu/+bug/2
 
@@ -1581,7 +1583,7 @@
     ...     hardware_owner_is_bug_reporter=True,
     ...     hardware_driver_name='ahci').jsonBody()['entries']
     >>> for bugtask in bugtasks:
-    ...     print bugtask['self_link']
+    ...     print(bugtask['self_link'])
     http://api.launchpad.dev/beta/ubuntu/+source/mozilla-firefox/+bug/1
     http://api.launchpad.dev/beta/ubuntu/+bug/2
 
@@ -1590,7 +1592,7 @@
     ...     hardware_vendor_id='0x10de', hardware_product_id='0x045d',
     ...     hardware_driver_name='nonsense',
     ...     hardware_owner_is_bug_reporter=True).jsonBody()['entries']
-    >>> print len(bugtasks)
+    >>> print(len(bugtasks))
     0
 
 We can search for bugs related to any hardware device controlled by a
@@ -1600,7 +1602,7 @@
     ...     '/ubuntu', 'searchTasks', hardware_driver_name='ahci',
     ...     hardware_owner_is_bug_reporter=True).jsonBody()['entries']
     >>> for bugtask in bugtasks:
-    ...     print bugtask['self_link']
+    ...     print(bugtask['self_link'])
     http://api.launchpad.dev/beta/ubuntu/+source/mozilla-firefox/+bug/1
     http://api.launchpad.dev/beta/ubuntu/+bug/2
 
@@ -1612,7 +1614,7 @@
     ...     hardware_driver_package_name='linux-image-2.6.24-19-generic',
     ...     hardware_owner_is_bug_reporter=True).jsonBody()['entries']
     >>> for bugtask in bugtasks:
-    ...     print bugtask['self_link']
+    ...     print(bugtask['self_link'])
     http://api.launchpad.dev/beta/ubuntu/+source/mozilla-firefox/+bug/1
     http://api.launchpad.dev/beta/ubuntu/+bug/2
 
@@ -1620,7 +1622,7 @@
     ...     '/ubuntu', 'searchTasks', hardware_driver_name='ahci',
     ...     hardware_driver_package_name='nonsense',
     ...     hardware_owner_is_bug_reporter=True).jsonBody()['entries']
-    >>> print len(bugtasks)
+    >>> print(len(bugtasks))
     0
 
 Instead of searching for bugs reported by the owner of a given device,
@@ -1630,7 +1632,7 @@
     ...     '/ubuntu', 'searchTasks', hardware_bus='PCI',
     ...     hardware_vendor_id='0x10de', hardware_product_id='0x045d',
     ...     hardware_owner_is_affected_by_bug=True).jsonBody()['entries']
-    >>> print len(bugtasks)
+    >>> print(len(bugtasks))
     0
 
     >>> login(ANONYMOUS)
@@ -1648,7 +1650,7 @@
     ...     hardware_vendor_id='0x10de', hardware_product_id='0x045d',
     ...     hardware_owner_is_affected_by_bug=True).jsonBody()['entries']
     >>> for bugtask in bugtasks:
-    ...     print bugtask['self_link']
+    ...     print(bugtask['self_link'])
     http://api.launchpad.dev/beta/ubuntu/+bug/2
 
 ...or for bugs to which owners of a given device are subscribed...
@@ -1658,7 +1660,7 @@
     ...     hardware_vendor_id='0x10de', hardware_product_id='0x045d',
     ...     hardware_owner_is_subscribed_to_bug=True).jsonBody()['entries']
     >>> for bugtask in bugtasks:
-    ...     print bugtask['self_link']
+    ...     print(bugtask['self_link'])
     http://api.launchpad.dev/beta/ubuntu/+source/mozilla-firefox/+bug/1
     http://api.launchpad.dev/beta/ubuntu/+source/thunderbird/+bug/9
 
@@ -1669,15 +1671,15 @@
     ...     '/ubuntu', 'searchTasks', hardware_bus='PCI',
     ...     hardware_vendor_id='0x10de', hardware_product_id='0x045d',
     ...     hardware_is_linked_to_bug=True).jsonBody()['entries']
-    >>> print len(bugtasks)
+    >>> print(len(bugtasks))
     0
 
     >>> bug_ten = webservice.get('/bugs/10').jsonBody()
     >>> sample_submission = webservice.get(
     ...     '/+hwdb/+submission/sample-submission').jsonBody()
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     bug_ten['self_link'], 'linkHWSubmission',
-    ...     submission=sample_submission['self_link'])
+    ...     submission=sample_submission['self_link']))
     HTTP/1.1 200 Ok
     ...
 
@@ -1686,7 +1688,7 @@
     ...     hardware_vendor_id='0x10de', hardware_product_id='0x045d',
     ...     hardware_is_linked_to_bug=True).jsonBody()['entries']
     >>> for bugtask in bugtasks:
-    ...     print bugtask['self_link']
+    ...     print(bugtask['self_link'])
     http://api.launchpad.dev/beta/ubuntu/+source/linux-source-2.6.15/+bug/10
 
 
@@ -1752,12 +1754,12 @@
 case a `400 Bad Request`-Error will be returned.
 
     >>> name12 = webservice.get("/~name12").jsonBody()
-    >>> print webservice.named_get(
+    >>> print(webservice.named_get(
     ...     '/~name16', 'searchTasks', assignee=name12['self_link'],
     ...     owner=name12['self_link'], bug_subscriber=name12['self_link'],
     ...     bug_commenter=name12['self_link'],
     ...     structural_subscriber=name12['self_link']
-    ...     )
+    ...     ))
     HTTP/1.1 400 Bad Request...
 
 
@@ -1786,8 +1788,8 @@
 
 It is possible to mark a bug as affecting the user using the web service.
 
-    >>> print webservice.named_post(
-    ...     bug_one['self_link'], 'isUserAffected').jsonBody()
+    >>> print(webservice.named_post(
+    ...     bug_one['self_link'], 'isUserAffected').jsonBody())
     None
     >>> webservice.named_post(
     ...     bug_one['self_link'], 'markUserAffected',
@@ -1885,14 +1887,14 @@
     >>> bug_links = webservice.get(
     ...     cve_entry['bugs_collection_link']).jsonBody()
     >>> for bug in bug_links['entries']:
-    ...     print bug['self_link']
+    ...     print(bug['self_link'])
     http://.../bugs/1
 
 Unlink CVEs from that bug.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     bug_one['self_link'], 'unlinkCVE',
-    ...     cve='http://api.launchpad.dev/beta/bugs/cve/1999-8979')
+    ...     cve='http://api.launchpad.dev/beta/bugs/cve/1999-8979'))
     HTTP/1.1 200 Ok...
     >>> pprint_collection(webservice.get(bug_one_cves_url).jsonBody())
     resource_type_link: u'http://.../#cve-page-resource'
@@ -1902,9 +1904,9 @@
 
 And link new CVEs to the bug.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     bug_one['self_link'], 'linkCVE',
-    ...     cve='http://api.launchpad.dev/beta/bugs/cve/2005-2733')
+    ...     cve='http://api.launchpad.dev/beta/bugs/cve/2005-2733'))
     HTTP/1.1 200 Ok...
     >>> pprint_collection(webservice.get(bug_one_cves_url).jsonBody())
     resource_type_link: u'http://.../#cve-page-resource'
@@ -1925,9 +1927,9 @@
     ...
 
     >>> redfish = webservice.get('/redfish').jsonBody()
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     bug_one['self_link'], 'addTask',
-    ...     target=redfish['self_link'])
+    ...     target=redfish['self_link']))
     HTTP/1.1 201 Created...
 
     >>> bugtasks_url = bug_one['bug_tasks_collection_link']
@@ -1946,9 +1948,9 @@
 
     >>> sample_submission = webservice.get(
     ...     '/+hwdb/+submission/sample-submission').jsonBody()
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     bug_one['self_link'], 'linkHWSubmission',
-    ...     submission=sample_submission['self_link'])
+    ...     submission=sample_submission['self_link']))
     HTTP/1.1 200 Ok
     ...
 
@@ -1957,7 +1959,7 @@
     >>> linked_submissions = webservice.named_get(
     ...     bug_one['self_link'], 'getHWSubmissions').jsonBody()
     >>> for submission in linked_submissions['entries']:
-    ...     print submission['submission_key']
+    ...     print(submission['submission_key'])
     sample-submission
 
 Private submissions are returned only for their owners and for admins.
@@ -1970,9 +1972,9 @@
 
     >>> private_submission = webservice.get(
     ...     '/+hwdb/+submission/private-submission').jsonBody()
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     bug_one['self_link'], 'linkHWSubmission',
-    ...     submission=private_submission['self_link'])
+    ...     submission=private_submission['self_link']))
     HTTP/1.1 200 Ok
     ...
 
@@ -1982,7 +1984,7 @@
     >>> linked_submissions = webservice.named_get(
     ...     bug_one['self_link'], 'getHWSubmissions').jsonBody()
     >>> for submission in linked_submissions['entries']:
-    ...     print submission['submission_key']
+    ...     print(submission['submission_key'])
     private-submission
     sample-submission
 
@@ -1994,7 +1996,7 @@
     >>> linked_submissions = no_priv_webservice.named_get(
     ...     bug_one['self_link'], 'getHWSubmissions').jsonBody()
     >>> for submission in linked_submissions['entries']:
-    ...     print submission['submission_key']
+    ...     print(submission['submission_key'])
     private-submission
     sample-submission
 
@@ -2008,22 +2010,22 @@
     >>> linked_submissions = sample_person_webservice.named_get(
     ...     bug_one['self_link'], 'getHWSubmissions').jsonBody()
     >>> for submission in linked_submissions['entries']:
-    ...     print submission['submission_key']
+    ...     print(submission['submission_key'])
     sample-submission
 
 We can delete links between a HWDB submission and a bug by calling
 bug.unlinkHWSubmission().
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     bug_one['self_link'], 'unlinkHWSubmission',
-    ...     submission=sample_submission['self_link'])
+    ...     submission=sample_submission['self_link']))
     HTTP/1.1 200 Ok
     ...
 
     >>> linked_submissions = webservice.named_get(
     ...     bug_one['self_link'], 'getHWSubmissions').jsonBody()
     >>> for submission in linked_submissions['entries']:
-    ...     print submission['submission_key']
+    ...     print(submission['submission_key'])
     private-submission
 
 Bug branches
@@ -2055,7 +2057,7 @@
     >>> branch_entry = bug_four_branches['entries'][0]
     >>> bug_link = webservice.get(
     ...     branch_entry['bug_link']).jsonBody()
-    >>> print bug_link['self_link']
+    >>> print(bug_link['self_link'])
     http://.../bugs/4
 
 Bug expiration
@@ -2069,22 +2071,22 @@
 Check to ensure that isExpirable() works without days_old.
 
     >>> bug_four = webservice.get("/bugs/4").jsonBody()
-    >>> print webservice.named_get(bug_four['self_link'],
-    ...     'isExpirable').jsonBody()
+    >>> print(webservice.named_get(bug_four['self_link'],
+    ...     'isExpirable').jsonBody())
     False
 
 Pass isExpirable() an integer for days_old.
 
     >>> bug_four = webservice.get("/bugs/4").jsonBody()
-    >>> print webservice.named_get(bug_four['self_link'], 'isExpirable',
-    ...     days_old='14').jsonBody()
+    >>> print(webservice.named_get(bug_four['self_link'], 'isExpirable',
+    ...     days_old='14').jsonBody())
     False
 
 Pass isExpirable() a string for days_old.
 
     >>> bug_four = webservice.get("/bugs/4").jsonBody()
-    >>> print webservice.named_get(bug_four['self_link'], 'isExpirable',
-    ...     days_old='sixty')
+    >>> print(webservice.named_get(bug_four['self_link'], 'isExpirable',
+    ...     days_old='sixty'))
     HTTP/1.1 400 Bad Request
     ...
     days_old: got 'unicode', expected int: u'sixty'

=== modified file 'lib/lp/bugs/stories/webservice/xx-hide-comments.txt'
--- lib/lp/bugs/stories/webservice/xx-hide-comments.txt	2011-12-20 11:30:27 +0000
+++ lib/lp/bugs/stories/webservice/xx-hide-comments.txt	2018-06-30 16:32:46 +0000
@@ -9,9 +9,9 @@
 indexed.  To hide the third comment in the list of comments
 for bug 11:
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     '/bugs/11', 'setCommentVisibility',
-    ...     comment_number=2, visible=False)
+    ...     comment_number=2, visible=False))
     HTTP/1.1 200 Ok
     ...
 
@@ -34,9 +34,9 @@
 Since the array order is the same, the message can be marked
 visible again.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     '/bugs/11', 'setCommentVisibility',
-    ...     comment_number=2, visible=True)
+    ...     comment_number=2, visible=True))
     HTTP/1.1 200 Ok
     ...
 
@@ -53,8 +53,8 @@
 This method can only be accessed by Launchpad admins.  (In the example
 above, the default "webservice" uses an admin account.)
 
-    >>> print user_webservice.named_post(
+    >>> print(user_webservice.named_post(
     ...     '/bugs/11', 'setCommentVisibility',
-    ...     comment_number=1, visible=False)
+    ...     comment_number=1, visible=False))
     HTTP/1.1 401 Unauthorized
     ...

=== modified file 'lib/lp/bugs/stories/xx-bugs-statistics-portlet.txt'
--- lib/lp/bugs/stories/xx-bugs-statistics-portlet.txt	2016-01-26 15:47:37 +0000
+++ lib/lp/bugs/stories/xx-bugs-statistics-portlet.txt	2018-06-30 16:32:46 +0000
@@ -77,7 +77,7 @@
 
 The content includes a link to the distribution CVE report.
 
-    >>> print user_browser.getLink('CVE report').url
+    >>> print(user_browser.getLink('CVE report').url)
     http://bugs.launchpad.dev/debian/+cve
 
 
@@ -146,7 +146,7 @@
 
 The content includes a link to the distribution CVE report.
 
-    >>> print user_browser.getLink('CVE report').url
+    >>> print(user_browser.getLink('CVE report').url)
     http://bugs.launchpad.dev/debian/woody/+cve
 
 
@@ -215,7 +215,7 @@
 Note that the "CVE reports" link is not shown above; distribution
 source packages do not have a CVE reports page.
 
-    >>> print user_browser.getLink('CVE report').url
+    >>> print(user_browser.getLink('CVE report').url)
     Traceback (most recent call last):
     ...
     LinkNotFoundError
@@ -287,7 +287,7 @@
 Note that the "CVE reports" link is not shown above; source packages
 do not have a CVE reports page.
 
-    >>> print user_browser.getLink('CVE report').url
+    >>> print(user_browser.getLink('CVE report').url)
     Traceback (most recent call last):
     ...
     LinkNotFoundError
@@ -359,7 +359,7 @@
 Note that the "CVE reports" link is not shown above; project groups do
 not have a CVE reports page.
 
-    >>> print user_browser.getLink('CVE report').url
+    >>> print(user_browser.getLink('CVE report').url)
     Traceback (most recent call last):
     ...
     LinkNotFoundError
@@ -431,5 +431,5 @@
 
 The content includes a link to the distribution CVE report.
 
-    >>> print user_browser.getLink('CVE report').url
+    >>> print(user_browser.getLink('CVE report').url)
     http://bugs.launchpad.dev/firefox/+cve

=== modified file 'lib/lp/bugs/tests/test_buglinktarget.py'
--- lib/lp/bugs/tests/test_buglinktarget.py	2016-06-24 23:35:24 +0000
+++ lib/lp/bugs/tests/test_buglinktarget.py	2018-06-30 16:32:46 +0000
@@ -29,23 +29,23 @@
 
 
 def questionSetUp(test):
-    setUp(test)
+    setUp(test, future=True)
     test.globs['target'] = getUtility(IQuestionSet).get(1)
 
 
 def cveSetUp(test):
-    setUp(test)
+    setUp(test, future=True)
     test.globs['target'] = getUtility(ICveSet)['2005-2730']
 
 
 def specificationSetUp(test):
-    setUp(test)
+    setUp(test, future=True)
     test.globs['target'] = getUtility(ISpecificationSet).getByURL(
         'http://wiki.mozilla.org/Firefox:1.1_Product_Team')
 
 
 def branchMergeProposalSetUp(test):
-    setUp(test)
+    setUp(test, future=True)
     factory = LaunchpadObjectFactory()
     test.globs['target'] = ProxyFactory(
         factory.makeBranchMergeProposalForGit())

=== modified file 'lib/lp/bugs/tests/test_bugtarget.py'
--- lib/lp/bugs/tests/test_bugtarget.py	2015-09-28 17:38:45 +0000
+++ lib/lp/bugs/tests/test_bugtarget.py	2018-06-30 16:32:46 +0000
@@ -42,7 +42,7 @@
 
 def productSetUp(test):
     """Setup the `IProduct` test."""
-    setUp(test)
+    setUp(test, future=True)
     test.globs['bugtarget'] = getUtility(IProductSet).getByName('firefox')
     test.globs['filebug'] = bugtarget_filebug
     test.globs['question_target'] = test.globs['bugtarget']
@@ -62,7 +62,7 @@
 
 def projectSetUp(test):
     """Setup the `IProjectGroup` test."""
-    setUp(test)
+    setUp(test, future=True)
     projectgroups = getUtility(IProjectGroupSet)
     test.globs['bugtarget'] = projectgroups.getByName('mozilla')
     test.globs['filebug'] = project_filebug
@@ -83,7 +83,7 @@
 
 def productSeriesSetUp(test):
     """Setup the `IProductSeries` test."""
-    setUp(test)
+    setUp(test, future=True)
     firefox = getUtility(IProductSet).getByName('firefox')
     test.globs['bugtarget'] = firefox.getSeries('trunk')
     test.globs['filebug'] = productseries_filebug
@@ -92,7 +92,7 @@
 
 def distributionSetUp(test):
     """Setup the `IDistribution` test."""
-    setUp(test)
+    setUp(test, future=True)
     test.globs['bugtarget'] = getUtility(IDistributionSet).getByName('ubuntu')
     test.globs['filebug'] = bugtarget_filebug
     test.globs['question_target'] = test.globs['bugtarget']
@@ -100,7 +100,7 @@
 
 def distributionSourcePackageSetUp(test):
     """Setup the `IDistributionSourcePackage` test."""
-    setUp(test)
+    setUp(test, future=True)
     ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
     test.globs['bugtarget'] = ubuntu.getSourcePackage('mozilla-firefox')
     test.globs['filebug'] = bugtarget_filebug
@@ -126,7 +126,7 @@
 
 def distributionSeriesSetUp(test):
     """Setup the `IDistroSeries` test."""
-    setUp(test)
+    setUp(test, future=True)
     ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
     test.globs['bugtarget'] = ubuntu.getSeries('hoary')
     test.globs['filebug'] = distroseries_filebug

=== modified file 'lib/lp/bugs/tests/test_doc.py'
--- lib/lp/bugs/tests/test_doc.py	2018-06-29 23:10:57 +0000
+++ lib/lp/bugs/tests/test_doc.py	2018-06-30 16:32:46 +0000
@@ -30,7 +30,10 @@
     LaunchpadFunctionalLayer,
     LaunchpadZopelessLayer,
     )
-from lp.testing.pages import PageTestSuite
+from lp.testing.pages import (
+    PageTestSuite,
+    setUpGlobs,
+    )
 from lp.testing.systemdocs import (
     LayeredDocFileSuite,
     setGlobs,
@@ -486,13 +489,15 @@
     suite = unittest.TestSuite()
 
     stories_dir = os.path.join(os.path.pardir, 'stories')
-    suite.addTest(PageTestSuite(stories_dir))
+    suite.addTest(PageTestSuite(
+        stories_dir, setUp=lambda test: setUpGlobs(test, future=True)))
     stories_path = os.path.join(here, stories_dir)
     for story_entry in scandir.scandir(stories_path):
         if not story_entry.is_dir():
             continue
         story_path = os.path.join(stories_dir, story_entry.name)
-        suite.addTest(PageTestSuite(story_path))
+        suite.addTest(PageTestSuite(
+            story_path, setUp=lambda test: setUpGlobs(test, future=True)))
 
     testsdir = os.path.abspath(
         os.path.normpath(os.path.join(here, os.path.pardir, 'doc'))

=== modified file 'lib/lp/bugs/tests/test_structuralsubscriptiontarget.py'
--- lib/lp/bugs/tests/test_structuralsubscriptiontarget.py	2015-01-29 14:14:01 +0000
+++ lib/lp/bugs/tests/test_structuralsubscriptiontarget.py	2018-06-30 16:32:46 +0000
@@ -491,7 +491,7 @@
 
 
 def distributionSourcePackageSetUp(test):
-    setUp(test)
+    setUp(test, future=True)
     ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
     test.globs['target'] = ubuntu.getSourcePackage('evolution')
     test.globs['other_target'] = ubuntu.getSourcePackage('pmount')
@@ -499,13 +499,13 @@
 
 
 def productSetUp(test):
-    setUp(test)
+    setUp(test, future=True)
     test.globs['target'] = getUtility(IProductSet).getByName('firefox')
     test.globs['filebug'] = bugtarget_filebug
 
 
 def distributionSetUp(test):
-    setUp(test)
+    setUp(test, future=True)
     test.globs['target'] = getUtility(IDistributionSet).getByName('ubuntu')
     test.globs['filebug'] = bugtarget_filebug
 
@@ -517,7 +517,7 @@
 
 
 def milestoneSetUp(test):
-    setUp(test)
+    setUp(test, future=True)
     firefox = getUtility(IProductSet).getByName('firefox')
     test.globs['target'] = firefox.getMilestone('1.0')
     test.globs['filebug'] = milestone_filebug
@@ -535,7 +535,7 @@
 
 
 def distroSeriesSourcePackageSetUp(test):
-    setUp(test)
+    setUp(test, future=True)
     test.globs['target'] = (
         getUtility(IDistributionSet).getByName('ubuntu').getSeries('hoary'))
     test.globs['filebug'] = distroseries_sourcepackage_filebug

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2018-04-21 10:01:22 +0000
+++ lib/lp/testing/factory.py	2018-06-30 16:32:46 +0000
@@ -51,6 +51,7 @@
 from lazr.jobrunner.jobrunner import SuspendJobException
 import pytz
 from pytz import UTC
+import six
 from twisted.conch.ssh.common import (
     MP,
     NS,
@@ -1204,7 +1205,7 @@
         else:
             make_sourcepackagename = (
                 sourcepackagename is None or
-                isinstance(sourcepackagename, str))
+                isinstance(sourcepackagename, six.text_type))
             if make_sourcepackagename:
                 sourcepackagename = self.makeSourcePackageName(
                     sourcepackagename)


Follow ups