← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:py3-bugs-doctest-unicode-strings into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:py3-bugs-doctest-unicode-strings into launchpad:master.

Commit message:
lp.bugs: Fix u'...' doctest examples for Python 3

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/397612
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:py3-bugs-doctest-unicode-strings into launchpad:master.
diff --git a/lib/lp/bugs/browser/tests/bug-views.txt b/lib/lp/bugs/browser/tests/bug-views.txt
index 2dccbf9..705ab03 100644
--- a/lib/lp/bugs/browser/tests/bug-views.txt
+++ b/lib/lp/bugs/browser/tests/bug-views.txt
@@ -161,11 +161,11 @@ grab a new view to actually see them:
     ...     (latest_ubuntu_bugtask, request), name="+index")
     >>> print(len(ubuntu_bugview.comments))
     3
-    >>> [ (c.index, c.owner.name, c.text_contents)
-    ...  for c in ubuntu_bugview.comments ]
-    [(0, u'name16', u'a bug in a bin pkg'),
-     (1, u'name16', u'I can reproduce this bug.'),
-     (2, u'name16', u'I can reproduce this bug.')]
+    >>> for c in ubuntu_bugview.comments:
+    ...     print("%d %s: %s" % (c.index, c.owner.name, c.text_contents))
+    0 name16: a bug in a bin pkg
+    1 name16: I can reproduce this bug.
+    2 name16: I can reproduce this bug.
 
 
 Description and Comment Display
@@ -207,8 +207,9 @@ indentical to the second, we really only display one comment:
 
     >>> 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.')]
+    >>> for c in comments:
+    ...     print("%d %s: %s" % (c.index, c.owner.name, c.text_contents))
+    1 name16: I can reproduce this bug.
 
 (Unregister our listener, since we no longer need it.)
 
@@ -295,18 +296,22 @@ librarian file of the attachment.
     ...     bug=bug_seven, description='patch 2', is_patch=True,
     ...     filename='p2')
     >>> view = BugView(bug_seven, request)
-    >>> [attachment['attachment'].title
-    ...  for attachment in view.regular_attachments]
-    [u'attachment 1', u'attachment 2']
-    >>> [patch['attachment'].title for patch in view.patches]
-    [u'patch 1', u'patch 2']
-    >>> [attachment['file'].http_url
-    ...  for attachment in view.regular_attachments]
-    [u'http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/a1',
-     u'http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/a2']
-    >>> [patch['file'].http_url for patch in view.patches]
-    [u'http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/p1',
-     u'http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/p2']
+    >>> for attachment in view.regular_attachments:
+    ...     print(attachment['attachment'].title)
+    attachment 1
+    attachment 2
+    >>> for patch in view.patches:
+    ...     print(patch['attachment'].title)
+    patch 1
+    patch 2
+    >>> for attachment in view.regular_attachments:
+    ...     print(attachment['file'].http_url)
+    http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/a1
+    http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/a2
+    >>> for patch in view.patches:
+    ...     print(patch['file'].http_url)
+    http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/p1
+    http://bugs.launchpad.test/firefox/+bug/5/+attachment/.../+files/p2
 
 
 Bug Navigation
@@ -609,8 +614,8 @@ initialize the test harness.
     ...         print('EDIT BUG')
 
     >>> firefox_task = bug_one.bugtasks[0]
-    >>> firefox_task.bugtargetdisplayname
-    u'Mozilla Firefox'
+    >>> print(firefox_task.bugtargetdisplayname)
+    Mozilla Firefox
     >>> from lp.testing.deprecated import LaunchpadFormHarness
     >>> bug_edit = LaunchpadFormHarness(firefox_task, BugEditViewTest)
 
@@ -637,12 +642,13 @@ and the bug will have been edited.
     False
     >>> bug_edit.wasRedirected()
     True
-    >>> bug_one.title
-    u'New title'
-    >>> bug_one.description
-    u'New description.'
-    >>> bug_one.tags
-    [u'doc']
+    >>> print(bug_one.title)
+    New title
+    >>> print(bug_one.description)
+    New description.
+    >>> for tag in bug_one.tags:
+    ...     print(tag)
+    doc
 
 Emails are sent out by adding entries to the bugnotification table. We
 need to know how many messages are currently in that table.
@@ -661,8 +667,10 @@ once.
     False
     >>> bug_edit.wasRedirected()
     True
-    >>> bug_one.tags
-    [u'doc', u'new-tag']
+    >>> for tag in bug_one.tags:
+    ...     print(tag)
+    doc
+    new-tag
 
 Since the 'new-tag' was added, a new entry in the bugnotification table
 should exist.
@@ -671,8 +679,8 @@ should exist.
     ...     BugNotification, bug=bug_one).order_by(BugNotification.id)
     >>> start_bugnotification_count == bn_set.count() - 1
     True
-    >>> bn_set.last().message.text_contents
-    u'** Tags added: new-tag'
+    >>> print(bn_set.last().message.text_contents)
+    ** Tags added: new-tag
 
 
 Displaying BugActivity interleaved with comments
diff --git a/lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt b/lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt
index 8e0bb99..b77c3c0 100644
--- a/lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt
+++ b/lib/lp/bugs/browser/tests/bugtarget-filebug-views.txt
@@ -39,11 +39,11 @@ directly.
     True
 
     >>> filebug_view.submit_bug_action.success(bug_data)
-    >>> filebug_view.added_bug.title
-    u'Test Title'
+    >>> print(filebug_view.added_bug.title)
+    Test Title
 
-    >>> filebug_view.added_bug.description
-    u'Test description.'
+    >>> print(filebug_view.added_bug.description)
+    Test description.
 
 
 URLs to additional FileBug elements
@@ -88,11 +88,11 @@ directly.
     True
 
     >>> filebug_view.submit_bug_action.success(bug_data)
-    >>> filebug_view.added_bug.title
-    u'Test Title'
+    >>> print(filebug_view.added_bug.title)
+    Test Title
 
-    >>> filebug_view.added_bug.description
-    u'Test description.'
+    >>> print(filebug_view.added_bug.description)
+    Test description.
 
     >>> for tag in filebug_view.added_bug.tags:
     ...     print(tag)
@@ -114,8 +114,8 @@ The base class allows security bugs to be filed.
     True
 
     >>> filebug_view.submit_bug_action.success(bug_data)
-    >>> filebug_view.added_bug.title
-    u'Security bug'
+    >>> print(filebug_view.added_bug.title)
+    Security bug
 
     >>> filebug_view.added_bug.security_related
     True
diff --git a/lib/lp/bugs/browser/tests/bugtask-adding-views.txt b/lib/lp/bugs/browser/tests/bugtask-adding-views.txt
index b0200e2..bcadad6 100644
--- a/lib/lp/bugs/browser/tests/bugtask-adding-views.txt
+++ b/lib/lp/bugs/browser/tests/bugtask-adding-views.txt
@@ -33,8 +33,9 @@ our meta view.
 
 We haven't posted the form, so we'll see one button.
 
-    >>> [action.label for action in add_task_view.actions]
-    [u'Continue']
+    >>> for action in add_task_view.actions:
+    ...     print(action.label)
+    Continue
 
 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.
@@ -59,8 +60,8 @@ that product is required:
     ...     'field.__visited_steps__': 'choose_product'}
     >>> add_task_view = get_and_setup_view(
     ...     firefox_task, '+choose-affected-product', form)
-    >>> add_task_view.getFieldError('product')
-    u'Required input is missing.'
+    >>> print(add_task_view.getFieldError('product'))
+    Required input is missing.
 
 
 If we supply a valid product, it will move on to the next step.
@@ -88,8 +89,8 @@ If the validation fails, an error will be displayed.
     ...     firefox_task, '+choose-affected-product', form)
     >>> add_task_view.step_name
     'choose_product'
-    >>> add_task_view.getFieldError('product')
-    u'A fix for this bug has already been requested for Mozilla Firefox'
+    >>> print(add_task_view.getFieldError('product'))
+    A fix for this bug has already been requested for Mozilla Firefox
 
 
 When adding a product from an upstream task, we always have to choose
@@ -117,8 +118,8 @@ selected.
     ...     method='GET')
     >>> add_task_view.step_name
     'specify_remote_bug_url'
-    >>> add_task_view.widgets['product'].getInputValue().name
-    u'firefox'
+    >>> print(add_task_view.widgets['product'].getInputValue().name)
+    firefox
 
 If some package doesn't have a packaging link, a product will have to
 be chosen manually, and the user may choose to link the package to the
@@ -252,8 +253,8 @@ tried to create a bugtask without a bug watch.
 
     >>> len(add_task_view.errors)
     1
-    >>> add_task_view.getFieldError('bug_url')
-    u'Required input is missing.'
+    >>> print(add_task_view.getFieldError('bug_url'))
+    Required input is missing.
     >>> add_task_view.next_url is None
     True
 
@@ -370,8 +371,8 @@ attributes got initialized to Unknown. The bugtask will be synced with
 the bug watch's status later.
 
     >>> alsa_task = bug_four.bugtasks[0]
-    >>> alsa_task.bugtargetname
-    u'alsa-utils'
+    >>> print(alsa_task.bugtargetname)
+    alsa-utils
     >>> alsa_task.product.bug_tracking_usage
     <DBItem ServiceUsage.UNKNOWN, (10) Unknown>
     >>> alsa_task.bugwatch == bug_four.watches[0]
@@ -416,8 +417,8 @@ confirmation page.
     ...     firefox_task, '+choose-affected-product', form)
     >>> add_task_view.errors
     []
-    >>> add_task_view.getTarget().displayname
-    u'Mozilla Thunderbird'
+    >>> print(add_task_view.getTarget().displayname)
+    Mozilla Thunderbird
 
 If we request a fix in a source package, the distribution's display
 name is returned.
@@ -427,8 +428,8 @@ name is returned.
     ...     'field.sourcepackagename': u'evolution'}
     >>> add_task_view = get_and_setup_view(
     ...     firefox_task, '+distrotask', form)
-    >>> add_task_view.getTarget().displayname
-    u'Debian'
+    >>> print(add_task_view.getTarget().displayname)
+    Debian
 
 
 The form also accept binary package names to be entered. The binary
@@ -503,8 +504,9 @@ bugs.foo.org, we'll present 'The Foo Product' as a candidate.
     >>> add_task_view = create_view(
     ...     firefox_task, '+affects-new-product', form=form, method='POST')
     >>> add_task_view.initialize()
-    >>> [product.name for product in add_task_view.existing_products]
-    [u'foo-product']
+    >>> for product in add_task_view.existing_products:
+    ...     print(product.name)
+    foo-product
 
     # Now we choose to register the product anyway, as it's not one of the
     # existing ones.
@@ -527,15 +529,18 @@ we'll present only the ones whose name is similar to what the user entered.
     >>> add_task_view.initialize()
     >>> add_task_view.MAX_PRODUCTS_TO_DISPLAY
     10
-    >>> [product.name for product in add_task_view.existing_products]
-    [u'bar-product', u'foo-product']
+    >>> for product in add_task_view.existing_products:
+    ...     print(product.name)
+    bar-product
+    foo-product
 
     >>> add_task_view = create_view(
     ...     firefox_task, '+affects-new-product', form=form, method='POST')
     >>> add_task_view.MAX_PRODUCTS_TO_DISPLAY = 1
     >>> add_task_view.initialize()
-    >>> [product.name for product in add_task_view.existing_products]
-    [u'foo-product']
+    >>> for product in add_task_view.existing_products:
+    ...     print(product.name)
+    foo-product
 
 Here another user will choose to report a bug on the existing project.
 Note that we use another user to make sure our code doesn't attempt to
diff --git a/lib/lp/bugs/browser/tests/bugtask-edit-views.txt b/lib/lp/bugs/browser/tests/bugtask-edit-views.txt
index ab26edd..1896e5e 100644
--- a/lib/lp/bugs/browser/tests/bugtask-edit-views.txt
+++ b/lib/lp/bugs/browser/tests/bugtask-edit-views.txt
@@ -52,8 +52,8 @@ assigned to linux-source-2.6.15 instead.
     >>> edit_view = getMultiAdapter(
     ...     (ubuntu_thunderbird_task, request), name='+editstatus')
     >>> edit_view.initialize()
-    >>> ubuntu_thunderbird_task.sourcepackagename.name
-    u'linux-source-2.6.15'
+    >>> print(ubuntu_thunderbird_task.sourcepackagename.name)
+    linux-source-2.6.15
 
 A notification was added informing the user that the binary package was
 changed to the corresponding source package.
@@ -83,9 +83,9 @@ exist in Launchpad. we'll get an error message.
     ...     (ubuntu_thunderbird_task, request), name='+editstatus')
     >>> edit_view.initialize()
     >>> for error in edit_view.errors:
-    ...     print(error)
-    (u'ubuntu_thunderbird.target', u'Target',
-     LaunchpadValidationError(u'There is no package named
+    ...     print(pretty(error.args))
+    ('ubuntu_thunderbird.target', 'Target',
+     LaunchpadValidationError(...'There is no package named
      &#x27;no-such-package&#x27; published in Ubuntu.'))
 
 An error is reported to the user when a bug is retargeted and there is
@@ -98,8 +98,8 @@ an existing task for the same target.
     >>> login('test@xxxxxxxxxxxxx')
     >>> bug_seven = getUtility(IBugSet).get(7)
     >>> product_task = bug_seven.bugtasks[0]
-    >>> product_task.bugtargetname
-    u'evolution'
+    >>> print(product_task.bugtargetname)
+    evolution
 
     >>> edit_form = {
     ...    'evolution.actions.save': 'Save Changes',
@@ -117,8 +117,8 @@ an existing task for the same target.
     >>> edit_view.initialize()
     >>> [str(error) for error in edit_view.errors]
     []
-    >>> product_task.bugtargetname
-    u'firefox'
+    >>> print(product_task.bugtargetname)
+    firefox
 
 If no product name is given, an error message is displayed.
 
@@ -137,8 +137,8 @@ If no product name is given, an error message is displayed.
     ...     (product_task, request), name='+editstatus')
     >>> edit_view.initialize()
     >>> for error in edit_view.errors:
-    ...     print(error)
-    ('product', u'Project', RequiredMissing('product'))
+    ...     print(pretty(error.args))
+    ('product', 'Project', RequiredMissing('product'))
 
 
 == Bug Watch Linkage ==
diff --git a/lib/lp/bugs/browser/tests/bugtask-search-views.txt b/lib/lp/bugs/browser/tests/bugtask-search-views.txt
index 537c680..97120cc 100644
--- a/lib/lp/bugs/browser/tests/bugtask-search-views.txt
+++ b/lib/lp/bugs/browser/tests/bugtask-search-views.txt
@@ -132,9 +132,10 @@ We can filter our search results by reporter
 
     >>> bugtasks_filtered_by_reporter = list(
     ...     distro_advanced_search_listingview.search().batch)
-    >>> [(bugtask.bug.id, bugtask.bug.owner.name)
-    ...     for bugtask in bugtasks_filtered_by_reporter]
-    [(1, u'name12'), (2, u'name12')]
+    >>> for bugtask in bugtasks_filtered_by_reporter:
+    ...     print('%d: %s' % (bugtask.bug.id, bugtask.bug.owner.name))
+    1: name12
+    2: name12
 
 But if we query for an invalid person, the view displays a nice error
 message.
@@ -148,8 +149,8 @@ message.
     >>> distro_advanced_search_listingview = create_view(
     ...     debian, '+bugs', form_values)
 
-    >>> distro_advanced_search_listingview.getFieldError('bug_reporter')
-    u'There&#x27;s no person with the name or email address &#x27;invalid-reporter&#x27;.'
+    >>> print(distro_advanced_search_listingview.getFieldError('bug_reporter'))
+    There&#x27;s no person with the name or email address &#x27;invalid-reporter&#x27;.
 
 The same if we try with an invalid assignee.
 
@@ -162,8 +163,8 @@ The same if we try with an invalid assignee.
     >>> distro_advanced_search_listingview = create_view(
     ...     debian, '+bugs', form_values)
 
-    >>> distro_advanced_search_listingview.getFieldError('assignee')
-    u'There&#x27;s no person with the name or email address &#x27;invalid-assignee&#x27;.'
+    >>> print(distro_advanced_search_listingview.getFieldError('assignee'))
+    There&#x27;s no person with the name or email address &#x27;invalid-assignee&#x27;.
 
 Searching by component is possible, as long as the context has defined a
 .currentseries.
diff --git a/lib/lp/bugs/browser/tests/person-bug-views.txt b/lib/lp/bugs/browser/tests/person-bug-views.txt
index d4cd88a..439512d 100644
--- a/lib/lp/bugs/browser/tests/person-bug-views.txt
+++ b/lib/lp/bugs/browser/tests/person-bug-views.txt
@@ -55,17 +55,18 @@ unassigned bug tasks.
 
     >>> reported_bugtasks_view = create_view(name16, '+reportedbugs')
     >>> reported_bugtasks = list(reported_bugtasks_view.search().batch)
-    >>> sorted([(bugtask.bug.id, bugtask.status.name, bugtask.bug.owner.name,
-    ...          getattr(bugtask.assignee, 'name', None))
-    ...         for bugtask in reported_bugtasks])
-    [(3, 'NEW', u'name16', None),
-     (7, 'NEW', u'name16', u'name16'),
-     (9, 'CONFIRMED', u'name16', None),
-     (10, 'NEW', u'name16', None),
-     (11, 'NEW', u'name16', None),
-     (12, 'CONFIRMED', u'name16', None),
-     (15, 'NEW', u'name16', None),
-     (15, 'NEW', u'name16', None)]
+    >>> print(pretty(sorted([
+    ...     (bugtask.bug.id, bugtask.status.name, bugtask.bug.owner.name,
+    ...      getattr(bugtask.assignee, 'name', None))
+    ...     for bugtask in reported_bugtasks])))
+    [(3, 'NEW', 'name16', None),
+     (7, 'NEW', 'name16', 'name16'),
+     (9, 'CONFIRMED', 'name16', None),
+     (10, 'NEW', 'name16', None),
+     (11, 'NEW', 'name16', None),
+     (12, 'CONFIRMED', 'name16', None),
+     (15, 'NEW', 'name16', None),
+     (15, 'NEW', 'name16', None)]
 
 But the advanced search allows us to query only the bug tasks that aren't
 assigned.
@@ -78,16 +79,17 @@ assigned.
     >>> reported_bugtasks = sorted(
     ...     reported_bugtasks_view.search().batch,
     ...     key=lambda bugtask: (bugtask.bug.id, bugtask.id))
-    >>> [(bugtask.bug.id, bugtask.status.name, bugtask.bug.owner.name,
-    ...   getattr(bugtask.assignee, 'name', None))
-    ... for bugtask in reported_bugtasks]
-    [(3, 'NEW', u'name16', None),
-     (9, 'CONFIRMED', u'name16', None),
-     (10, 'NEW', u'name16', None),
-     (11, 'NEW', u'name16', None),
-     (12, 'CONFIRMED', u'name16', None),
-     (15, 'NEW', u'name16', None),
-     (15, 'NEW', u'name16', None)]
+    >>> print(pretty([
+    ...     (bugtask.bug.id, bugtask.status.name, bugtask.bug.owner.name,
+    ...      getattr(bugtask.assignee, 'name', None))
+    ...     for bugtask in reported_bugtasks]))
+    [(3, 'NEW', 'name16', None),
+     (9, 'CONFIRMED', 'name16', None),
+     (10, 'NEW', 'name16', None),
+     (11, 'NEW', 'name16', None),
+     (12, 'CONFIRMED', 'name16', None),
+     (15, 'NEW', 'name16', None),
+     (15, 'NEW', 'name16', None)]
 
 Using the advanced form we can also query for closed bugs reported by someone.
 Let's first close a bug setting its status to 'Invalid'.
@@ -164,10 +166,11 @@ Using the advanced form we can query for closed bugs someone is subscribed to.
     ...     name12, '+subscribedbugs', form)
     >>> closed_subscribed_bugtasks = list(
     ...     closed_subscribed_bugtasks_view.search().batch)
-    >>> sorted([(bugtask.bug.id, bugtask.status.name,
-    ...          getattr(bugtask.assignee, 'name', None))
-    ...         for bugtask in closed_subscribed_bugtasks])
-    [(8, 'FIXRELEASED', u'name16')]
+    >>> print(pretty(sorted([
+    ...     (bugtask.bug.id, bugtask.status.name,
+    ...      getattr(bugtask.assignee, 'name', None))
+    ...     for bugtask in closed_subscribed_bugtasks])))
+    [(8, 'FIXRELEASED', 'name16')]
 
 
 Bugs for Bug Supervisor
@@ -187,10 +190,10 @@ render the overview report.
     2
     >>> ubuntu_firefox_bugcounts = package_bug_counts[0]
 
-    >>> ubuntu_firefox_bugcounts['package_name']
-    u'mozilla-firefox in Ubuntu'
-    >>> ubuntu_firefox_bugcounts['package_search_url']
-    u'http://bugs.launchpad.test/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['package_name'])
+    mozilla-firefox in Ubuntu
+    >>> print(ubuntu_firefox_bugcounts['package_search_url'])
+    http://bugs.launchpad.test/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'])
     1
@@ -201,14 +204,14 @@ render the overview report.
     >>> print(ubuntu_firefox_bugcounts['inprogress_bugs_count'])
     0
 
-    >>> ubuntu_firefox_bugcounts['open_bugs_url']
-    u'http://bugs.launchpad.test/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'
-    >>> ubuntu_firefox_bugcounts['critical_bugs_url']
-    u'http://bugs.launchpad.test/ubuntu/+source/mozilla-firefox?field.importance=Critical&field.status=New&field.status=Incomplete&field.status=Confirmed&field.status=Triaged&field.status=In+Progress&field.status=Fix+Committed&search=Search'
-    >>> ubuntu_firefox_bugcounts['unassigned_bugs_url']
-    u'http://bugs.launchpad.test/ubuntu/+source/mozilla-firefox?assignee_option=none&field.status=New&field.status=Incomplete&field.status=Confirmed&field.status=Triaged&field.status=In+Progress&field.status=Fix+Committed&search=Search'
-    >>> ubuntu_firefox_bugcounts['inprogress_bugs_url']
-    u'http://bugs.launchpad.test/ubuntu/+source/mozilla-firefox?field.status=In+Progress&search=Search'
+    >>> print(ubuntu_firefox_bugcounts['open_bugs_url'])
+    http://bugs.launchpad.test/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['critical_bugs_url'])
+    http://bugs.launchpad.test/ubuntu/+source/mozilla-firefox?field.importance=Critical&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['unassigned_bugs_url'])
+    http://bugs.launchpad.test/ubuntu/+source/mozilla-firefox?assignee_option=none&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['inprogress_bugs_url'])
+    http://bugs.launchpad.test/ubuntu/+source/mozilla-firefox?field.status=In+Progress&search=Search
 
 The total number of bugs, broken down in the same ways as the package
 bug counts, is also available.
@@ -277,8 +280,8 @@ alter the total bug counts but not the firefox ones. Here, we use the
 other package listed in name16's package bug listing overview, which is
 pmount:
 
-    >>> packagebugs_search_view.package_bug_counts[1]['package_name']
-    u'pmount in Ubuntu'
+    >>> print(packagebugs_search_view.package_bug_counts[1]['package_name'])
+    pmount in Ubuntu
 
     >>> pmount = ubuntu.getSourcePackage('pmount')
     >>> new_bug = pmount.createBug(bug_params)
diff --git a/lib/lp/bugs/doc/bug-set-status.txt b/lib/lp/bugs/doc/bug-set-status.txt
index 0626c48..af6cd66 100644
--- a/lib/lp/bugs/doc/bug-set-status.txt
+++ b/lib/lp/bugs/doc/bug-set-status.txt
@@ -32,8 +32,8 @@ changing the status, the target, and the new status.
 
 It returns the edited bugtask.
 
-    >>> firefox_bugtask.target.name
-    u'firefox'
+    >>> print(firefox_bugtask.target.name)
+    firefox
     >>> firefox_bugtask.status.name
     'CONFIRMED'
 
@@ -52,12 +52,12 @@ BugActivity records are created.
 
     >>> from lp.bugs.model.bugactivity import BugActivity
     >>> latest_activity = BugActivity.selectFirst(orderBy='-id')
-    >>> latest_activity.whatchanged
-    u'firefox: status'
-    >>> latest_activity.oldvalue
-    u'New'
-    >>> latest_activity.newvalue
-    u'Confirmed'
+    >>> print(latest_activity.whatchanged)
+    firefox: status
+    >>> print(latest_activity.oldvalue)
+    New
+    >>> print(latest_activity.newvalue)
+    Confirmed
 
 The edited bugtask is only returned if it's actually edited. If the
 bugtask already has the specified status, None is returned.
@@ -77,8 +77,8 @@ product, not the product series, the product bugtask is edited.
     True
     >>> firefox_bugtask = bug.setStatus(
     ...     firefox_trunk, BugTaskStatus.NEW, no_priv)
-    >>> firefox_bugtask.target.name
-    u'firefox'
+    >>> print(firefox_bugtask.target.name)
+    firefox
     >>> firefox_bugtask.status.name
     'NEW'
 
@@ -101,8 +101,8 @@ is edited.
     >>> firefox_trunk_bugtask = bug.setStatus(
     ...     firefox_trunk, BugTaskStatus.INCOMPLETE, no_priv)
 
-    >>> firefox_trunk_bugtask.target.name
-    u'trunk'
+    >>> print(firefox_trunk_bugtask.target.name)
+    trunk
     >>> firefox_trunk_bugtask.status.name
     'INCOMPLETE'
 
@@ -111,16 +111,16 @@ master will be edited and returned. The conjoined slave is of course
 updated automatically.
 
     >>> firefox_bugtask = firefox_trunk_bugtask.conjoined_slave
-    >>> firefox_bugtask.target.name
-    u'firefox'
+    >>> print(firefox_bugtask.target.name)
+    firefox
     >>> firefox_bugtask.conjoined_master is not None
     True
     >>> firefox_bugtask.status.name
     'INCOMPLETE'
     >>> firefox_trunk_bugtask = bug.setStatus(
     ...     firefox_bugtask.target, BugTaskStatus.CONFIRMED, no_priv)
-    >>> firefox_trunk_bugtask.target.name
-    u'trunk'
+    >>> print(firefox_trunk_bugtask.target.name)
+    trunk
     >>> firefox_trunk_bugtask.status.name
     'CONFIRMED'
     >>> firefox_bugtask.status.name
@@ -151,8 +151,8 @@ for product tasks.
     >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
     >>> ubuntu_bugtask = bug.setStatus(
     ...     ubuntu, BugTaskStatus.CONFIRMED, no_priv)
-    >>> ubuntu_bugtask.target.name
-    u'ubuntu'
+    >>> print(ubuntu_bugtask.target.name)
+    ubuntu
     >>> ubuntu_bugtask.status.name
     'CONFIRMED'
 
@@ -173,8 +173,8 @@ edited.
     ...     ubuntu_firefox, getUtility(ILaunchBag).user)
     >>> ubuntu_firefox_task = bug.setStatus(
     ...     ubuntu_firefox, BugTaskStatus.INCOMPLETE, no_priv)
-    >>> ubuntu_firefox_task.target.displayname
-    u'mozilla-firefox in Ubuntu'
+    >>> print(ubuntu_firefox_task.target.displayname)
+    mozilla-firefox in Ubuntu
     >>> ubuntu_firefox_task.status.name
     'INCOMPLETE'
 
@@ -186,8 +186,8 @@ edited.
     >>> warty_firefox = ubuntu_warty.getSourcePackage('mozilla-firefox')
     >>> ubuntu_firefox_task = bug.setStatus(
     ...     warty_firefox, BugTaskStatus.CONFIRMED, no_priv)
-    >>> ubuntu_firefox_task.target.displayname
-    u'mozilla-firefox in Ubuntu'
+    >>> print(ubuntu_firefox_task.target.displayname)
+    mozilla-firefox in Ubuntu
     >>> ubuntu_firefox_task.status.name
     'CONFIRMED'
 
@@ -195,8 +195,8 @@ edited.
     >>> hoary_firefox = ubuntu_hoary.getSourcePackage('mozilla-firefox')
     >>> ubuntu_firefox_task = bug.setStatus(
     ...     hoary_firefox, BugTaskStatus.NEW, no_priv)
-    >>> ubuntu_firefox_task.target.displayname
-    u'mozilla-firefox in Ubuntu'
+    >>> print(ubuntu_firefox_task.target.displayname)
+    mozilla-firefox in Ubuntu
     >>> ubuntu_firefox_task.status.name
     'NEW'
 
@@ -204,8 +204,8 @@ However, if the bug is targeted to the current series, passing a
 non-current series won't modify any bugtask, unless the bug is already
 targeted to the non-current series of course.
 
-    >>> ubuntu.currentseries.name
-    u'hoary'
+    >>> print(ubuntu.currentseries.name)
+    hoary
 
     # Need to be privileged user to target the bug to a series.
     >>> login('foo.bar@xxxxxxxxxxxxx')
@@ -243,7 +243,7 @@ targeted to the non-current series of course.
     >>> warty_firefox = ubuntu_warty.getSourcePackage('mozilla-firefox')
     >>> ubuntu_firefox_task = bug.setStatus(
     ...     warty_firefox, BugTaskStatus.INCOMPLETE, no_priv)
-    >>> ubuntu_firefox_task.target.displayname
-    u'mozilla-firefox in Ubuntu Warty'
+    >>> print(ubuntu_firefox_task.target.displayname)
+    mozilla-firefox in Ubuntu Warty
     >>> ubuntu_firefox_task.status.name
     'INCOMPLETE'
diff --git a/lib/lp/bugs/doc/bug-tags.txt b/lib/lp/bugs/doc/bug-tags.txt
index 45ccac4..b6aee57 100644
--- a/lib/lp/bugs/doc/bug-tags.txt
+++ b/lib/lp/bugs/doc/bug-tags.txt
@@ -16,8 +16,10 @@ order the tags are in, the result will be ordered alphabetically:
 
     >>> login('test@xxxxxxxxxxxxx')
     >>> bug_one.tags = [u'svg', u'sco']
-    >>> bug_one.tags
-    [u'sco', u'svg']
+    >>> for tag in bug_one.tags:
+    ...     print(tag)
+    sco
+    svg
 
 Under the hood the tags are stored in a different table. If we take a
 look at it we can see that the added tags are there.
@@ -35,8 +37,11 @@ So if we add another tag by setting the 'tags' attribute to a new list.
 The tag will be added in the table.
 
     >>> bug_one.tags = [u'svg', u'sco', u'installl']
-    >>> bug_one.tags
-    [u'installl', u'sco', u'svg']
+    >>> for tag in bug_one.tags:
+    ...     print(tag)
+    installl
+    sco
+    svg
     >>> from lp.services.database.interfaces import IStore
     >>> bugtags = IStore(BugTag).find(
     ...     BugTag, bug_id=bug_one.id).order_by(BugTag.tag)
@@ -52,14 +57,19 @@ We allow adding the same tag twice, but it won't be stored twice in the
 db:
 
     >>> bug_one.tags = [u'svg', u'svg', u'sco', u'installl']
-    >>> bug_one.tags
-    [u'installl', u'sco', u'svg']
+    >>> for tag in bug_one.tags:
+    ...     print(tag)
+    installl
+    sco
+    svg
 
 Let's correct the spelling mistake we did and delete one of the tags:
 
     >>> bug_one.tags = [u'sco', u'install']
-    >>> bug_one.tags
-    [u'install', u'sco']
+    >>> for tag in bug_one.tags:
+    ...     print(tag)
+    install
+    sco
 
     >>> from lp.services.database.interfaces import IStore
     >>> bugtags = IStore(BugTag).find(
@@ -87,34 +97,40 @@ use BugTagsWidget.
 Since we didn't provided a value in the request, the form value will be
 empty:
 
-    >>> tags_widget._getFormValue()
-    u''
+    >>> print(tags_widget._getFormValue())
+    <BLANKLINE>
 
 If we set the value to bug one's tags, it will be a space separated
 string:
 
     >>> tags_widget.setRenderedValue(bug_one.tags)
-    >>> tags_widget._getFormValue()
-    u'install sco'
+    >>> print(tags_widget._getFormValue())
+    install sco
 
 If we pass in a value via the request, we'll be able to get the tags as
 a sorted list from getInputValue():
 
     >>> request = LaunchpadTestRequest(form={'field.tags': u'svg sco'})
     >>> tags_widget = BugTagsWidget(bug_tags_field, tag_field, request)
-    >>> tags_widget._getFormValue()
-    u'sco svg'
-    >>> tags_widget.getInputValue()
-    [u'sco', u'svg']
+    >>> print(tags_widget._getFormValue())
+    sco svg
+    >>> for tag in tags_widget.getInputValue():
+    ...     print(tag)
+    sco
+    svg
 
 When we have an input value, the widget can edit the bug tags.
 
-    >>> bug_one.tags
-    [u'install', u'sco']
+    >>> for tag in bug_one.tags:
+    ...     print(tag)
+    install
+    sco
     >>> tags_widget.applyChanges(bug_one)
     True
-    >>> bug_one.tags
-    [u'sco', u'svg']
+    >>> for tag in bug_one.tags:
+    ...     print(tag)
+    sco
+    svg
 
 If a user enters an invalid tag, we get an error explaining what's
 wrong.
@@ -136,37 +152,49 @@ wrong.
 Let's take a closer look at _toFormValue() to ensure that it works
 properly:
 
-    >>> tags_widget._toFormValue([])
-    u''
-    >>> tags_widget._toFormValue(['foo'])
-    u'foo'
-    >>> tags_widget._toFormValue(['foo', 'bar'])
-    u'foo bar'
+    >>> print(tags_widget._toFormValue([]))
+    <BLANKLINE>
+    >>> print(tags_widget._toFormValue(['foo']))
+    foo
+    >>> print(tags_widget._toFormValue(['foo', 'bar']))
+    foo bar
 
 And _toFieldValue():
 
     >>> tags_widget._toFieldValue(u'')
     []
-    >>> tags_widget._toFieldValue(u'foo')
-    [u'foo']
-    >>> tags_widget._toFieldValue(u'FOO bar')
-    [u'bar', u'foo']
-    >>> tags_widget._toFieldValue(u'foo   \t          bar')
-    [u'bar', u'foo']
+    >>> for tag in tags_widget._toFieldValue(u'foo'):
+    ...     print(tag)
+    foo
+    >>> for tag in tags_widget._toFieldValue(u'FOO bar'):
+    ...     print(tag)
+    bar
+    foo
+    >>> for tag in tags_widget._toFieldValue(u'foo   \t          bar'):
+    ...     print(tag)
+    bar
+    foo
 
 A comma isn't valid in a tag name and sometimes users use commas to
 separate the tags, so we accept that as well.
 
-    >>> tags_widget._toFieldValue(u'foo, bar')
-    [u'bar', u'foo']
+    >>> for tag in tags_widget._toFieldValue(u'foo, bar'):
+    ...     print(tag)
+    bar
+    foo
 
-    >>> tags_widget._toFieldValue(u'foo,bar')
-    [u'bar', u'foo']
+    >>> for tag in tags_widget._toFieldValue(u'foo,bar'):
+    ...     print(tag)
+    bar
+    foo
 
 Duplicate tags are converted to a single instance.
 
-    >>> tags_widget._toFieldValue(u'FOO, , , , bar bar, bar, bar foo')
-    [u'bar', u'foo']
+    >>> for tag in tags_widget._toFieldValue(
+    ...         u'FOO, , , , bar bar, bar, bar foo'):
+    ...     print(tag)
+    bar
+    foo
 
 
 Bug Tags Widget for Frozen Sets
@@ -245,8 +273,9 @@ We can search for bugs with some specific tag.
     >>> svg_tasks = ubuntu.searchTasks(
     ...     BugTaskSearchParams(tag=all(u'svg'), user=None))
     >>> for bugtask in svg_tasks:
-    ...     print(bugtask.bug.id, bugtask.bug.tags)
-    1 [u'sco', u'svg']
+    ...     print(bugtask.bug.id,
+    ...           ' '.join("'%s'" % tag for tag in bugtask.bug.tags))
+    1 'sco' 'svg'
 
 We can also search for bugs with any of the tags in a supplied list.
 
@@ -254,10 +283,11 @@ We can also search for bugs with any of the tags in a supplied list.
     >>> crash_dataloss_tasks = ubuntu.searchTasks(BugTaskSearchParams(
     ...     tag=any(u'crash', u'dataloss'), orderby='id', user=None))
     >>> for bugtask in crash_dataloss_tasks:
-    ...     print(bugtask.bug.id, bugtask.bug.tags)
-    2 [u'dataloss', u'pebcak']
-    9 [u'crash']
-    10 [u'crash']
+    ...     print(bugtask.bug.id,
+    ...           ' '.join("'%s'" % tag for tag in bugtask.bug.tags))
+    2 'dataloss' 'pebcak'
+    9 'crash'
+    10 'crash'
 
 And for bugs with all of the tags in a supplied list.
 
@@ -266,8 +296,9 @@ And for bugs with all of the tags in a supplied list.
     >>> crash_burn_tasks = ubuntu.searchTasks(BugTaskSearchParams(
     ...     tag=all(u'crash', u'burn'), orderby='id', user=None))
     >>> for bugtask in crash_burn_tasks:
-    ...     print(bugtask.bug.id, bugtask.bug.tags)
-    10 [u'burn', u'crash']
+    ...     print(bugtask.bug.id,
+    ...           ' '.join("'%s'" % tag for tag in bugtask.bug.tags))
+    10 'burn' 'crash'
     >>> getUtility(IBugSet).get(10).tags = [u'crash']
 
 Tags are also searched when searching for some text in general. For
@@ -291,8 +322,9 @@ If we now set bug one's tag to 'some-tag', it will be found.
     XXX some_tag_tasks = ubuntu.searchTasks(
     ...     BugTaskSearchParams(searchtext=u'some-tag', user=None))
     XXX for bugtask in some_tag_tasks:
-    ...     print(bugtask.id, bugtask.bug.id, bugtask.bug.tags)
-    1 [u'some-tag']
+    ...     print(bugtask.bug.id,
+    ...           ' '.join("'%s'" % tag for tag in bugtask.bug.tags))
+    1 'some-tag'
 
 
 Tags for a context
@@ -313,36 +345,57 @@ that are used in that context. We can also get all the used tags, together
 with the number of open bugs each tag has. Only tags having open bugs are
 returned.
 
-    >>> sorted(firefox.getUsedBugTagsWithOpenCounts(None).items())
-    [(u'doc', 1L), (u'layout-test', 1L), (u'sco', 1L), (u'svg', 1L)]
-
-    >>> sorted(mozilla.getUsedBugTagsWithOpenCounts(None).items())
-    [(u'doc', 1L), (u'layout-test', 1L), (u'sco', 1L), (u'svg', 1L)]
-
-    >>> sorted(ubuntu.getUsedBugTagsWithOpenCounts(None).items())
-    [(u'crash', 2L), (u'dataloss', 1L), (u'pebcak', 1L),
-     (u'sco', 1L), (u'svg', 1L)]
+    >>> def print_tag_counts(target, user, **kwargs):
+    ...     for tag, sum_count in sorted(target.getUsedBugTagsWithOpenCounts(
+    ...             user, **kwargs).items()):
+    ...         print(tag, sum_count)
+
+    >>> print_tag_counts(firefox, None)
+    doc 1
+    layout-test 1
+    sco 1
+    svg 1
+
+    >>> print_tag_counts(mozilla, None)
+    doc 1
+    layout-test 1
+    sco 1
+    svg 1
+
+    >>> print_tag_counts(ubuntu, None)
+    crash 2
+    dataloss 1
+    pebcak 1
+    sco 1
+    svg 1
 
 We can require that some tags be included in the output even when limiting the
 results.
 
-    >>> sorted(ubuntu.getUsedBugTagsWithOpenCounts(None,
-    ...     tag_limit=1, include_tags=[u'pebcak', u'svg', u'fake']).items())
-    [(u'crash', 2L), (u'fake', 0), (u'pebcak', 1L), (u'svg', 1L)]
+    >>> print_tag_counts(
+    ...     ubuntu, None, tag_limit=1,
+    ...     include_tags=[u'pebcak', u'svg', u'fake'])
+    crash 2
+    fake 0
+    pebcak 1
+    svg 1
 
 Source packages are a bit special, they return all the tags that are
 used in the whole distribution, while the bug count includes only bugs
 in the specific package.
 
-    >>> ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(None)
-    {u'crash': 1L}
+    >>> print_tag_counts(ubuntu_thunderbird, None)
+    crash 1
 
-    >>> sorted(debian_woody.getUsedBugTagsWithOpenCounts(None).items())
-    [(u'dataloss', 1L), (u'layout-test', 1L), (u'pebcak', 1L)]
+    >>> print_tag_counts(debian_woody, None)
+    dataloss 1
+    layout-test 1
+    pebcak 1
 
-    >>> sorted(
-    ...     debian_woody_firefox.getUsedBugTagsWithOpenCounts(None).items())
-    [(u'dataloss', 1L), (u'layout-test', 1L), (u'pebcak', 1L)]
+    >>> print_tag_counts(debian_woody_firefox, None)
+    dataloss 1
+    layout-test 1
+    pebcak 1
 
 Only bugs that the supplied user has access to will be counted:
 
@@ -351,11 +404,10 @@ Only bugs that the supplied user has access to will be counted:
     True
     >>> flush_database_updates()
 
-    >>> ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(None)
-    {}
+    >>> print_tag_counts(ubuntu_thunderbird, None)
 
     >>> sample_person = getUtility(ILaunchBag).user
     >>> bug_nine.isSubscribed(sample_person)
     True
-    >>> ubuntu_thunderbird.getUsedBugTagsWithOpenCounts(sample_person)
-    {u'crash': 1L}
+    >>> print_tag_counts(ubuntu_thunderbird, sample_person)
+    crash 1
diff --git a/lib/lp/bugs/doc/bug.txt b/lib/lp/bugs/doc/bug.txt
index b8252e6..56a21c0 100644
--- a/lib/lp/bugs/doc/bug.txt
+++ b/lib/lp/bugs/doc/bug.txt
@@ -58,9 +58,10 @@ result set below has only one element.
     >>> print(result_set.count())
     2
 
-    >>> print([(bug.id, bug.title[:40]) for bug in result_set])
-    [(1, u'Firefox does not support SVG'),
-     (6, u'Firefox crashes when Save As dialog for ')]
+    >>> for bug in result_set:
+    ...     print('%d: %s' % (bug.id, bug.title[:40]))
+    1: Firefox does not support SVG
+    6: Firefox crashes when Save As dialog for 
 
 If no bug numbers are specified an empty result set is returned.
 
@@ -244,8 +245,8 @@ So, remembering that we're still logged in as Sample Person (ID 12 in
 the Person table), and that Sample Person is a direct subscriber to the
 firefox_crashes bug, we can still access properties of this bug:
 
-    >>> firefox_crashes.title
-    u'Firefox crashes when Save As dialog for a nonexistent window is closed'
+    >>> print(firefox_crashes.title)
+    Firefox crashes when Save As dialog for a nonexistent window is closed
 
 Note that a search will return all public bugs, omitting bug 14 which is
 private:
@@ -274,11 +275,11 @@ Likewise Foo Bar, an admin, can access the bug.
 
     >>> old_title = firefox_crashes.title
     >>> firefox_crashes.title = "new title"
-    >>> firefox_crashes.title
-    u'new title'
+    >>> print(firefox_crashes.title)
+    new title
     >>> firefox_crashes.title = old_title
-    >>> firefox_crashes.title
-    u'Firefox crashes when Save As dialog for a nonexistent window is closed'
+    >>> print(firefox_crashes.title)
+    Firefox crashes when Save As dialog for a nonexistent window is closed
 
 Bug 14, which is private, is returned by the search results for an
 admin as well:
@@ -314,11 +315,11 @@ Jeff Waugh, a member of the Ubuntu Team, is able to access this bug:
 
     >>> old_title = reflow_problems_bug.title
     >>> reflow_problems_bug.title = "new title"
-    >>> reflow_problems_bug.title
-    u'new title'
+    >>> print(reflow_problems_bug.title)
+    new title
     >>> reflow_problems_bug.title = old_title
-    >>> reflow_problems_bug.title
-    u'Reflow problems with complex page layouts'
+    >>> print(reflow_problems_bug.title)
+    Reflow problems with complex page layouts
 
 Bug #4 is visible to him in searches. Note that bugs #6 and #14 are
 hidden from him.
@@ -410,8 +411,9 @@ When a private bug is filed:
 
 *only* the submitter is directly subscribed:
 
-    >>> [subscriber.name for subscriber in private_bug.getDirectSubscribers()]
-    [u'name16']
+    >>> for subscriber in private_bug.getDirectSubscribers():
+    ...     print(subscriber.name)
+    name16
 
 It's up to the submitter to subscribe the maintainer, if they so choose.
 
@@ -432,8 +434,9 @@ sourcepackage. E.g.
     ...     target=ubuntu.getSourcePackage(evolution))
     >>> added_bug = getUtility(IBugSet).createBug(params)
     >>> private_bug = bugset.get(added_bug.id)
-    >>> [subscriber.name for subscriber in private_bug.getDirectSubscribers()]
-    [u'name16']
+    >>> for subscriber in private_bug.getDirectSubscribers():
+    ...     print(subscriber.name)
+    name16
 
 
 Prevent reporter from being subscribed to filed bugs
@@ -864,30 +867,30 @@ use getBugTask() to get it.
 
     >>> tomcat = getUtility(IProductSet).getByName('tomcat')
     >>> tomcat_task = bug_two.getBugTask(tomcat)
-    >>> tomcat_task.target.name
-    u'tomcat'
+    >>> print(tomcat_task.target.name)
+    tomcat
 
     >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
     >>> ubuntu_task = bug_two.getBugTask(ubuntu)
-    >>> ubuntu_task.target.name
-    u'ubuntu'
+    >>> print(ubuntu_task.target.name)
+    ubuntu
 
     >>> ubuntu_hoary = ubuntu.getSeries('hoary')
     >>> ubuntu_hoary_task = bug_two.getBugTask(ubuntu_hoary)
-    >>> ubuntu_hoary_task.target.name
-    u'hoary'
+    >>> print(ubuntu_hoary_task.target.name)
+    hoary
 
     >>> debian = getUtility(IDistributionSet).getByName('debian')
     >>> mozilla_in_debian = debian.getSourcePackage('mozilla-firefox')
     >>> mozilla_in_debian_task = bug_two.getBugTask(mozilla_in_debian)
-    >>> mozilla_in_debian_task.target.displayname
-    u'mozilla-firefox in Debian'
+    >>> print(mozilla_in_debian_task.target.displayname)
+    mozilla-firefox in Debian
 
     >>> debian_woody = debian.getSeries('woody')
     >>> mozilla_in_woody = debian_woody.getSourcePackage('mozilla-firefox')
     >>> mozilla_in_woody_task = bug_two.getBugTask(mozilla_in_woody)
-    >>> mozilla_in_woody_task.target.displayname
-    u'mozilla-firefox in Debian Woody'
+    >>> print(mozilla_in_woody_task.target.displayname)
+    mozilla-firefox in Debian Woody
 
 If the bug isn't targeted to the target, None is returned.
 
@@ -982,12 +985,13 @@ show messages in the bugtask index template in one shot.
     >>> queries = len(CursorWrapper.last_executed_sql)
 
     >>> chunks = bug_two.getMessagesForView(None)
-    >>> for _, _1, chunk in sorted(chunks, key=lambda x:x[2].id):
-    ...    (chunk.id, chunk.message.id, chunk.message.owner.id,
-    ...     chunk.content[:30])
-    (4, 1, 16, u'Problem exists between chair a')
-    (7, 5, 12, u'This would be a real killer fe')
-    (8, 6, 12, u'Oddly enough the bug system se')
+    >>> for _, _, chunk in sorted(chunks, key=lambda x:x[2].id):
+    ...    print('%d %d %d: %s' % (
+    ...        chunk.id, chunk.message.id, chunk.message.owner.id,
+    ...        chunk.content[:30]))
+    4 1 16: Problem exists between chair a
+    7 5 12: This would be a real killer fe
+    8 6 12: Oddly enough the bug system se
 
 It's done in a way that we only issue two queries to fetch all this
 information, too:
diff --git a/lib/lp/bugs/doc/bugactivity.txt b/lib/lp/bugs/doc/bugactivity.txt
index 79d54d6..6af78f4 100644
--- a/lib/lp/bugs/doc/bugactivity.txt
+++ b/lib/lp/bugs/doc/bugactivity.txt
@@ -41,10 +41,10 @@ User files a bug
     >>> latest_activity = bug.activity[-1]
     >>> latest_activity.person == user
     True
-    >>> latest_activity.whatchanged
-    u'bug'
-    >>> latest_activity.message
-    u'added bug'
+    >>> print(latest_activity.whatchanged)
+    bug
+    >>> print(latest_activity.message)
+    added bug
 
 
 Bug title edited
@@ -53,12 +53,12 @@ Bug title edited
     >>> with notify_modified(bug, ["title", "description"]):
     ...     bug.title = "new bug title"
     >>> latest_activity = bug.activity[-1]
-    >>> latest_activity.whatchanged
-    u'summary'
-    >>> latest_activity.oldvalue
-    u'a test bug'
-    >>> latest_activity.newvalue
-    u'new bug title'
+    >>> print(latest_activity.whatchanged)
+    summary
+    >>> print(latest_activity.oldvalue)
+    a test bug
+    >>> print(latest_activity.newvalue)
+    new bug title
 
 
 Source package assignment edited
@@ -79,8 +79,8 @@ Source package assignment edited
     ...     source_package_assignment.transitionToStatus(
     ...         BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
     >>> latest_activity = bug.activity[-1]
-    >>> latest_activity.whatchanged
-    u'mozilla-firefox (Ubuntu): status'
+    >>> print(latest_activity.whatchanged)
+    mozilla-firefox (Ubuntu): status
     >>> latest_activity.oldvalue == BugTaskStatus.NEW.title
     True
     >>> latest_activity.newvalue == BugTaskStatus.CONFIRMED.title
@@ -91,10 +91,10 @@ a project and an attribute.  This happens when the change is to a bugtask.
 The activity object object provides a couple of simple attributes to separate
 out these values: `target` and `attribute`.
 
-    >>> latest_activity.target
-    u'mozilla-firefox (Ubuntu)'
-    >>> latest_activity.attribute
-    u'status'
+    >>> print(latest_activity.target)
+    mozilla-firefox (Ubuntu)
+    >>> print(latest_activity.attribute)
+    status
 
 If the activity is not for a bug task, `target` is None, and `attribute` is
 typically the same as `whatchanged`.  However, in some cases (ideally,
@@ -103,8 +103,8 @@ For instance, look at the attributes on the previous activity.
 
     >>> print(bug.activity[-2].target)
     None
-    >>> bug.activity[-2].whatchanged
-    u'summary'
+    >>> print(bug.activity[-2].whatchanged)
+    summary
     >>> bug.activity[-2].attribute
     'title'
 
@@ -122,12 +122,12 @@ Upstream product assignment edited
     ...     product_assignment.transitionToStatus(
     ...         BugTaskStatus.INVALID, getUtility(ILaunchBag).user)
     >>> latest_activity = bug.activity[-1]
-    >>> latest_activity.whatchanged
-    u'thunderbird: status'
-    >>> latest_activity.target
-    u'thunderbird'
-    >>> latest_activity.attribute
-    u'status'
+    >>> print(latest_activity.whatchanged)
+    thunderbird: status
+    >>> print(latest_activity.target)
+    thunderbird
+    >>> print(latest_activity.attribute)
+    status
     >>> latest_activity.oldvalue == BugTaskStatus.NEW.title
     True
     >>> latest_activity.newvalue == BugTaskStatus.INVALID.title
@@ -144,8 +144,8 @@ Bug report is marked as a duplicate of another bug report
     ...     latest_bug = factory.makeBug()
     ...     bug.markAsDuplicate(latest_bug)
     >>> latest_activity = bug.activity[-1]
-    >>> latest_activity.whatchanged
-    u'marked as duplicate'
+    >>> print(latest_activity.whatchanged)
+    marked as duplicate
     >>> latest_activity.oldvalue is None
     True
     >>> latest_activity.newvalue == six.text_type(latest_bug.id)
@@ -162,8 +162,8 @@ Bug report has its duplicate marker changed to another bug report
     ...     another_bug = factory.makeBug()
     ...     bug.markAsDuplicate(another_bug)
     >>> latest_activity = bug.activity[-1]
-    >>> latest_activity.whatchanged
-    u'changed duplicate marker'
+    >>> print(latest_activity.whatchanged)
+    changed duplicate marker
     >>> latest_activity.oldvalue == six.text_type(latest_bug.id)
     True
     >>> latest_activity.newvalue == six.text_type(another_bug.id)
@@ -179,8 +179,8 @@ The bug report is un-duplicated
     >>> with notify_modified(bug, edit_fields):
     ...     bug.markAsDuplicate(None)
     >>> latest_activity = bug.activity[-1]
-    >>> latest_activity.whatchanged
-    u'removed duplicate marker'
+    >>> print(latest_activity.whatchanged)
+    removed duplicate marker
     >>> latest_activity.oldvalue == six.text_type(another_bug.id)
     True
     >>> latest_activity.newvalue is None
diff --git a/lib/lp/bugs/doc/bugattachments.txt b/lib/lp/bugs/doc/bugattachments.txt
index 046d753..af62209 100644
--- a/lib/lp/bugs/doc/bugattachments.txt
+++ b/lib/lp/bugs/doc/bugattachments.txt
@@ -31,7 +31,7 @@ ObjectCreatedEvent in order to trigger email notifications:
     >>> from lazr.lifecycle.event import IObjectCreatedEvent
     >>> from lp.testing.fixture import ZopeEventHandlerFixture
     >>> def attachment_added(attachment, event):
-    ...     print("Attachment added: %r" % attachment.libraryfile.filename)
+    ...     print("Attachment added: '%s'" % attachment.libraryfile.filename)
     >>> event_listener = ZopeEventHandlerFixture(
     ...     attachment_added, (IBugAttachment, IObjectCreatedEvent))
     >>> event_listener.setUp()
@@ -53,7 +53,7 @@ ObjectCreatedEvent in order to trigger email notifications:
     ...     description="this fixes the bug",
     ...     comment=message,
     ...     is_patch=False)
-    Attachment added: u'foo.bar'
+    Attachment added: 'foo.bar'
     <BugAttachment ...>
 
     >>> import transaction
@@ -76,10 +76,10 @@ passed in is often a file-like object, but can be bytes too.
     ...     description="this fixes the bug",
     ...     comment="a string comment",
     ...     is_patch=False)
-    Attachment added: u'foo.baz'
+    Attachment added: 'foo.baz'
 
-    >>> attachment_from_strings.message.text_contents
-    u'a string comment'
+    >>> print(attachment_from_strings.message.text_contents)
+    a string comment
 
 If no description is given, the title is set to the filename.
 
@@ -90,14 +90,14 @@ If no description is given, the title is set to the filename.
     ...     filename="screenshot.jpg",
     ...     comment="a string comment",
     ...     is_patch=False)
-    Attachment added: u'screenshot.jpg'
-    >>> screenshot.title
-    u'screenshot.jpg'
+    Attachment added: 'screenshot.jpg'
+    >>> print(screenshot.title)
+    screenshot.jpg
 
 The content type is guessed based on the information provided.
 
-    >>> screenshot.libraryfile.mimetype
-    u'image/jpeg'
+    >>> print(screenshot.libraryfile.mimetype)
+    image/jpeg
 
     >>> data = BytesIO(b'</something-htmlish>')
     >>> debdiff = bug_four.addAttachment(
@@ -106,13 +106,13 @@ The content type is guessed based on the information provided.
     ...     filename="something.debdiff",
     ...     comment="something debdiffish",
     ...     is_patch=False)
-    Attachment added: u'something.debdiff'
-    >>> debdiff.title
-    u'something.debdiff'
-    >>> debdiff.libraryfile.filename
-    u'something.debdiff'
-    >>> debdiff.libraryfile.mimetype
-    u'text/plain'
+    Attachment added: 'something.debdiff'
+    >>> print(debdiff.title)
+    something.debdiff
+    >>> print(debdiff.libraryfile.filename)
+    something.debdiff
+    >>> print(debdiff.libraryfile.mimetype)
+    text/plain
 
 The librarian won't allow empty files, so the view that creates the
 attachment needs to handle that:
@@ -144,8 +144,8 @@ and work with that:
     1
     >>> add_comment_view.error_count
     'There is 1 error.'
-    >>> add_comment_view.getFieldError('filecontent')
-    u'Cannot upload empty file.'
+    >>> print(add_comment_view.getFieldError('filecontent'))
+    Cannot upload empty file.
 
 It's possible to limit the maximum size of the attachments by setting
 max_attachment_size in launchpad.conf. The default value for the
@@ -168,8 +168,9 @@ upload it:
     >>> add_comment_view.initialize()
     >>> len(add_comment_view.errors)
     1
-    >>> [error.doc() for error in add_comment_view.errors]
-    [u'Cannot upload files larger than 1024 bytes']
+    >>> for error in add_comment_view.errors:
+    ...     print(error.doc())
+    Cannot upload files larger than 1024 bytes
 
 If we set the limit to 0 we can upload it, though, since a value of 0
 means no limit:
@@ -192,7 +193,7 @@ means no limit:
     >>> add_comment_view = getMultiAdapter(
     ...     (bugtask, add_request), name='+addcomment-form')
     >>> add_comment_view.initialize()
-    Attachment added: u'foo.txt'
+    Attachment added: 'foo.txt'
     >>> len(add_comment_view.errors)
     0
 
@@ -209,8 +210,9 @@ must have at least one.
     >>> add_comment_view.initialize()
     >>> len(add_comment_view.errors)
     1
-    >>> [error for error in add_comment_view.errors]
-    [u'Either a comment or attachment must be provided.']
+    >>> for error in add_comment_view.errors:
+    ...     print(error)
+    Either a comment or attachment must be provided.
 
 If the request contains no attachment description the filename should be used.
 
@@ -227,11 +229,11 @@ If the request contains no attachment description the filename should be used.
     >>> add_comment_view = getMultiAdapter(
     ...     (bugtask, add_request), name='+addcomment-form')
     >>> add_comment_view.initialize()
-    Attachment added: u'RA.txt'
+    Attachment added: 'RA.txt'
     >>> len(add_comment_view.errors)
     0
-    >>> bug_four.attachments[bug_four.attachments.count()-1].title
-    u'RA.txt'
+    >>> print(bug_four.attachments[bug_four.attachments.count()-1].title)
+    RA.txt
 
 Since the ObjectCreatedEvent was generated, a notification about the
 attachment was added.
@@ -261,12 +263,12 @@ Let's try uploading a file with some weird characters in them:
     >>> len(add_comment_view.errors)
     0
     >>> add_comment_view.initialize()
-    Attachment added: u'fo\xf6 bar'
+    Attachment added: 'foö bar'
     >>> len(add_comment_view.errors)
     0
     >>> attachments = bug_four.attachments
-    >>> attachments[bug_four.attachments.count()-1].libraryfile.filename
-    u'fo\xf6 bar'
+    >>> print(attachments[bug_four.attachments.count()-1].libraryfile.filename)
+    foö bar
     >>> attachments[bug_four.attachments.count()-1].libraryfile.http_url
     'http://.../fo%C3%B6%20bar'
 
@@ -287,11 +289,12 @@ from the librarian.
     >>> add_comment_view = getMultiAdapter(
     ...     (bugtask, add_request), name='+addcomment-form')
     >>> add_comment_view.initialize()
-    Attachment added: u'foo-bar-baz'
+    Attachment added: 'foo-bar-baz'
     >>> len(add_comment_view.errors)
     0
-    >>> attachments[bug_four.attachments.count()-1].libraryfile.filename
-    u'foo-bar-baz'
+    >>> print(
+    ...     attachments[bug_four.attachments.count()-1].libraryfile.filename)
+    foo-bar-baz
     >>> attachments[bug_four.attachments.count()-1].libraryfile.http_url
     'http://.../foo-bar-baz'
 
@@ -307,8 +310,8 @@ also view/edit the attachment. At the moment the bug_four is public, so
 anonymous can read the attachment's attributes, but they can't set them:
 
     >>> login(ANONYMOUS)
-    >>> attachment.title
-    u'this fixes the bug'
+    >>> print(attachment.title)
+    this fixes the bug
     >>> attachment.title = 'Better Title'
     ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
@@ -321,8 +324,8 @@ anonymous can read the attachment's attributes, but they can't set them:
 Attachment owner can access and set the attributes, though:
 
     >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> attachment.title
-    u'this fixes the bug'
+    >>> print(attachment.title)
+    this fixes the bug
     >>> attachment.title = 'Even Better Title'
 
 Now let's make the bug private instead:
@@ -335,8 +338,8 @@ Foo Bar isn't explicitly subscribed to the bug, BUT they are an admin, so
 they can access the attachment's attributes:
 
     >>> login('test@xxxxxxxxxxxxx')
-    >>> attachment.title
-    u'Even Better Title'
+    >>> print(attachment.title)
+    Even Better Title
 
 Mr. No Privs, who is not subscribed to bug_four, cannot access or set the
 attachments attributes:
@@ -370,8 +373,8 @@ Of course, anonymous is also not allowed to access or set them:
 Sample Person is explicitly subscribed, so they can access the attributes:
 
     >>> login('test@xxxxxxxxxxxxx')
-    >>> attachment.title
-    u'Even Better Title'
+    >>> print(attachment.title)
+    Even Better Title
 
 
 Let's make the bug public again:
@@ -595,16 +598,19 @@ deleted, for example. because an admin deleted a privacy sensitive file.
 These attachments are not included in Bug.attachments. Our test bug has
 at present two attachments.
 
-    >>> [attachment.title for attachment in bug.attachments]
-    [u'foobar', u'An attachment of some sort']
+    >>> for attachment in bug.attachments:
+    ...     print(attachment.title)
+    foobar
+    An attachment of some sort
 
 If we remove the content record from one attachment, it is no longer
 returned by Bug.attachments.
 
     >>> from zope.security.proxy import removeSecurityProxy
     >>> removeSecurityProxy(attachment.libraryfile).content = None
-    >>> [attachment.title for attachment in bug.attachments]
-    [u'foobar']
+    >>> for attachment in bug.attachments:
+    ...     print(attachment.title)
+    foobar
 
 
 Adding bug attachments to private bugs
@@ -651,8 +657,8 @@ Miscellaneous
 
 The method IBugAttachment.getFileByName() returns the Librarian file.
 
-    >>> attachment.libraryfile.filename
-    u'foobar'
+    >>> print(attachment.libraryfile.filename)
+    foobar
     >>> attachment.getFileByName('foobar')
     <LibraryFileAlias at...
 
diff --git a/lib/lp/bugs/doc/bugcomment.txt b/lib/lp/bugs/doc/bugcomment.txt
index a6e9c1c..c96b75f 100644
--- a/lib/lp/bugs/doc/bugcomment.txt
+++ b/lib/lp/bugs/doc/bugcomment.txt
@@ -143,10 +143,12 @@ get_comments_for_bugtask:
 
     >>> [bug_comment.index for bug_comment in all_comments]
     [0, 1]
-    >>> all_comments[0].text_for_display
-    u'test bug'
-    >>> all_comments[1].text_for_display
-    u'Welcome to Canada!\n\nUnicode\u2122 text'
+    >>> print(all_comments[0].text_for_display)
+    test bug
+    >>> print(all_comments[1].text_for_display)
+    Welcome to Canada!
+    <BLANKLINE>
+    Unicode™ text
 
 Note that multi-chunk comments are only created by the email interface
 itself; adding comments through the web UI always places them in the
@@ -227,13 +229,13 @@ hidden.
     >>> bug_view = getMultiAdapter(
     ...     (bug_three_bugtask, LaunchpadTestRequest()), name='+index')
     >>> bug_view.initialize()
-    >>> [(c.index, c.title, c.text_for_display)
-    ...  for c in bug_comments(bug_view)]
-    [(1, u'Hi', u'Hello there'),
-     (3, u'Ho', u'Hello there'),
-     (5, u'Ho', u'Hello there'),
-     (6, u'Ho', u'Hello there'),
-     (7, u'Ho', u'Hello there')]
+    >>> for c in bug_comments(bug_view):
+    ...     print("%d: '%s', '%s'" % (c.index, c.title, c.text_for_display))
+    1: 'Hi', 'Hello there'
+    3: 'Ho', 'Hello there'
+    5: 'Ho', 'Hello there'
+    6: 'Ho', 'Hello there'
+    7: 'Ho', 'Hello there'
 
 
 Bugs with lots of comments
diff --git a/lib/lp/bugs/doc/bugmail-headers.txt b/lib/lp/bugs/doc/bugmail-headers.txt
index e31d00e..59955d6 100644
--- a/lib/lp/bugs/doc/bugmail-headers.txt
+++ b/lib/lp/bugs/doc/bugmail-headers.txt
@@ -25,28 +25,28 @@ You can ask an IBugTask for its representation as an email header value:
     >>> ubuntu_firefox_bugtask = bugtaskset.get(17)
     >>> warty_firefox_bugtask = bugtaskset.get(16)
 
-    >>> upstream_firefox_bugtask.asEmailHeaderValue()
-    u'product=firefox; status=New; importance=Low; assignee=mark@xxxxxxxxxxx;'
+    >>> print(upstream_firefox_bugtask.asEmailHeaderValue())
+    product=firefox; status=New; importance=Low; assignee=mark@xxxxxxxxxxx;
 
-    >>> upstream_firefox_bugtask_no_assignee.asEmailHeaderValue()
-    u'product=firefox; status=New; importance=Medium; assignee=None;'
+    >>> print(upstream_firefox_bugtask_no_assignee.asEmailHeaderValue())
+    product=firefox; status=New; importance=Medium; assignee=None;
 
-    >>> upstream_firefox_bugtask_1_0.asEmailHeaderValue()
-    u'product=firefox; productseries=1.0; status=New;
-      importance=Undecided; assignee=None;'
+    >>> print(upstream_firefox_bugtask_1_0.asEmailHeaderValue())
+    product=firefox; productseries=1.0; status=New;
+    importance=Undecided; assignee=None;
 
-    >>> debian_firefox_bugtask.asEmailHeaderValue()
-    u'distribution=debian; sourcepackage=mozilla-firefox;
-      component=None; status=Confirmed; importance=Low; assignee=None;'
+    >>> print(debian_firefox_bugtask.asEmailHeaderValue())
+    distribution=debian; sourcepackage=mozilla-firefox;
+    component=None; status=Confirmed; importance=Low; assignee=None;
 
-    >>> ubuntu_firefox_bugtask.asEmailHeaderValue()
-    u'distribution=ubuntu; sourcepackage=mozilla-firefox;
-      component=main; status=New; importance=Medium; assignee=None;'
+    >>> print(ubuntu_firefox_bugtask.asEmailHeaderValue())
+    distribution=ubuntu; sourcepackage=mozilla-firefox;
+    component=main; status=New; importance=Medium; assignee=None;
 
-    >>> warty_firefox_bugtask.asEmailHeaderValue()
-    u'distribution=ubuntu; distroseries=warty;
-      sourcepackage=mozilla-firefox; component=main; status=New;
-      importance=Medium; assignee=None;'
+    >>> print(warty_firefox_bugtask.asEmailHeaderValue())
+    distribution=ubuntu; distroseries=warty;
+    sourcepackage=mozilla-firefox; component=main; status=New;
+    importance=Medium; assignee=None;
 
 Here's what the header values look like for distro tasks that have no
 sourcepackage (we have to login to be allowed to set attributes on these tasks):
@@ -63,13 +63,13 @@ sourcepackage (we have to login to be allowed to set attributes on these tasks):
     >>> warty_firefox_bugtask.transitionToTarget(
     ...     warty, getUtility(ILaunchBag).user)
 
-    >>> debian_firefox_bugtask.asEmailHeaderValue()
-    u'distribution=debian; sourcepackage=None; component=None;
-      status=Confirmed; importance=Low; assignee=None;'
+    >>> print(debian_firefox_bugtask.asEmailHeaderValue())
+    distribution=debian; sourcepackage=None; component=None;
+    status=Confirmed; importance=Low; assignee=None;
 
-    >>> warty_firefox_bugtask.asEmailHeaderValue()
-    u'distribution=ubuntu; distroseries=warty; sourcepackage=None;
-      component=None; status=New; importance=Medium; assignee=None;'
+    >>> print(warty_firefox_bugtask.asEmailHeaderValue())
+    distribution=ubuntu; distroseries=warty; sourcepackage=None;
+    component=None; status=New; importance=Medium; assignee=None;
 
 It is possible to have an assignee on a task who doesn't have a preferred email
 address. The assignee might be a team with no email address or a person who has
@@ -82,6 +82,6 @@ doesn't have a "preferred email" set in Launchpad.
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> scott = getUtility(IPersonSet).getByName('keybuk')
     >>> debian_firefox_bugtask.transitionToAssignee(scott)
-    >>> debian_firefox_bugtask.asEmailHeaderValue()
-    u'distribution=debian; sourcepackage=None; component=None;
-      status=Confirmed; importance=Low; assignee=keybuk;'
+    >>> print(debian_firefox_bugtask.asEmailHeaderValue())
+    distribution=debian; sourcepackage=None; component=None;
+    status=Confirmed; importance=Low; assignee=keybuk;
diff --git a/lib/lp/bugs/doc/bugmessage.txt b/lib/lp/bugs/doc/bugmessage.txt
index bcd38a7..9e14348 100644
--- a/lib/lp/bugs/doc/bugmessage.txt
+++ b/lib/lp/bugs/doc/bugmessage.txt
@@ -18,8 +18,8 @@ An individual IBugMessage can be retrieved with
 IBugMessageSet.get:
 
     >>> bugmessage_four = bugmessageset.get(4)
-    >>> bugmessage_four.message.subject
-    u"Fantastic idea, I'd really like to see this"
+    >>> print(bugmessage_four.message.subject)
+    Fantastic idea, I'd really like to see this
 
 You can get all the imported comments for a bug using
 getImportedBugMessages. Imported comments are comments being linked to a
@@ -60,8 +60,8 @@ To create a bug message, use IBugMessageSet.createMessage:
     ...     content="text message content",
     ...     owner=sample_person,
     ...     bug=bug_one)
-    >>> test_message.message.subject
-    u'test message subject'
+    >>> print(test_message.message.subject)
+    test message subject
 
 The parent gets set to the initial message of the bug:
 
diff --git a/lib/lp/bugs/doc/bugnotification-email.txt b/lib/lp/bugs/doc/bugnotification-email.txt
index 38c599b..cfe795f 100644
--- a/lib/lp/bugs/doc/bugnotification-email.txt
+++ b/lib/lp/bugs/doc/bugnotification-email.txt
@@ -55,8 +55,8 @@ So, let's pretend that we filed bug 4 just now:
 Let's take a look at what the notification email looks like:
 
     >>> subject, body = generate_bug_add_email(bug_four)
-    >>> subject
-    u'[Bug 4] [NEW] Reflow problems with complex page layouts'
+    >>> print(subject)
+    [Bug 4] [NEW] Reflow problems with complex page layouts
 
     >>> print(body)
     Public bug reported:
@@ -77,8 +77,8 @@ as well.
     >>> bug_four.tags = [u'foo', u'bar']
 
     >>> subject, body = generate_bug_add_email(bug_four)
-    >>> subject
-    u'[Bug 4] [NEW] Reflow problems with complex page layouts'
+    >>> print(subject)
+    [Bug 4] [NEW] Reflow problems with complex page layouts
 
     >>> print(body)
     Public bug reported:
@@ -98,8 +98,8 @@ New security related bugs are sent with a prominent warning:
     ...     InformationType.PUBLICSECURITY, getUtility(ILaunchBag).user)
 
     >>> subject, body = generate_bug_add_email(bug_four)
-    >>> subject
-    u'[Bug 4] [NEW] Reflow problems with complex page layouts'
+    >>> print(subject)
+    [Bug 4] [NEW] Reflow problems with complex page layouts
 
     >>> print(body)
     *** This bug is a security vulnerability ***
@@ -451,8 +451,8 @@ pair of handy functions defined in mailnotification.py:
 
 The Reply-To address generation is straightforward:
 
-    >>> get_bugmail_replyto_address(bug_four)
-    u'Bug 4 <4@xxxxxxxxxxxxxxxxxx>'
+    >>> print(get_bugmail_replyto_address(bug_four))
+    Bug 4 <4@xxxxxxxxxxxxxxxxxx>
 
 In order to send DMARC-compliant bug notifications, the From address generator
 is also quite straightforward and uses the bug's email address for the From
@@ -461,12 +461,12 @@ address, while adjusting the friendly display name field.
 This applies for all users.  For example, Stuart has four email addresses:
 
     >>> stub = getUtility(IPersonSet).getByName("stub")
-    >>> [(email.email, email.status.name) for email
-    ...     in getUtility(IEmailAddressSet).getByPerson(stub)]
-    [(u'stuart.bishop@xxxxxxxxxxxxx', 'PREFERRED'),
-     (u'stuart@xxxxxxxxxxxxxxxx', 'VALIDATED'),
-     (u'stub@xxxxxxxxxxx', 'NEW'),
-     (u'zen@xxxxxxxxxxxxxxxxxxxxxxxxx', 'OLD')]
+    >>> for email in getUtility(IEmailAddressSet).getByPerson(stub):
+    ...     print(email.email, email.status.name)
+    stuart.bishop@xxxxxxxxxxxxx PREFERRED
+    stuart@xxxxxxxxxxxxxxxx VALIDATED
+    stub@xxxxxxxxxxx NEW
+    zen@xxxxxxxxxxxxxxxxxxxxxxxxx OLD
 
 However, because of DMARC compliance, we only use the bug's email address in the
 From field, with Stuart's name in the 'display name' portion of the email address:
diff --git a/lib/lp/bugs/doc/bugnotification-sending.txt b/lib/lp/bugs/doc/bugnotification-sending.txt
index 92c265f..e0ed9aa 100644
--- a/lib/lp/bugs/doc/bugnotification-sending.txt
+++ b/lib/lp/bugs/doc/bugnotification-sending.txt
@@ -662,10 +662,11 @@ X-Launchpad-Bug headers were added:
     >>> email_notifications = get_email_notifications(notifications)
     >>> for bug_notifications, omitted, messages in email_notifications:
     ...     for message in messages:
-    ...         sorted(message.get_all('X-Launchpad-Bug'))
-    [u'distribution=debian; distroseries=sarge;... milestone=3.1;...',
-     u'distribution=debian; distroseries=woody;...',
-     u'distribution=debian; sourcepackage=mozilla-firefox; component=...']
+    ...         for line in sorted(message.get_all('X-Launchpad-Bug')):
+    ...             print(line)
+    distribution=debian; distroseries=sarge;... milestone=3.1;...
+    distribution=debian; distroseries=woody;...
+    distribution=debian; sourcepackage=mozilla-firefox; component=...
 
 The milestone field in X-Launchpad-Bug won't be filled where no milestone is
 specified:
@@ -707,12 +708,14 @@ email messages, and a third that combines the first two.
 If a bug is tagged, those tags will be included in the message in the
 X-Launchpad-Bug-Tags header.
 
-    >>> bug_three.tags
-    [u'layout-test']
+    >>> for tag in bug_three.tags:
+    ...     print(tag)
+    layout-test
 
     >>> for message in trigger_and_get_email_messages(bug_three):
-    ...     message.get_all('X-Launchpad-Bug-Tags')
-    [u'layout-test']
+    ...     for line in message.get_all('X-Launchpad-Bug-Tags'):
+    ...         print(line)
+    layout-test
 
 If we add a tag to bug three that will also be included in the header.
 The tags will be space-separated to allow the list to be wrapped if it
@@ -723,8 +726,9 @@ gets over-long.
 
     >>> bug_three = getUtility(IBugSet).get(3)
     >>> for message in trigger_and_get_email_messages(bug_three):
-    ...     message.get_all('X-Launchpad-Bug-Tags')
-    [u'another-tag layout-test yet-another']
+    ...     for line in message.get_all('X-Launchpad-Bug-Tags'):
+    ...         print(line)
+    another-tag layout-test yet-another
 
 If we remove the tags from the bug, the X-Launchpad-Bug-Tags header
 won't be included.
diff --git a/lib/lp/bugs/doc/bugsubscription.txt b/lib/lp/bugs/doc/bugsubscription.txt
index 21051d8..bf6b69b 100644
--- a/lib/lp/bugs/doc/bugsubscription.txt
+++ b/lib/lp/bugs/doc/bugsubscription.txt
@@ -589,9 +589,9 @@ Let's have a look at an example for a distribution bug:
 
 Only the bug reporter, Foo Bar, has an explicit subscription.
 
-    >>> [subscription.person.displayname
-    ...  for subscription in new_bug.subscriptions]
-    [u'Foo Bar']
+    >>> for subscription in new_bug.subscriptions:
+    ...     print(subscription.person.displayname)
+    Foo Bar
 
 But because Sample Person is the distribution contact for Ubuntu, they
 will be implicitly added to the notification recipients.
@@ -629,9 +629,9 @@ Another example, this time for an upstream:
 
 Again, only Foo Bar is explicitly subscribed:
 
-    >>> [subscription.person.displayname
-    ...  for subscription in new_bug.subscriptions]
-    [u'Foo Bar']
+    >>> for subscription in new_bug.subscriptions:
+    ...     print(subscription.person.displayname)
+    Foo Bar
 
 But the upstream Firefox bug supervisor, mark, is implicitly added to the
 recipients list.
@@ -650,9 +650,9 @@ supervisor will be subscribed:
 
 But still, only Foo Bar is explicitly subscribed.
 
-    >>> [subscription.person.displayname
-    ...  for subscription in new_bug.subscriptions]
-    [u'Foo Bar']
+    >>> for subscription in new_bug.subscriptions:
+    ...     print(subscription.person.displayname)
+    Foo Bar
 
 When an upstream does *not* have a specific bug supervisor set, the
 product.owner is used instead. So, if Firefox's bug supervisor is unset,
@@ -667,9 +667,9 @@ Sample Person, the Firefox "owner" will get subscribed instead:
 
 Foo Bar is the only explicit subscriber:
 
-    >>> [subscription.person.displayname
-    ...  for subscription in new_bug.subscriptions]
-    [u'Foo Bar']
+    >>> for subscription in new_bug.subscriptions:
+    ...     print(subscription.person.displayname)
+    Foo Bar
 
 But the product owner, Sample Person, is implicitly added to the
 recipient list:
@@ -712,9 +712,9 @@ So, if the Ubuntu team is added as a bug supervisor to evolution:
 The team will be implicitly subscribed to the previous bug we
 created:
 
-    >>> [subscription.person.displayname
-    ...  for subscription in new_bug.subscriptions]
-    [u'Foo Bar']
+    >>> for subscription in new_bug.subscriptions:
+    ...     print(subscription.person.displayname)
+    Foo Bar
 
     >>> new_bug.clearBugNotificationRecipientsCache()
     >>> getSubscribers(new_bug)
@@ -728,9 +728,9 @@ And the Ubuntu team will be implicitly subscribed to future bugs:
     ...     owner=foobar)
     >>> new_bug = evolution.createBug(params)
 
-    >>> [subscription.person.displayname
-    ...  for subscription in new_bug.subscriptions]
-    [u'Foo Bar']
+    >>> for subscription in new_bug.subscriptions:
+    ...     print(subscription.person.displayname)
+    Foo Bar
 
     >>> getSubscribers(new_bug)
     ['foo.bar@xxxxxxxxxxxxx', 'support@xxxxxxxxxx']
diff --git a/lib/lp/bugs/doc/bugtask-assignee-widget.txt b/lib/lp/bugs/doc/bugtask-assignee-widget.txt
index a57a7c8..5bc18d6 100644
--- a/lib/lp/bugs/doc/bugtask-assignee-widget.txt
+++ b/lib/lp/bugs/doc/bugtask-assignee-widget.txt
@@ -65,8 +65,8 @@ But it is assigned to another user:
     >>> widget.selectedRadioButton() == widget.assign_to_me
     True
 
-    >>> widget.getAssigneeDisplayValue()
-    u'Mark Shuttleworth (mark)'
+    >>> print(widget.getAssigneeDisplayValue())
+    Mark Shuttleworth (mark)
 
 The widget has input:
 
@@ -87,12 +87,12 @@ The input value is the current user:
 
 Let's apply the changes:
 
-    >>> bugtask.assignee.displayname
-    u'Mark Shuttleworth'
+    >>> print(bugtask.assignee.displayname)
+    Mark Shuttleworth
     >>> widget.applyChanges(bugtask)
     True
-    >>> bugtask.assignee.displayname
-    u'Foo Bar'
+    >>> print(bugtask.assignee.displayname)
+    Foo Bar
 
 Finally, let's rebind the widget's context to the updated context, as
 its current context will be out of sync with the new value:
@@ -164,8 +164,8 @@ Which means it's not assigned to another user:
     >>> widget.assignedToAnotherUser()
     False
 
-    >>> widget.getAssigneeDisplayValue()
-    u'Foo Bar (name16)'
+    >>> print(widget.getAssigneeDisplayValue())
+    Foo Bar (name16)
 
 The widget has input:
 
diff --git a/lib/lp/bugs/doc/bugtask-bugwatch-widget.txt b/lib/lp/bugs/doc/bugtask-bugwatch-widget.txt
index ea8a8cd..2fd1524 100644
--- a/lib/lp/bugs/doc/bugtask-bugwatch-widget.txt
+++ b/lib/lp/bugs/doc/bugtask-bugwatch-widget.txt
@@ -13,8 +13,8 @@ the Debian task in bug 1
     >>> bug_one = getUtility(IBugSet).get(1)
     >>> bugwatch_field = IBugTask['bugwatch']
     >>> debian_task = bug_one.bugtasks[2]
-    >>> debian_task.bugtargetname
-    u'mozilla-firefox (Debian)'
+    >>> print(debian_task.bugtargetname)
+    mozilla-firefox (Debian)
 
     >>> bugwatch_field = bugwatch_field.bind(debian_task)
 
@@ -60,8 +60,8 @@ If we pass a bug watch to renderItems(), the corresponding radio button
 will be selected.
 
     >>> mozilla_bugwatch = bug_one.watches[0]
-    >>> mozilla_bugwatch.title
-    u'The Mozilla.org Bug Tracker #123543'
+    >>> print(mozilla_bugwatch.title)
+    The Mozilla.org Bug Tracker #123543
 
     >>> for item in bugwatch_widget.renderItems(mozilla_bugwatch):
     ...     print_item(item)
@@ -110,11 +110,11 @@ Now let's create a new bug watch, pointing at bug #84 in the Gnome tracker.
     >>> bugwatch_widget = BugTaskBugWatchWidget(
     ...     bugwatch_field, bugwatch_field.vocabulary, request)
     >>> bugwatch = bugwatch_widget.getInputValue()
-    >>> bugwatch.bugtracker.title
-    u'GnomeGBug GTracker'
+    >>> print(bugwatch.bugtracker.title)
+    GnomeGBug GTracker
 
-    >>> bugwatch.remotebug
-    u'84'
+    >>> print(bugwatch.remotebug)
+    84
 
     >>> for bug_watch in bug_one.watches:
     ...     print("%s: #%s" % (
@@ -134,10 +134,10 @@ watch is being returned, and no new bug watch is created.
     >>> bugwatch_widget = BugTaskBugWatchWidget(
     ...     bugwatch_field, bugwatch_field.vocabulary, request)
     >>> bugwatch = bugwatch_widget.getInputValue()
-    >>> bugwatch.bugtracker.title
-    u'GnomeGBug GTracker'
-    >>> bugwatch.remotebug
-    u'84'
+    >>> print(bugwatch.bugtracker.title)
+    GnomeGBug GTracker
+    >>> print(bugwatch.remotebug)
+    84
 
     >>> for bug_watch in bug_one.watches:
     ...     print("%s: #%s" % (
diff --git a/lib/lp/bugs/doc/bugtask-display-widgets.txt b/lib/lp/bugs/doc/bugtask-display-widgets.txt
index 400b16d..fc5ebc8 100644
--- a/lib/lp/bugs/doc/bugtask-display-widgets.txt
+++ b/lib/lp/bugs/doc/bugtask-display-widgets.txt
@@ -17,8 +17,8 @@ person icon in front of the name.
     >>> from lp.bugs.interfaces.bug import IBugSet, IBugTask
     >>> bug_one = getUtility(IBugSet).get(1)
     >>> firefox_bugtask = bug_one.bugtasks[0]
-    >>> firefox_bugtask.assignee.displayname
-    u'Mark Shuttleworth'
+    >>> print(firefox_bugtask.assignee.displayname)
+    Mark Shuttleworth
 
     >>> assignee_field = IBugTask['assignee'].bind(firefox_bugtask)
     >>> assignee_widget = AssigneeDisplayWidget(assignee_field, None, None)
diff --git a/lib/lp/bugs/doc/bugtask-expiration.txt b/lib/lp/bugs/doc/bugtask-expiration.txt
index 11b755e..f8a1605 100644
--- a/lib/lp/bugs/doc/bugtask-expiration.txt
+++ b/lib/lp/bugs/doc/bugtask-expiration.txt
@@ -390,18 +390,22 @@ The user parameter indicates which user is performing the search. Only
 bugs that the user has permission to view are returned. A value of None
 indicates the anonymous user.
 
+    >>> from operator import attrgetter
+
     >>> expirable_bugtasks = bugtaskset.findExpirableBugTasks(
     ...     0, user=None, target=ubuntu)
     >>> visible_bugs = set(bugtask.bug for bugtask in expirable_bugtasks)
-    >>> print(sorted(bug.title for bug in visible_bugs))
-    [u'expirable_distro', u'recent']
+    >>> for bug in sorted(visible_bugs, key=attrgetter('title')):
+    ...     print(bug.title)
+    expirable_distro
+    recent
 
 If one of the bugs is set to private, anonymous users can no longer see
 it as being marked for expiration.
 
     >>> private_bug = ubuntu_bugtask.bug
-    >>> private_bug.title
-    u'expirable_distro'
+    >>> print(private_bug.title)
+    expirable_distro
     >>> private_bug.setPrivate(True, sample_person)
     True
     >>> reset_bug_modified_date(private_bug, 351)
@@ -409,8 +413,9 @@ it as being marked for expiration.
     >>> expirable_bugtasks = bugtaskset.findExpirableBugTasks(
     ...     0, user=None, target=ubuntu)
     >>> visible_bugs = set(bugtask.bug for bugtask in expirable_bugtasks)
-    >>> print(sorted(bug.title for bug in visible_bugs))
-    [u'recent']
+    >>> for bug in sorted(visible_bugs, key=attrgetter('title')):
+    ...     print(bug.title)
+    recent
 
 No Privileges Person can't see the bug either...
 
@@ -419,8 +424,9 @@ No Privileges Person can't see the bug either...
     >>> expirable_bugtasks = bugtaskset.findExpirableBugTasks(
     ...     0, user=no_priv, target=ubuntu)
     >>> visible_bugs = set(bugtask.bug for bugtask in expirable_bugtasks)
-    >>> print(sorted(bug.title for bug in visible_bugs))
-    [u'recent']
+    >>> for bug in sorted(visible_bugs, key=attrgetter('title')):
+    ...     print(bug.title)
+    recent
 
 ... unless they're subscribed to the bug.
 
@@ -430,8 +436,10 @@ No Privileges Person can't see the bug either...
     >>> expirable_bugtasks = bugtaskset.findExpirableBugTasks(
     ...     0, user=no_priv, target=ubuntu)
     >>> visible_bugs = set(bugtask.bug for bugtask in expirable_bugtasks)
-    >>> print(sorted(bug.title for bug in visible_bugs))
-    [u'expirable_distro', u'recent']
+    >>> for bug in sorted(visible_bugs, key=attrgetter('title')):
+    ...     print(bug.title)
+    expirable_distro
+    recent
 
 The Janitor needs to be able to access all bugs, even private ones, in
 order to be able to expire them. If the Janitor is passed as the user,
@@ -445,8 +453,10 @@ even the private bugs are returned.
     >>> expirable_bugtasks = bugtaskset.findExpirableBugTasks(
     ...     0, user=janitor, target=ubuntu)
     >>> visible_bugs = set(bugtask.bug for bugtask in expirable_bugtasks)
-    >>> print(sorted(bug.title for bug in visible_bugs))
-    [u'expirable_distro', u'recent']
+    >>> for bug in sorted(visible_bugs, key=attrgetter('title')):
+    ...     print(bug.title)
+    expirable_distro
+    recent
 
     >>> private_bug.setPrivate(False, sample_person)
     True
diff --git a/lib/lp/bugs/doc/bugtask-package-bugcounts.txt b/lib/lp/bugs/doc/bugtask-package-bugcounts.txt
index 611b752..60af96d 100644
--- a/lib/lp/bugs/doc/bugtask-package-bugcounts.txt
+++ b/lib/lp/bugs/doc/bugtask-package-bugcounts.txt
@@ -59,8 +59,8 @@ If we file a new unassigned bug on mozilla-firefox both the open and
 open_unassigned count will increase by one.
 
     >>> ubuntu_firefox = ubuntu.getSourcePackage('mozilla-firefox')
-    >>> ubuntu_firefox.bugtargetdisplayname
-    u'mozilla-firefox (Ubuntu)'
+    >>> print(ubuntu_firefox.bugtargetdisplayname)
+    mozilla-firefox (Ubuntu)
 
     >>> login('foo.bar@xxxxxxxxxxxxx')
     >>> from lp.bugs.interfaces.bug import CreateBugParams
diff --git a/lib/lp/bugs/doc/bugtask-package-widget.txt b/lib/lp/bugs/doc/bugtask-package-widget.txt
index 6277a90..81722bb 100644
--- a/lib/lp/bugs/doc/bugtask-package-widget.txt
+++ b/lib/lp/bugs/doc/bugtask-package-widget.txt
@@ -25,8 +25,8 @@ need a distribution, so we give the widget a distribution task to work with.
     >>> from lp.bugs.interfaces.bugtask import IBugTask
     >>> bug_one = getUtility(IBugSet).get(1)
     >>> ubuntu_task = bug_one.bugtasks[-2]
-    >>> ubuntu_task.distribution.name
-    u'ubuntu'
+    >>> print(ubuntu_task.distribution.name)
+    ubuntu
 
     >>> unbound_package_field = IBugTask['sourcepackagename']
     >>> if bool(getFeatureFlag('disclosure.dsp_picker.enabled')):
@@ -44,8 +44,8 @@ need a distribution, so we give the widget a distribution task to work with.
     ...     package_field, package_field.vocabulary, request)
     >>> widget.getInputValue().__class__.__name__ == expected_input_class
     True
-    >>> widget.getInputValue().name
-    u'evolution'
+    >>> print(widget.getInputValue().name)
+    evolution
 
 If we pass in a binary package name, which can be mapped to a source package
 name, the corresponding SourcePackageName is returned.  (In the case of the
@@ -62,8 +62,8 @@ new picker, this instead requires searching first.)
     ...     package_field, package_field.vocabulary, request)
     >>> widget.getInputValue().__class__.__name__ == expected_input_class
     True
-    >>> widget.getInputValue().name
-    u'linux-source-2.6.15'
+    >>> print(widget.getInputValue().name)
+    linux-source-2.6.15
 
 For some distributions we don't know exactly which source packages they
 contain, so IDistribution.guessPublishedSourcePackageName will raise a
@@ -90,8 +90,8 @@ will still be returned.
     ...     package_field, package_field.vocabulary, request)
     >>> widget.getInputValue().__class__.__name__ == expected_input_class
     True
-    >>> widget.getInputValue().name
-    u'evolution'
+    >>> print(widget.getInputValue().name)
+    evolution
 
 If we pass in a package name that doesn't exist in Launchpad, we get a
 ConversionError saying that the package name doesn't exist.
@@ -119,10 +119,10 @@ distribution from the request.
     >>> request = LaunchpadTestRequest(
     ...     form={'field.distribution': 'debian',
     ...           'field.sourcepackagename': 'linux-2.6.12'})
-    >>> BugTaskAlsoAffectsSourcePackageNameWidget(
+    >>> print(BugTaskAlsoAffectsSourcePackageNameWidget(
     ...     package_field, package_field.vocabulary,
-    ...     request).getDistribution().name
-    u'debian'
+    ...     request).getDistribution().name)
+    debian
 
 +distrotask always supplies a valid distribution name or none at all. If the
 name isn't the name of a distro, UnexpectedFormData is raised.
@@ -173,8 +173,8 @@ bug target rather than a bug task.
     ...     package_field, package_field.vocabulary, request)
     >>> widget.getInputValue().__class__.__name__ == expected_input_class
     True
-    >>> widget.getInputValue().name
-    u'evolution'
+    >>> print(widget.getInputValue().name)
+    evolution
 
 If we pass in a binary package name, which can be mapped to a source
 package name, the corresponding source package name (albeit as a
@@ -192,8 +192,8 @@ this instead requires searching first.)
     ...     package_field, package_field.vocabulary, request)
     >>> widget.getInputValue().__class__.__name__ == expected_input_class
     True
-    >>> widget.getInputValue().name
-    u'linux-source-2.6.15'
+    >>> print(widget.getInputValue().name)
+    linux-source-2.6.15
 
 For some distributions we don't know exactly which source packages they
 contain, so IDistribution.guessPublishedSourcePackageName will raise a
@@ -217,8 +217,8 @@ will still be returned.
     ...     package_field, package_field.vocabulary, request)
     >>> widget.getInputValue().__class__.__name__ == expected_input_class
     True
-    >>> widget.getInputValue().name
-    u'evolution'
+    >>> print(widget.getInputValue().name)
+    evolution
 
 If we pass in a package name that doesn't exist in Launchpad, we get a
 ConversionError saying that the package name doesn't exist.
diff --git a/lib/lp/bugs/doc/bugtracker.txt b/lib/lp/bugs/doc/bugtracker.txt
index 1163135..3556598 100644
--- a/lib/lp/bugs/doc/bugtracker.txt
+++ b/lib/lp/bugs/doc/bugtracker.txt
@@ -115,8 +115,8 @@ bug tracker.
     ...     baseurl='http://bugs.example.com', owner=sample_person,
     ...     bugtrackertype=BugTrackerType.BUGZILLA,
     ...     title=None, summary=None, contactdetails=None, name=None)
-    >>> a_bugtracker.name
-    u'auto-bugs.example.com'
+    >>> print(a_bugtracker.name)
+    auto-bugs.example.com
 
 ensureBugTracker() also performs collision-avoidance on the names which
 it generates using make_bugtracker_name(). If another bug tracker is
@@ -128,8 +128,8 @@ collide.
     ...     baseurl='http://bugs.example.com/ni', owner=sample_person,
     ...     bugtrackertype=BugTrackerType.BUGZILLA,
     ...     title=None, summary=None, contactdetails=None, name=None)
-    >>> a_bugtracker.name
-    u'auto-bugs.example.com-1'
+    >>> print(a_bugtracker.name)
+    auto-bugs.example.com-1
 
 
 Top Bug Trackers
@@ -140,10 +140,13 @@ ordered by the number of bugs being monitored by Malone in each of
 them. Use IBugTrackerSet.getMostActiveBugTrackers to get this list.
 
     >>> top_trackers = bugtracker_set.getMostActiveBugTrackers(limit=4)
-    >>> sorted([(tracker.watches.count(), tracker.name)
-    ...         for tracker in top_trackers])
-    [(1, u'ubuntu-bugzilla'), (2, u'gnome-bugzilla'),
-     (4, u'mozilla.org'), (5, u'debbugs')]
+    >>> for tracker in sorted(
+    ...         top_trackers, key=lambda tracker: tracker.watches.count()):
+    ...     print('%d: %s' % (tracker.watches.count(), tracker.name))
+    1: ubuntu-bugzilla
+    2: gnome-bugzilla
+    4: mozilla.org
+    5: debbugs
 
 
 Getting Bug Trackers
@@ -154,16 +157,16 @@ its base URL.
 
     >>> ubuntu_bugzilla = bugtracker_set.queryByBaseURL(
     ...     u'http://bugzilla.ubuntu.com/bugs/')
-    >>> ubuntu_bugzilla.baseurl
-    u'http://bugzilla.ubuntu.com/bugs/'
+    >>> print(ubuntu_bugzilla.baseurl)
+    http://bugzilla.ubuntu.com/bugs/
 
 It's necessary to specify the exact URL, differences in the schema
 (http vs. https) and trailing slashes are accepted.
 
     >>> ubuntu_bugzilla = bugtracker_set.queryByBaseURL(
     ...     u'https://bugzilla.ubuntu.com/bugs')
-    >>> ubuntu_bugzilla.baseurl
-    u'http://bugzilla.ubuntu.com/bugs/'
+    >>> print(ubuntu_bugzilla.baseurl)
+    http://bugzilla.ubuntu.com/bugs/
 
 If no bug tracker can be found None is returned.
 
@@ -187,9 +190,10 @@ aliases.
     ...     'https://norwich.example.com/',
     ...     'http://cambridge.example.com/']
 
-    >>> mozilla_bugzilla.aliases
-    (u'http://cambridge.example.com/',
-     u'https://norwich.example.com/')
+    >>> for alias in mozilla_bugzilla.aliases:
+    ...     print(alias)
+    http://cambridge.example.com/
+    https://norwich.example.com/
 
     >>> mozilla_bugzilla.aliases = []
     >>> mozilla_bugzilla.aliases
@@ -213,12 +217,14 @@ the same effect as assigning an empty list.
     ()
 
     >>> mozilla_bugzilla.aliases = set([u'http://set.example.com/'])
-    >>> mozilla_bugzilla.aliases
-    (u'http://set.example.com/',)
+    >>> for alias in mozilla_bugzilla.aliases:
+    ...     print(alias)
+    http://set.example.com/
 
     >>> mozilla_bugzilla.aliases = (u'http://tuple.example.com/',)
-    >>> mozilla_bugzilla.aliases
-    (u'http://tuple.example.com/',)
+    >>> for alias in mozilla_bugzilla.aliases:
+    ...     print(alias)
+    http://tuple.example.com/
 
 Your ordering is not preserved; aliases are sorted using Python's
 standard unicode ordering.
@@ -226,11 +232,12 @@ standard unicode ordering.
     >>> mozilla_bugzilla.aliases = (
     ...     u'http://%s.example.com/' % domain
     ...     for domain in '111 zzz ccc ZZZ'.split())
-    >>> mozilla_bugzilla.aliases
-    (u'http://111.example.com/',
-     u'http://ZZZ.example.com/',
-     u'http://ccc.example.com/',
-     u'http://zzz.example.com/')
+    >>> for alias in mozilla_bugzilla.aliases:
+    ...     print(alias)
+    http://111.example.com/
+    http://ZZZ.example.com/
+    http://ccc.example.com/
+    http://zzz.example.com/
 
 BugTrackerAliases can also be looked up by bug tracker.
 
@@ -240,10 +247,13 @@ BugTrackerAliases can also be looked up by bug tracker.
 
 Query by bug tracker:
 
-    >>> sorted(alias.base_url for alias in
-    ...        bugtrackeralias_set.queryByBugTracker(mozilla_bugzilla))
-    [u'http://just.example.com/',
-     u'http://magic.example.com/']
+    >>> from operator import attrgetter
+    >>> for alias in sorted(
+    ...         bugtrackeralias_set.queryByBugTracker(mozilla_bugzilla),
+    ...         key=attrgetter('base_url')):
+    ...     print(alias.base_url)
+    http://just.example.com/
+    http://magic.example.com/
 
 The aliases attribute never contains the current baseurl. For example,
 if BugTracker.baseurl is changed to an existing alias of itself, the
@@ -251,13 +261,16 @@ aliases attribute hides the baseurl, although it is still recorded as
 an alias.
 
     >>> mozilla_bugzilla.baseurl = u'http://magic.example.com/'
-    >>> mozilla_bugzilla.aliases
-    (u'http://just.example.com/',)
+    >>> for alias in mozilla_bugzilla.aliases:
+    ...     print(alias)
+    http://just.example.com/
 
-    >>> sorted(alias.base_url for alias in
-    ...        bugtrackeralias_set.queryByBugTracker(mozilla_bugzilla))
-    [u'http://just.example.com/',
-     u'http://magic.example.com/']
+    >>> for alias in sorted(
+    ...         bugtrackeralias_set.queryByBugTracker(mozilla_bugzilla),
+    ...         key=attrgetter('base_url')):
+    ...     print(alias.base_url)
+    http://just.example.com/
+    http://magic.example.com/
 
     >>> mozilla_bugzilla.baseurl = u'https://bugzilla.mozilla.org/'
 
@@ -268,8 +281,8 @@ Pillars for bugtrackers
     >>> trackers = list(bugtracker_set)
     >>> pillars = bugtracker_set.getPillarsForBugtrackers(trackers)
     >>> for t in pillars:
-    ...     print(t.name, [p.name for p in pillars[t]])
-    gnome-bugzilla [u'gnome-terminal', u'gnome']
+    ...     print(t.name, pretty([p.name for p in pillars[t]]))
+    gnome-bugzilla ['gnome-terminal', 'gnome']
 
 
 Imported bug messages
diff --git a/lib/lp/bugs/doc/bugwatch.txt b/lib/lp/bugs/doc/bugwatch.txt
index 4698d5a..f69fdc8 100644
--- a/lib/lp/bugs/doc/bugwatch.txt
+++ b/lib/lp/bugs/doc/bugwatch.txt
@@ -27,31 +27,31 @@ Bug watches are accessed via a utility that provides IBugWatchSet.
     ...
     lp.app.errors.NotFoundError: 98765
     >>> bugwatch = getUtility(IBugWatchSet).get(2)
-    >>> bugwatch.remotebug
-    u'2000'
+    >>> print(bugwatch.remotebug)
+    2000
 
 The `url` property of the bugwatch produces the actual URL under which
 that bug lives in the remote system.
 
-    >>> bugwatch.bugtracker.baseurl
-    u'https://bugzilla.mozilla.org/'
-    >>> bugwatch.url
-    u'https://bugzilla.mozilla.org/show_bug.cgi?id=2000'
+    >>> print(bugwatch.bugtracker.baseurl)
+    https://bugzilla.mozilla.org/
+    >>> print(bugwatch.url)
+    https://bugzilla.mozilla.org/show_bug.cgi?id=2000
 
 It works regardless of whether the bugtracker's baseurl ends with a
 slash or not:
 
     >>> bugwatch = getUtility(IBugWatchSet).get(4)
-    >>> bugwatch.bugtracker.baseurl
-    u'http://bugzilla.gnome.org/bugs'
-    >>> bugwatch.url
-    u'http://bugzilla.gnome.org/bugs/show_bug.cgi?id=3224'
+    >>> print(bugwatch.bugtracker.baseurl)
+    http://bugzilla.gnome.org/bugs
+    >>> print(bugwatch.url)
+    http://bugzilla.gnome.org/bugs/show_bug.cgi?id=3224
 
     >>> bugwatch = getUtility(IBugWatchSet).get(6)
-    >>> bugwatch.bugtracker.baseurl
-    u'http://bugzilla.ubuntu.com/bugs/'
-    >>> bugwatch.url
-    u'http://bugzilla.ubuntu.com/bugs/show_bug.cgi?id=1234'
+    >>> print(bugwatch.bugtracker.baseurl)
+    http://bugzilla.ubuntu.com/bugs/
+    >>> print(bugwatch.url)
+    http://bugzilla.ubuntu.com/bugs/show_bug.cgi?id=1234
 
 Watches of Email Address bugtrackers are slightly different: the `url`
 property is always the same as the bugtracker baseurl property.
@@ -67,30 +67,44 @@ property is always the same as the bugtracker baseurl property.
     ...         getUtility(IBugSet).get(1), getUtility(IPersonSet).get(1),
     ...         email_bugtracker, 'remote-bug-id'))
 
-    >>> email_bugwatch.remotebug
-    u'remote-bug-id'
-    >>> email_bugwatch.url
-    u'mailto:bugs@xxxxxxxxxxx'
-    >>> email_bugtracker.baseurl
-    u'mailto:bugs@xxxxxxxxxxx'
+    >>> print(email_bugwatch.remotebug)
+    remote-bug-id
+    >>> print(email_bugwatch.url)
+    mailto:bugs@xxxxxxxxxxx
+    >>> print(email_bugtracker.baseurl)
+    mailto:bugs@xxxxxxxxxxx
 
 Bug watches can also be accessed as a property of a bug tracker, with
 the .watches attribute.
 
+    >>> from operator import attrgetter
     >>> debbugs = bugtrackerset['debbugs']
-    >>> sorted([(watch.bug.id, watch.remotebug) for watch in debbugs.watches])
-    [(1, u'304014'), (2, u'327452'), (3, u'327549'), (7, u'280883'),
-     (15, u'308994')]
+    >>> for watch in sorted(
+    ...         debbugs.watches, key=attrgetter('bug.id', 'remotebug')):
+    ...     print('%d: %s' % (watch.bug.id, watch.remotebug))
+    1: 304014
+    2: 327452
+    3: 327549
+    7: 280883
+    15: 308994
     >>> mozilla_bugtracker = bugtrackerset['mozilla.org']
-    >>> sorted([(watch.bug.id, watch.remotebug) for watch in
-    ...     mozilla_bugtracker.watches])
-    [(1, u'123543'), (1, u'2000'), (1, u'42'), (2, u'42')]
+    >>> for watch in sorted(
+    ...         mozilla_bugtracker.watches,
+    ...         key=attrgetter('bug.id', 'remotebug')):
+    ...     print('%d: %s' % (watch.bug.id, watch.remotebug))
+    1: 123543
+    1: 2000
+    1: 42
+    2: 42
 
 To get the latest 10 watches, use IBugTracker.latestwatches:
 
-    >>> [(watch.bug.id, watch.remotebug) for watch in
-    ...     mozilla_bugtracker.latestwatches]
-    [(1, u'2000'), (1, u'123543'), (1, u'42'), (2, u'42')]
+    >>> for watch in mozilla_bugtracker.latestwatches:
+    ...     print('%d: %s' % (watch.bug.id, watch.remotebug))
+    1: 2000
+    1: 123543
+    1: 42
+    2: 42
 
 We can retrieve the list of Launchpad bugs watching a particular
 remote bug using getBugsWatching():
@@ -107,10 +121,10 @@ being added.
     >>> mozilla_watch = bug_one.getBugWatch(mozilla_bugtracker, '2000')
     >>> mozilla_watch in bug_one.watches
     True
-    >>> mozilla_watch.bugtracker.name
-    u'mozilla.org'
-    >>> mozilla_watch.remotebug
-    u'2000'
+    >>> print(mozilla_watch.bugtracker.name)
+    mozilla.org
+    >>> print(mozilla_watch.remotebug)
+    2000
 
 If no matching bug watch can be found, None is returned.
 
@@ -130,8 +144,8 @@ To create a bugwatch, use IBugWatchSet.createBugWatch:
     >>> bugwatch = getUtility(IBugWatchSet).createBugWatch(
     ...     bug=bug_one, owner=sample_person, bugtracker=mozilla_bugtracker,
     ...     remotebug='1234')
-    >>> bugwatch.url
-    u'https://bugzilla.mozilla.org/show_bug.cgi?id=1234'
+    >>> print(bugwatch.url)
+    https://bugzilla.mozilla.org/show_bug.cgi?id=1234
     >>> bugwatch.lastchecked is None
     True
 
@@ -149,8 +163,8 @@ group_id and aid arguments filled in:
     >>> bugwatch = getUtility(IBugWatchSet).createBugWatch(
     ...     bug=bug_one, owner=sample_person, bugtracker=sftracker,
     ...     remotebug='1337833')
-    >>> bugwatch.url
-    u'http://sourceforge.net/support/tracker.php?aid=1337833'
+    >>> print(bugwatch.url)
+    http://sourceforge.net/support/tracker.php?aid=1337833
 
 Extracting Bug Watches From Text
 --------------------------------
@@ -205,13 +219,13 @@ bugs.gnome.org vs. bugzilla.gnome.org).
     >>> old_bugtracker_count = getUtility(IBugTrackerSet).count
     >>> gnome_bugzilla = getUtility(IBugTrackerSet).queryByBaseURL(
     ...     'http://bugzilla.gnome.org/bugs')
-    >>> gnome_bugzilla.name
-    u'gnome-bugzilla'
+    >>> print(gnome_bugzilla.name)
+    gnome-bugzilla
     >>> text = "https://bugzilla.gnome.org/bugs/show_bug.cgi?id=12345";
     >>> [gnome_bugwatch] = getUtility(IBugWatchSet).fromText(
     ...     text, bug_one, sample_person)
-    >>> gnome_bugwatch.bugtracker.name
-    u'gnome-bugzilla'
+    >>> print(gnome_bugwatch.bugtracker.name)
+    gnome-bugzilla
     >>> new_bugtracker_count = getUtility(IBugTrackerSet).count
     >>> old_bugtracker_count == new_bugtracker_count
     True
@@ -299,8 +313,8 @@ have to be convertable to a real Malone status.
 
     >>> debian_bugwatch.updateStatus(u'some status', BugTaskStatus.NEW)
 
-    >>> debian_bugwatch.remotestatus
-    u'some status'
+    >>> print(debian_bugwatch.remotestatus)
+    some status
     >>> print(debian_task.status.title)
     New
 
@@ -368,8 +382,8 @@ doesn't necessarily have to be convertible to a real Malone status.
     >>> debian_bugwatch.updateImportance(u'some importance',
     ...     BugTaskImportance.CRITICAL)
 
-    >>> debian_bugwatch.remote_importance
-    u'some importance'
+    >>> print(debian_bugwatch.remote_importance)
+    some importance
     >>> print(debian_task.importance.title)
     Critical
 
@@ -491,8 +505,9 @@ watches that are linked to a bug task targeted to the Product.
     >>> bug_watch = factory.makeBugWatch(remote_bug='42')
     >>> bug_task.bugwatch = bug_watch
     >>> product.bugtracker = bug_watch.bugtracker
-    >>> [bug_watch.remotebug for bug_watch in product.getLinkedBugWatches()]
-    [u'42']
+    >>> for bug_watch in product.getLinkedBugWatches():
+    ...     print(bug_watch.remotebug)
+    42
 
 It's not uncommon to link to other bug trackers than the one the Product
 is using officially, for example to link to related bugs. To avoid
@@ -511,8 +526,9 @@ Bug watches can be removed using the removeWatch method.
 
     >>> bug_watch = factory.makeBugWatch(remote_bug='42')
     >>> bug = bug_watch.bug
-    >>> [bug_watch.remotebug for bug_watch in bug.watches]
-    [u'42']
+    >>> for bug_watch in bug.watches:
+    ...     print(bug_watch.remotebug)
+    42
     >>> bug.removeWatch(bug_watch, factory.makePerson())
     >>> [bug_watch.remotebug for bug_watch in bug.watches]
     []
diff --git a/lib/lp/bugs/doc/checkwatches-batching.txt b/lib/lp/bugs/doc/checkwatches-batching.txt
index f4353f7..16b165b 100644
--- a/lib/lp/bugs/doc/checkwatches-batching.txt
+++ b/lib/lp/bugs/doc/checkwatches-batching.txt
@@ -23,8 +23,8 @@ Basics
 
 When there are no bug watches to check, the result is empty.
 
-    >>> pprint(updater._getRemoteIdsToCheck(
-    ...     remote, [], batch_size=2))
+    >>> print(pretty(updater._getRemoteIdsToCheck(
+    ...     remote, [], batch_size=2)))
     {'all_remote_ids': [],
      'remote_ids_to_check': [],
      'unmodified_remote_ids': []}
@@ -38,10 +38,10 @@ bug IDs given.
     ...     ]
     >>> transaction.commit()
 
-    >>> pprint(updater._getRemoteIdsToCheck(
-    ...     remote, bug_watches, batch_size=2))
-    {'all_remote_ids': [u'a', u'b'],
-     'remote_ids_to_check': [u'a', u'b'],
+    >>> print(pretty(updater._getRemoteIdsToCheck(
+    ...     remote, bug_watches, batch_size=2)))
+    {'all_remote_ids': ['a', 'b'],
+     'remote_ids_to_check': ['a', 'b'],
      'unmodified_remote_ids': []}
 
 With more than batch_size watches, it advises to only check a subset
@@ -54,10 +54,10 @@ of the remote bug IDs given.
     ...     ]
     >>> transaction.commit()
 
-    >>> pprint(updater._getRemoteIdsToCheck(
-    ...     remote, bug_watches, batch_size=2))
-    {'all_remote_ids': [u'a', u'b'],
-     'remote_ids_to_check': [u'a', u'b'],
+    >>> print(pretty(updater._getRemoteIdsToCheck(
+    ...     remote, bug_watches, batch_size=2)))
+    {'all_remote_ids': ['a', 'b'],
+     'remote_ids_to_check': ['a', 'b'],
      'unmodified_remote_ids': []}
 
 
@@ -74,8 +74,8 @@ asked which of a list of bugs have been modified since a given date.
     >>> class QueryableRemoteSystem:
     ...     sync_comments = False
     ...     def getModifiedRemoteBugs(self, remote_bug_ids, timestamp):
-    ...         print("getModifiedRemoteBugs(%r, %r)" % (
-    ...             remote_bug_ids, timestamp))
+    ...         print("getModifiedRemoteBugs(%s, %r)" % (
+    ...             pretty(remote_bug_ids), timestamp))
     ...         # Return every *other* bug ID for demo purposes.
     ...         return remote_bug_ids[::2]
 
@@ -89,7 +89,7 @@ remote system is not queried.
     ...     remote, [], batch_size=2,
     ...     server_time=now, now=now)
 
-    >>> pprint(ids_to_check)
+    >>> print(pretty(ids_to_check))
     {'all_remote_ids': [],
      'remote_ids_to_check': [],
      'unmodified_remote_ids': []}
@@ -108,12 +108,12 @@ queried once, and we are advised to check only one of the watches.
     >>> ids_to_check = updater._getRemoteIdsToCheck(
     ...     remote, bug_watches, batch_size=2,
     ...     server_time=now, now=now)
-    getModifiedRemoteBugs([u'a', u'b'], datetime.datetime(...))
+    getModifiedRemoteBugs(['a', 'b'], datetime.datetime(...))
 
-    >>> pprint(ids_to_check)
-    {'all_remote_ids': [u'a', u'b'],
-     'remote_ids_to_check': [u'a'],
-     'unmodified_remote_ids': [u'b']}
+    >>> print(pretty(ids_to_check))
+    {'all_remote_ids': ['a', 'b'],
+     'remote_ids_to_check': ['a'],
+     'unmodified_remote_ids': ['b']}
 
 With just more than batch_size previously checked watches, the remote
 system is queried twice, and we are advised to check two of the
@@ -131,10 +131,10 @@ watches.
     >>> ids_to_check = updater._getRemoteIdsToCheck(
     ...     remote, bug_watches, batch_size=2,
     ...     server_time=now, now=now)
-    getModifiedRemoteBugs([u'a', u'b'], datetime.datetime(...))
-    getModifiedRemoteBugs([u'c'], datetime.datetime(...))
+    getModifiedRemoteBugs(['a', 'b'], datetime.datetime(...))
+    getModifiedRemoteBugs(['c'], datetime.datetime(...))
 
-    >>> pprint(ids_to_check)
-    {'all_remote_ids': [u'a', u'b', u'c'],
-     'remote_ids_to_check': [u'a', u'c'],
-     'unmodified_remote_ids': [u'b']}
+    >>> print(pretty(ids_to_check))
+    {'all_remote_ids': ['a', 'b', 'c'],
+     'remote_ids_to_check': ['a', 'c'],
+     'unmodified_remote_ids': ['b']}
diff --git a/lib/lp/bugs/doc/cve.txt b/lib/lp/bugs/doc/cve.txt
index 9f83aeb..631cd55 100644
--- a/lib/lp/bugs/doc/cve.txt
+++ b/lib/lp/bugs/doc/cve.txt
@@ -32,8 +32,8 @@ To create a CVE, call ICveSet.new(sequence, description,
     >>> cve = cveset.new(sequence="2004-0276",
     ...     description="A new CVE", status=CveStatus.ENTRY,
     ...     )
-    >>> cve.displayname
-    u'CVE-2004-0276'
+    >>> print(cve.displayname)
+    CVE-2004-0276
 
 === Number of CVE links ===
 
@@ -127,8 +127,8 @@ CVEs creation accepts 2014 format:
     >>> cve_2014 = cveset.new(sequence="2014-999999",
     ...     description="A new-style CVE sequence", status=CveStatus.ENTRY,
     ...     )
-    >>> cve_2014.displayname
-    u'CVE-2014-999999'
+    >>> print(cve_2014.displayname)
+    CVE-2014-999999
 
 Text references to CVEs using 2014 format can be found:
  
@@ -152,9 +152,10 @@ ICveSet.getBugCvesForBugTasks:
     >>> ubuntu = Distribution.selectOneBy(name="ubuntu")
     >>> ubuntu_tasks = ubuntu.searchTasks(params)
     >>> bugcves = cveset.getBugCvesForBugTasks(ubuntu_tasks)
-    >>> [(bug.id, cve.title) for (bug, cve) in bugcves]
-    [(1, u'CVE-1999-8979 (Entry)'),
-     (2, u'CVE-1999-2345 (Candidate)')]
+    >>> for (bug, cve) in bugcves:
+    ...     print('%d: %s' % (bug.id, cve.title))
+    1: CVE-1999-8979 (Entry)
+    2: CVE-1999-2345 (Candidate)
 
 This method is used in the CVEReportView:
 
@@ -169,12 +170,12 @@ connects bugs to bugtasks and CVEs. The open_cve_bugtasks returned are, not
 surprisingly, the same the method above returned:
 
     >>> for bugtaskcve in cve_report.open_cve_bugtasks:
-    ...    ([bugtask.title for bugtask in bugtaskcve.bugtasks],
-    ...     [cve['displayname'] for cve in bugtaskcve.cves])
-    ([u'Bug #1 in mozilla-firefox (Ubuntu): "Firefox does not support SVG"'],
-     [u'CVE-1999-8979'])
-    ([u'Bug #2 in Ubuntu: "Blackhole Trash folder"'],
-     [u'CVE-1999-2345'])
+    ...     print(pretty([bugtask.title for bugtask in bugtaskcve.bugtasks]))
+    ...     print(pretty([cve['displayname'] for cve in bugtaskcve.cves]))
+    ['Bug #1 in mozilla-firefox (Ubuntu): "Firefox does not support SVG"']
+    ['CVE-1999-8979']
+    ['Bug #2 in Ubuntu: "Blackhole Trash folder"']
+    ['CVE-1999-2345']
 
 There are no resolved bugtasks linked to CVEs in Ubuntu:
 
diff --git a/lib/lp/bugs/doc/displaying-bugs-and-tasks.txt b/lib/lp/bugs/doc/displaying-bugs-and-tasks.txt
index 06c0270..25f9674 100644
--- a/lib/lp/bugs/doc/displaying-bugs-and-tasks.txt
+++ b/lib/lp/bugs/doc/displaying-bugs-and-tasks.txt
@@ -124,18 +124,18 @@ Let's see some examples of how this works:
   >>> test_task.transitionToAssignee(foobar)
   >>> test_task.transitionToStatus(
   ...   BugTaskStatus.NEW, getUtility(ILaunchBag).user)
-  >>> render_bugtask_status(test_task)
-  u'New, assigned to ...Foo Bar...'
+  >>> print(render_bugtask_status(test_task))
+  New, assigned to ...Foo Bar...
 
   >>> test_task.transitionToStatus(
   ...   BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
-  >>> render_bugtask_status(test_task)
-  u'Confirmed, assigned to ...Foo Bar...'
+  >>> print(render_bugtask_status(test_task))
+  Confirmed, assigned to ...Foo Bar...
 
   >>> test_task.transitionToStatus(
   ...   BugTaskStatus.INVALID, getUtility(ILaunchBag).user)
-  >>> render_bugtask_status(test_task)
-  u'Invalid by ...Foo Bar...'
+  >>> print(render_bugtask_status(test_task))
+  Invalid by ...Foo Bar...
 
   >>> test_task.transitionToAssignee(None)
   >>> render_bugtask_status(test_task)
@@ -147,8 +147,8 @@ Let's see some examples of how this works:
   'Fix released (unassigned)'
 
   >>> test_task.transitionToAssignee(foobar)
-  >>> render_bugtask_status(test_task)
-  u'Fix released, assigned to ...Foo Bar...'
+  >>> print(render_bugtask_status(test_task))
+  Fix released, assigned to ...Foo Bar...
 
 Lastly, some cleanup:
 
diff --git a/lib/lp/bugs/doc/externalbugtracker-bug-imports.txt b/lib/lp/bugs/doc/externalbugtracker-bug-imports.txt
index 1266847..939e953 100644
--- a/lib/lp/bugs/doc/externalbugtracker-bug-imports.txt
+++ b/lib/lp/bugs/doc/externalbugtracker-bug-imports.txt
@@ -72,10 +72,10 @@ distributions are supported as the bug target.
 The summary and descriptions of the imported bugs are what was returned
 by getBugSummaryAndDescription().
 
-    >>> bug.title
-    u'Sample summary 3'
-    >>> bug.description
-    u'Sample description 3.'
+    >>> print(bug.title)
+    Sample summary 3
+    >>> print(bug.description)
+    Sample description 3.
 
 The bug reporter, as returned by getBugReporter(), got automatically created.
 
@@ -101,8 +101,8 @@ account is marked as NEW, since we don't know whether it's valid.
 
     >>> bug.owner.creation_rationale.name
     'BUGIMPORT'
-    >>> bug.owner.creation_comment
-    u'when importing bug #3 from http://...'
+    >>> print(bug.owner.creation_comment)
+    when importing bug #3 from http://...
 
 No one got subscribed to the created bug, since the relevant people
 already get email notifications via the external bug tracker.
@@ -115,13 +115,13 @@ bug watch pointing to the original remote bug, so that the bug report is
 kept in sync.
 
     >>> [added_task] = bug.bugtasks
-    >>> added_task.bugtargetname
-    u'evolution (Debian)'
+    >>> print(added_task.bugtargetname)
+    evolution (Debian)
 
-    >>> added_task.bugwatch.bugtracker.name
-    u'bugzilla-checkwatches-1'
-    >>> added_task.bugwatch.remotebug
-    u'3'
+    >>> print(added_task.bugwatch.bugtracker.name)
+    bugzilla-checkwatches-1
+    >>> print(added_task.bugwatch.remotebug)
+    3
 
 
 Non-existent source package
@@ -144,8 +144,8 @@ the script gets notified about it.
     (OOPS-...)
 
     >>> [added_task] = bug.bugtasks
-    >>> added_task.distribution.name
-    u'debian'
+    >>> print(added_task.distribution.name)
+    debian
     >>> print(added_task.sourcepackagename)
     None
 
@@ -179,10 +179,10 @@ valid email address), they still won't be subscribed to the imported bug.
     >>> bug = bug_watch_updater.importBug(
     ...     external_bugtracker, bugtracker, debian, '7')
 
-    >>> bug.owner.name
-    u'no-priv'
-    >>> bug.owner.displayname
-    u'No Privileges Person'
+    >>> print(bug.owner.name)
+    no-priv
+    >>> print(bug.owner.displayname)
+    No Privileges Person
 
     >>> [person.name for person in bug.getDirectSubscribers()]
     []
diff --git a/lib/lp/bugs/doc/externalbugtracker-bugzilla.txt b/lib/lp/bugs/doc/externalbugtracker-bugzilla.txt
index ff011cb..71589c2 100644
--- a/lib/lp/bugs/doc/externalbugtracker-bugzilla.txt
+++ b/lib/lp/bugs/doc/externalbugtracker-bugzilla.txt
@@ -773,10 +773,10 @@ information to be fetched from the external Bugzilla instance.
     ...     'RESOLVED', 'FIXED', 'MEDIUM', 'NORMAL')}
     >>> with external_bugzilla.responses():
     ...     external_bugzilla.initializeRemoteBugDB(['84'])
-    >>> external_bugzilla.remote_bug_product['84']
-    u'product-84'
-    >>> external_bugzilla.getRemoteProduct('84')
-    u'product-84'
+    >>> print(external_bugzilla.remote_bug_product['84'])
+    product-84
+    >>> print(external_bugzilla.getRemoteProduct('84'))
+    product-84
 
 Sometimes we might not get the product in the bug listing. In these
 cases getRemoteProduct() returns None.
diff --git a/lib/lp/bugs/doc/externalbugtracker-comment-imports.txt b/lib/lp/bugs/doc/externalbugtracker-comment-imports.txt
index d2c7931..6f4a849 100644
--- a/lib/lp/bugs/doc/externalbugtracker-comment-imports.txt
+++ b/lib/lp/bugs/doc/externalbugtracker-comment-imports.txt
@@ -153,8 +153,8 @@ invalid.
     >>> bug.messages[-1].owner == joe
     True
 
-    >>> joe.displayname
-    u'Joe Bloggs'
+    >>> print(joe.displayname)
+    Joe Bloggs
     >>> print(joe.preferredemail)
     None
     >>> print(joe.creation_rationale.name)
@@ -179,8 +179,8 @@ is associated with the existing person.
     >>> bugwatch_updater.importBugComments()
     INFO Imported 1 comments for remote bug 123456 on ...
 
-    >>> bug.messages[-1].owner.name
-    u'no-priv'
+    >>> print(bug.messages[-1].owner.name)
+    no-priv
 
 It's also possible for Launchpad to create Persons from remote
 bugtracker users when the remote bugtracker doesn't specify an email
@@ -197,8 +197,8 @@ used to create a Person based on the displayname alone.
     >>> bugwatch_updater.importBugComments()
     INFO Imported 1 comments for remote bug 123456 on ...
 
-    >>> bug.messages[-1].owner.name
-    u'noemail-bugzilla-checkwatches-1'
+    >>> print(bug.messages[-1].owner.name)
+    noemail-bugzilla-checkwatches-1
 
     >>> print(bug.messages[-1].owner.preferredemail)
     None
@@ -480,8 +480,8 @@ is used as the From address.
 
     >>> notifications[0].is_comment
     True
-    >>> notifications[0].message.owner.name
-    u'bug-watch-updater'
+    >>> print(notifications[0].message.owner.name)
+    bug-watch-updater
 
     >>> print(notifications[0].message.text_contents)
     Launchpad has imported 2 comments from the remote bug at
diff --git a/lib/lp/bugs/doc/externalbugtracker-comment-pushing.txt b/lib/lp/bugs/doc/externalbugtracker-comment-pushing.txt
index dbc1c6a..e64217b 100644
--- a/lib/lp/bugs/doc/externalbugtracker-comment-pushing.txt
+++ b/lib/lp/bugs/doc/externalbugtracker-comment-pushing.txt
@@ -85,8 +85,9 @@ remote server.
     >>> comments = [
     ...     comment.message.text_contents
     ...     for comment in bug_watch.unpushed_comments]
-    >>> print(comments)
-    [u'Pushing, for the purpose of.']
+    >>> for comment in comments:
+    ...     print(comment)
+    Pushing, for the purpose of.
 
 The CheckwatchesMaster method pushBugComments() is responsible for
 calling the addRemoteComment() method of ISupportsCommentPushing for
diff --git a/lib/lp/bugs/doc/externalbugtracker-debbugs.txt b/lib/lp/bugs/doc/externalbugtracker-debbugs.txt
index 576c13e..4353e79 100644
--- a/lib/lp/bugs/doc/externalbugtracker-debbugs.txt
+++ b/lib/lp/bugs/doc/externalbugtracker-debbugs.txt
@@ -662,10 +662,12 @@ tracker.
 
     >>> transaction.commit()
     >>> debbugs = getUtility(ILaunchpadCelebrities).debbugs
-    >>> [bug.title for bug in debbugs.getBugsWatching('237001')]
-    [u'evolution mail crashes on opening an email with a TIFF attachment']
-    >>> [bug.title for bug in debbugs.getBugsWatching('322535')]
-    [u'evolution: Multiple format string vulnerabilities in Evolution']
+    >>> for bug in debbugs.getBugsWatching('237001'):
+    ...     print(bug.title)
+    evolution mail crashes on opening an email with a TIFF attachment
+    >>> for bug in debbugs.getBugsWatching('322535'):
+    ...     print(bug.title)
+    evolution: Multiple format string vulnerabilities in Evolution
 
 In addition to simply importing the bugs and linking it to the debbugs
 bug, it will also create an Ubuntu task for the imported bugs. This will
diff --git a/lib/lp/bugs/doc/externalbugtracker-emailaddress.txt b/lib/lp/bugs/doc/externalbugtracker-emailaddress.txt
index 76fce6f..c440f94 100644
--- a/lib/lp/bugs/doc/externalbugtracker-emailaddress.txt
+++ b/lib/lp/bugs/doc/externalbugtracker-emailaddress.txt
@@ -62,8 +62,8 @@ the local part of an email address (e.g. <local_name>@foobar.com).
     >>> verifyObject(IBugTracker, other_tracker)
     True
 
-    >>> other_tracker.name
-    u'auto-another.bugtracker'
+    >>> print(other_tracker.name)
+    auto-another.bugtracker
 
 
 == Adding a BugWatch to an upstream email address ==
@@ -83,8 +83,12 @@ because BugWatch.remotebug is a NOT NULL field).
     >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
     >>> bug_watch = example_bug.addWatch(bugtracker=email_tracker,
     ...     remotebug='', owner=getUtility(ILaunchpadCelebrities).janitor)
-    >>> bug_watch.bugtracker.name, bug_watch.remotebug, bug_watch.bug.id
-    (u'email-tracker', u'', 15)
+    >>> print(bug_watch.bugtracker.name)
+    email-tracker
+    >>> print(bug_watch.remotebug)
+    <BLANKLINE>
+    >>> bug_watch.bug.id
+    15
 
 By contrast, if the message ID is known it is recorded in the remotebug
 field.
@@ -103,8 +107,13 @@ an email address bug tracker Launchpad can never know a remote bug ID,
 calling getBugsWatching() on an email address bug tracker will always
 return an empty list.
 
-    >>> sorted(watch.remotebug for watch in email_tracker.latestwatches)
-    [u'', u'...launchpad@...']
+    >>> from operator import attrgetter
+
+    >>> for watch in sorted(
+    ...         email_tracker.latestwatches, key=attrgetter('remotebug')):
+    ...     print(watch.remotebug)
+    <BLANKLINE>
+    ...launchpad@...
 
     >>> email_tracker.getBugsWatching('')
     []
diff --git a/lib/lp/bugs/doc/externalbugtracker-mantis-csv.txt b/lib/lp/bugs/doc/externalbugtracker-mantis-csv.txt
index d61942f..0cd932f 100644
--- a/lib/lp/bugs/doc/externalbugtracker-mantis-csv.txt
+++ b/lib/lp/bugs/doc/externalbugtracker-mantis-csv.txt
@@ -103,8 +103,8 @@ Let's add a few more watches:
     ...     (int(bug_watch.remotebug), bug_watch.remotestatus)
     ...     for bug_watch in example_bug_tracker.watches)
 
-    >>> expected_remote_statuses
-    {3224: u'assigned: open'}
+    >>> print(pretty(expected_remote_statuses))
+    {3224: 'assigned: open'}
 
     >>> remote_bugs = [
     ...     (7346, dict(status='assigned', resolution='open')),
diff --git a/lib/lp/bugs/doc/externalbugtracker-mantis.txt b/lib/lp/bugs/doc/externalbugtracker-mantis.txt
index f0d5151..368cbb5 100644
--- a/lib/lp/bugs/doc/externalbugtracker-mantis.txt
+++ b/lib/lp/bugs/doc/externalbugtracker-mantis.txt
@@ -105,8 +105,9 @@ Let's add a few more watches:
     ...     (int(bug_watch.remotebug), bug_watch.remotestatus)
     ...     for bug_watch in example_bug_tracker.watches)
 
-    >>> expected_remote_statuses
-    {1550: u'assigned: open'}
+    >>> for remotebug, remotestatus in expected_remote_statuses.items():
+    ...     print('%d: %s' % (remotebug, remotestatus))
+    1550: assigned: open
 
     >>> remote_bugs = [
     ...     (1550, dict(status='assigned', resolution='open')),
diff --git a/lib/lp/bugs/doc/externalbugtracker-trac.txt b/lib/lp/bugs/doc/externalbugtracker-trac.txt
index 840d03e..2dc976a 100644
--- a/lib/lp/bugs/doc/externalbugtracker-trac.txt
+++ b/lib/lp/bugs/doc/externalbugtracker-trac.txt
@@ -389,10 +389,10 @@ watch hasn't altered:
     >>> now = datetime.now(pytz.timezone('UTC'))
     >>> bug_watch.lastchanged = now - timedelta(weeks=2)
     >>> old_last_changed = bug_watch.lastchanged
-    >>> bug_watch.remotebug
-    u'153'
-    >>> bug_watch.remotestatus
-    u'new'
+    >>> print(bug_watch.remotebug)
+    153
+    >>> print(bug_watch.remotestatus)
+    new
 
     >>> trac.batch_query_threshold = 0
     >>> with trac.responses():
@@ -401,7 +401,7 @@ watch hasn't altered:
 
     >>> bug_watch.lastchanged == old_last_changed
     True
-    >>> bug_watch.remotebug
-    u'153'
-    >>> bug_watch.remotestatus
-    u'new'
+    >>> print(bug_watch.remotebug)
+    153
+    >>> print(bug_watch.remotestatus)
+    new
diff --git a/lib/lp/bugs/doc/externalbugtracker.txt b/lib/lp/bugs/doc/externalbugtracker.txt
index 1a33a12..6882a5d 100644
--- a/lib/lp/bugs/doc/externalbugtracker.txt
+++ b/lib/lp/bugs/doc/externalbugtracker.txt
@@ -26,8 +26,8 @@ transaction, since it doesn't need DB access.
     ...     TestExternalBugTracker)
     >>> class InitializingExternalBugTracker(TestExternalBugTracker):
     ...     def initializeRemoteBugDB(self, remote_bug_ids):
-    ...         print("initializeRemoteBugDB() called: %r" % (
-    ...             remote_bug_ids, ))
+    ...         print("initializeRemoteBugDB() called: %s" % (
+    ...             pretty(remote_bug_ids), ))
 
     >>> from lp.services.log.logger import FakeLogger
     >>> from lp.bugs.scripts.checkwatches import CheckwatchesMaster
@@ -438,11 +438,11 @@ know that their time is similar to ours.
     ...         return datetime.now(pytz.timezone('UTC'))
     ...     def getModifiedRemoteBugs(self, remote_bug_ids, last_checked):
     ...         print("last_checked: %s" % last_checked)
-    ...         print("getModifiedRemoteBugs() called: %r" % (
-    ...             remote_bug_ids, ))
+    ...         print("getModifiedRemoteBugs() called: %s" % (
+    ...             pretty(remote_bug_ids), ))
     ...         return [remote_bug_ids[0], remote_bug_ids[-1]]
     ...     def getRemoteStatus(self, bug_id):
-    ...         print("getRemoteStatus() called: %r" % bug_id)
+    ...         print("getRemoteStatus() called: %s" % pretty(bug_id))
     ...         return 'UNKNOWN'
 
 Only bugs that have been checked before are passed on to
@@ -501,9 +501,9 @@ _getRemoteIdsToCheck(), which returns a dict containing three lists:
     ...     external_bugtracker, bug_watches,
     ...     external_bugtracker.getCurrentDBTime())
     >>> for key in sorted(ids):
-    ...     print("%s: %s" % (key, sorted(ids[key])))
-    all_remote_ids: [u'1', u'2', u'3', u'4']
-    remote_ids_to_check: [u'1', u'2', u'3', u'4']
+    ...     print("%s: %s" % (key, pretty(sorted(ids[key]))))
+    all_remote_ids: ['1', '2', '3', '4']
+    remote_ids_to_check: ['1', '2', '3', '4']
     unmodified_remote_ids: []
 
 updateBugWatches() calls _getRemoteIdsToCheck() and passes its results
@@ -511,11 +511,11 @@ to the ExternalBugTracker's initializeRemoteBugDB() method.
 
     >>> bug_watch_updater.updateBugWatches(
     ...     external_bugtracker, bug_watches)
-    initializeRemoteBugDB() called: [u'1', u'2', u'3', u'4']
-    getRemoteStatus() called: u'1'
-    getRemoteStatus() called: u'2'
-    getRemoteStatus() called: u'3'
-    getRemoteStatus() called: u'4'
+    initializeRemoteBugDB() called: ['1', '2', '3', '4']
+    getRemoteStatus() called: '1'
+    getRemoteStatus() called: '2'
+    getRemoteStatus() called: '3'
+    getRemoteStatus() called: '4'
 
 If the bug watches have the lastchecked attribute set, they will be
 passed to getModifiedRemoteBugs(). Only the bugs that have been modified
@@ -533,18 +533,18 @@ will then be passed on to initializeRemoteBugDB().
     last_checked: 2007-03-17 15:...:...
 
     >>> for key in sorted(ids):
-    ...     print("%s: %s" % (key, sorted(ids[key])))
-    all_remote_ids: [u'1', u'2', u'3', u'4']
-    remote_ids_to_check: [u'1', u'4']
-    unmodified_remote_ids: [u'2', u'3']
+    ...     print("%s: %s" % (key, pretty(sorted(ids[key]))))
+    all_remote_ids: ['1', '2', '3', '4']
+    remote_ids_to_check: ['1', '4']
+    unmodified_remote_ids: ['2', '3']
 
     >>> bug_watch_updater.updateBugWatches(
     ...     external_bugtracker, bug_watches)
     last_checked: 2007-03-17 15:...:...
-    getModifiedRemoteBugs() called: [u'1', u'2', u'3', u'4']
-    initializeRemoteBugDB() called: [u'1', u'4']
-    getRemoteStatus() called: u'1'
-    getRemoteStatus() called: u'4'
+    getModifiedRemoteBugs() called: ['1', '2', '3', '4']
+    initializeRemoteBugDB() called: ['1', '4']
+    getRemoteStatus() called: '1'
+    getRemoteStatus() called: '4'
 
 The bug watches that are deemed as not being modified are still marked
 as being checked.
@@ -586,12 +586,12 @@ initializeRemoteBugDB() since we do need to update them.
     >>> bug_watch_updater.updateBugWatches(
     ...     CheckModifiedExternalBugTracker(), bug_watches)
     last_checked: 2007-03-16 15:...:...
-    getModifiedRemoteBugs() called: [u'1', u'4']
-    initializeRemoteBugDB() called: [u'1', u'2', u'3', u'4']
-    getRemoteStatus() called: u'1'
-    getRemoteStatus() called: u'2'
-    getRemoteStatus() called: u'3'
-    getRemoteStatus() called: u'4'
+    getModifiedRemoteBugs() called: ['1', '4']
+    initializeRemoteBugDB() called: ['1', '2', '3', '4']
+    getRemoteStatus() called: '1'
+    getRemoteStatus() called: '2'
+    getRemoteStatus() called: '3'
+    getRemoteStatus() called: '4'
 
 As mentioned earlier, getModifiedRemoteBugs() is only called if we can
 get the current time of the remote system. If the time is unknown, we
@@ -604,11 +604,11 @@ always update all the bug watches.
     ...     bug_watch.lastchecked = some_time_ago
     >>> bug_watch_updater.updateBugWatches(
     ...     TimeUnknownExternalBugTracker(), bug_watches)
-    initializeRemoteBugDB() called: [u'1', u'2', u'3', u'4']
-    getRemoteStatus() called: u'1'
-    getRemoteStatus() called: u'2'
-    getRemoteStatus() called: u'3'
-    getRemoteStatus() called: u'4'
+    initializeRemoteBugDB() called: ['1', '2', '3', '4']
+    getRemoteStatus() called: '1'
+    getRemoteStatus() called: '2'
+    getRemoteStatus() called: '3'
+    getRemoteStatus() called: '4'
 
 The only exception to the rule of only updating modified bugs is the set
 of bug watches which have comments that need to be pushed to the remote
@@ -631,8 +631,8 @@ possible.
     >>> print(sorted(ids['remote_ids_to_check']))
     []
 
-    >>> print(sorted(ids['unmodified_remote_ids']))
-    [u'1', u'2', u'3', u'4']
+    >>> print(pretty(sorted(ids['unmodified_remote_ids'])))
+    ['1', '2', '3', '4']
 
     >>> comment_message = factory.makeMessage(
     ...     "A test message", "That hasn't been pushed",
@@ -644,8 +644,8 @@ possible.
     >>> ids = bug_watch_updater._getRemoteIdsToCheck(
     ...     external_bugtracker, bug_watches,
     ...     external_bugtracker.getCurrentDBTime(), utc_now)
-    >>> print(sorted(ids['remote_ids_to_check']))
-    [u'4']
+    >>> print(pretty(sorted(ids['remote_ids_to_check'])))
+    ['4']
 
 Once the comment has been pushed it will no longer appear in the list of
 IDs to be updated.
@@ -935,8 +935,8 @@ remote server.
     ...     external_bugtracker,
     ...     unchecked_watches + watches_with_comments,
     ...     external_bugtracker.getCurrentDBTime(), utc_now)
-    >>> print(sorted(ids['remote_ids_to_check']))
-    [u'0', u'1', u'5', u'6', u'7']
+    >>> print(pretty(sorted(ids['remote_ids_to_check'])))
+    ['0', '1', '5', '6', '7']
 
 Previously-checked bug watches that need updating will only be included if
 there is enough room for them in the batch. If the number of new watches plus
@@ -960,8 +960,8 @@ checked again.
     ...     external_bugtracker,
     ...     unchecked_watches + watches_with_comments + old_watches,
     ...     external_bugtracker.getCurrentDBTime(), utc_now)
-    >>> print(sorted(ids['remote_ids_to_check']))
-    [u'0', u'1', u'5', u'6', u'7']
+    >>> print(pretty(sorted(ids['remote_ids_to_check'])))
+    ['0', '1', '5', '6', '7']
 
 The old IDs that aren't checked aren't included in the unmodified_remote_ids
 list, since they still need checking and shouldn't be marked as having been
@@ -978,8 +978,8 @@ also be included, up to the batch_size limit.
     ...     external_bugtracker,
     ...     unchecked_watches + watches_with_comments + old_watches,
     ...     external_bugtracker.getCurrentDBTime(), utc_now)
-    >>> print(sorted(ids['remote_ids_to_check']))
-    [u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8']
+    >>> print(pretty(sorted(ids['remote_ids_to_check'])))
+    ['0', '1', '2', '3', '4', '5', '6', '7', '8']
 
 If there's no batch_size set, all the bugs that should be checked are
 returned.
@@ -989,8 +989,8 @@ returned.
     ...     external_bugtracker,
     ...     unchecked_watches + watches_with_comments + old_watches,
     ...     external_bugtracker.getCurrentDBTime(), utc_now)
-    >>> print(sorted(ids['remote_ids_to_check']))
-    [u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9']
+    >>> print(pretty(sorted(ids['remote_ids_to_check'])))
+    ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
 
 
 Setting the batch size
@@ -1010,8 +1010,8 @@ results will be returned.
     ...     unchecked_watches + watches_with_comments + old_watches,
     ...     external_bugtracker.getCurrentDBTime(), utc_now,
     ...     batch_size=2)
-    >>> print(sorted(ids['remote_ids_to_check']))
-    [u'5', u'6']
+    >>> print(pretty(sorted(ids['remote_ids_to_check'])))
+    ['5', '6']
 
 If the batch_size parameter is set to None (the default value), the
 ExternalBugTracker's batch_size is used to decide the number of IDs returned.
@@ -1021,8 +1021,8 @@ ExternalBugTracker's batch_size is used to decide the number of IDs returned.
     ...     unchecked_watches + watches_with_comments + old_watches,
     ...     external_bugtracker.getCurrentDBTime(), utc_now,
     ...     batch_size=None)
-    >>> print(sorted(ids['remote_ids_to_check']))
-    [u'0', u'1', u'5', u'6', u'7']
+    >>> print(pretty(sorted(ids['remote_ids_to_check'])))
+    ['0', '1', '5', '6', '7']
 
 _getRemoteIdsToCheck() will interpret a batch_size parameter of 0 as an
 instruction to ignore the batch size limitation altogether and just return all
@@ -1036,17 +1036,17 @@ be used in place of using 0 verbatim.
     ...     unchecked_watches + watches_with_comments + old_watches,
     ...     external_bugtracker.getCurrentDBTime(), utc_now,
     ...     batch_size=BATCH_SIZE_UNLIMITED)
-    >>> print(sorted(ids['remote_ids_to_check']))
-    [u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9']
+    >>> print(pretty(sorted(ids['remote_ids_to_check'])))
+    ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
 
 batch_size can be passed to _getRemoteIdsToCheck() via updateBugWatches(),
 too.
 
     >>> bug_watch_updater.updateBugWatches(
     ...     external_bugtracker, unchecked_watches, utc_now, batch_size=2)
-    initializeRemoteBugDB() called: [u'0', u'1']
-    getRemoteStatus() called: u'0'
-    getRemoteStatus() called: u'1'
+    initializeRemoteBugDB() called: ['0', '1']
+    getRemoteStatus() called: '0'
+    getRemoteStatus() called: '1'
 
 It can also be passed via updateBugTracker() (which will in turn pass it to
 updateBugWatches()).  In order to prevent it from attempting to connect to the
@@ -1064,9 +1064,9 @@ external_bugtracker.
     >>> transaction.commit()
     >>> bug_watch_updater._updateBugTracker(
     ...     standard_bugzilla, batch_size=2)
-    initializeRemoteBugDB() called: [u'5', u'6']
-    getRemoteStatus() called: u'5'
-    getRemoteStatus() called: u'6'
+    initializeRemoteBugDB() called: ['5', '6']
+    getRemoteStatus() called: '5'
+    getRemoteStatus() called: '6'
 
 The default entry point into CheckwatchesMaster for the checkwatches script is
 the updateBugTrackers() method. This, too, takes a batch_size parameter, which
diff --git a/lib/lp/bugs/doc/initial-bug-contacts.txt b/lib/lp/bugs/doc/initial-bug-contacts.txt
index 65f13fc..6e89ca6 100644
--- a/lib/lp/bugs/doc/initial-bug-contacts.txt
+++ b/lib/lp/bugs/doc/initial-bug-contacts.txt
@@ -42,8 +42,9 @@ Let's login then to add a subscription:
     >>> debian_firefox.addBugSubscription(sample_person, sample_person)
     <...StructuralSubscription object at ...>
 
-    >>> [pbc.subscriber.name for pbc in debian_firefox.bug_subscriptions]
-    [u'name12']
+    >>> for pbc in debian_firefox.bug_subscriptions:
+    ...     print(pbc.subscriber.name)
+    name12
 
 Trying to add a subscription to a package when that person or team is
 already subscribe to that package will return the existing subscription.
@@ -57,9 +58,14 @@ Let's add an ITeam as one of the subscribers:
     >>> debian_firefox.addBugSubscription(ubuntu_team, ubuntu_team)
     <...StructuralSubscription object at ...>
 
-    >>> sorted([
-    ...     sub.subscriber.name for sub in debian_firefox.bug_subscriptions])
-    [u'name12', u'ubuntu-team']
+    >>> from operator import attrgetter
+
+    >>> for sub in sorted(
+    ...         debian_firefox.bug_subscriptions,
+    ...         key=attrgetter('subscriber.name')):
+    ...     print(sub.subscriber.name)
+    name12
+    ubuntu-team
 
 To remove a subscription, use
 IStructuralSubscriptionTarget.removeBugSubscription:
@@ -146,9 +152,14 @@ subscriber to the new package is already subscribed to the bug:
 
 With the source package changed, we can see that daf is now subscribed:
 
-    >>> subscriber_names(bug_one_in_ubuntu_firefox.bug)
-    [u'Dafydd Harries', u'Foo Bar', u'Mark Shuttleworth',
-     u'Sample Person', u'Steve Alexander', u'Ubuntu Team']
+    >>> for name in subscriber_names(bug_one_in_ubuntu_firefox.bug):
+    ...     print(name)
+    Dafydd Harries
+    Foo Bar
+    Mark Shuttleworth
+    Sample Person
+    Steve Alexander
+    Ubuntu Team
 
 daf is sent an email giving him complete information about the bug that
 has just been retargeted, including the title, description, status,
@@ -264,10 +275,15 @@ contact address. Let's add such a team as a subscriber.
 The Ubuntu Gnome team was subscribed to the bug:
 
     >>> stub.test_emails = []
-    >>> subscriber_names(bug_one_in_ubuntu_firefox.bug)
-    [u'Dafydd Harries', u'Foo Bar', u'Mark Shuttleworth',
-     u'Sample Person', u'Steve Alexander', u'Ubuntu Gnome Team',
-     u'Ubuntu Team']
+    >>> for name in subscriber_names(bug_one_in_ubuntu_firefox.bug):
+    ...     print(name)
+    Dafydd Harries
+    Foo Bar
+    Mark Shuttleworth
+    Sample Person
+    Steve Alexander
+    Ubuntu Gnome Team
+    Ubuntu Team
 
 
 Product Bug Supervisors and Bug Tasks
@@ -290,10 +306,11 @@ Then we'll reassign bug #2 in Ubuntu to be in Firefox:
     >>> print(bug_two_in_ubuntu.product.name)
     tomcat
 
-    >>> sorted(
-    ...        [subscription.person.displayname for subscription in
-    ...         bug_two_in_ubuntu.bug.subscriptions])
-    [u'Steve Alexander']
+    >>> for subscription in sorted(
+    ...         bug_two_in_ubuntu.bug.subscriptions,
+    ...         key=attrgetter('person.displayname')):
+    ...     print(subscription.person.displayname)
+    Steve Alexander
 
     >>> with notify_modified(bug_two_in_ubuntu, ["id", "title", "product"]):
     ...     bug_two_in_ubuntu.transitionToTarget(mozilla_firefox, daf)
@@ -320,12 +337,13 @@ will only contain those teams of which the user is an administrator.
 Sample Person is a member of four teams:
 
     >>> sample_person = view.user
-    >>> ["%s: %s" % (membership.team.displayname, membership.status.name)
-    ...     for membership in sample_person.team_memberships]
-    [u'HWDB Team: APPROVED',
-     u'Landscape Developers: ADMIN',
-     u'Launchpad Users: ADMIN',
-     u'Warty Security Team: APPROVED']
+    >>> for membership in sample_person.team_memberships:
+    ...     print('%s: %s' % (
+    ...         membership.team.displayname, membership.status.name))
+    HWDB Team: APPROVED
+    Landscape Developers: ADMIN
+    Launchpad Users: ADMIN
+    Warty Security Team: APPROVED
 
 But is only an administrator of Landscape Developers, so that is the
 only team that will be listed when the user is changing a package bug
diff --git a/lib/lp/bugs/doc/malone-xmlrpc.txt b/lib/lp/bugs/doc/malone-xmlrpc.txt
index e609a61..4e6d68d 100644
--- a/lib/lp/bugs/doc/malone-xmlrpc.txt
+++ b/lib/lp/bugs/doc/malone-xmlrpc.txt
@@ -140,8 +140,10 @@ Reporting a package bug.
     True
     >>> bug.private
     True
-    >>> sorted(p.name for p in bug.getDirectSubscribers())
-    [u'name12', u'no-priv']
+    >>> for name in sorted(p.name for p in bug.getDirectSubscribers()):
+    ...     print(name)
+    name12
+    no-priv
     >>> bug.getIndirectSubscribers()
     []
 
diff --git a/lib/lp/bugs/doc/new-line-to-spaces-widget.txt b/lib/lp/bugs/doc/new-line-to-spaces-widget.txt
index d3cedaf..06676ba 100644
--- a/lib/lp/bugs/doc/new-line-to-spaces-widget.txt
+++ b/lib/lp/bugs/doc/new-line-to-spaces-widget.txt
@@ -16,8 +16,8 @@ We pass a string with some new line characters to the widget
 
 And check that the new lines were replaced by spaces.
 
-    >>> widget.getInputValue()
-    u'some text with new lines removed'
+    >>> print(widget.getInputValue())
+    some text with new lines removed
 
 Since the widget inherits from StrippedTextWidget, trailing whitespaces are
 also removed.
@@ -26,5 +26,5 @@ also removed.
     ...     form={'field.searchtext':
     ...     'text\rwith\nnew\r\nlines and trailing whitespace removed  '})
     >>> widget = NewLineToSpacesWidget(field, request)
-    >>> widget.getInputValue()
-    u'text with new lines and trailing whitespace removed'
+    >>> print(widget.getInputValue())
+    text with new lines and trailing whitespace removed
diff --git a/lib/lp/bugs/doc/official-bug-tags.txt b/lib/lp/bugs/doc/official-bug-tags.txt
index e0fa08e..5d939be 100644
--- a/lib/lp/bugs/doc/official-bug-tags.txt
+++ b/lib/lp/bugs/doc/official-bug-tags.txt
@@ -141,8 +141,9 @@ Ordinary users cannot add and remove official bug tags.
 
 Official tags are accessible as a list property of official tag targets.
 
-    >>> ubuntu.official_bug_tags
-    [u'bar']
+    >>> for tag in ubuntu.official_bug_tags:
+    ...     print(tag)
+    bar
 
 To set the list, the user must have edit permissions for the target.
 
@@ -151,20 +152,24 @@ To set the list, the user must have edit permissions for the target.
 Setting the list creates any new tags appearing in the list.
 
     >>> ubuntu.official_bug_tags = [u'foo', u'bar']
-    >>> ubuntu.official_bug_tags
-    [u'bar', u'foo']
+    >>> for tag in ubuntu.official_bug_tags:
+    ...     print(tag)
+    bar
+    foo
 
 Any existing tags missing from the list are removed.
 
     >>> ubuntu.official_bug_tags = [u'foo']
-    >>> ubuntu.official_bug_tags
-    [u'foo']
+    >>> for tag in ubuntu.official_bug_tags:
+    ...     print(tag)
+    foo
 
 The list is publicly readable.
 
     >>> login(ANONYMOUS)
-    >>> ubuntu.official_bug_tags
-    [u'foo']
+    >>> for tag in ubuntu.official_bug_tags:
+    ...     print(tag)
+    foo
 
 But only writable for users with edit permissions.
 
@@ -180,8 +185,10 @@ The same is available for products.
     >>> login('test@xxxxxxxxxxxxx')
     >>> firefox.official_bug_tags = [u'foo', u'bar']
     >>> login(ANONYMOUS)
-    >>> firefox.official_bug_tags
-    [u'bar', u'foo']
+    >>> for tag in firefox.official_bug_tags:
+    ...     print(tag)
+    bar
+    foo
 
 
 Official tags for additional bug targets
@@ -193,22 +200,27 @@ taken from the relevant distribution or product.
 Distribution series and distribution source package get the official tags of
 their parent distribution.
 
-    >>> ubuntu.getSeries('hoary').official_bug_tags
-    [u'foo']
+    >>> for tag in ubuntu.getSeries('hoary').official_bug_tags:
+    ...     print(tag)
+    foo
 
     >>> login('test@xxxxxxxxxxxxx')
-    >>> ubuntu.getSeries(
-    ...     'hoary').getSourcePackage('alsa-utils').official_bug_tags
-    [u'foo']
+    >>> for tag in ubuntu.getSeries(
+    ...         'hoary').getSourcePackage('alsa-utils').official_bug_tags:
+    ...     print(tag)
+    foo
     >>> login(ANONYMOUS)
 
-    >>> ubuntu.getSourcePackage('alsa-utils').official_bug_tags
-    [u'foo']
+    >>> for tag in ubuntu.getSourcePackage('alsa-utils').official_bug_tags:
+    ...     print(tag)
+    foo
 
 Product series gets the tags of the parent product.
 
-    >>> firefox.getSeries('1.0').official_bug_tags
-    [u'bar', u'foo']
+    >>> for tag in firefox.getSeries('1.0').official_bug_tags:
+    ...     print(tag)
+    bar
+    foo
 
 Project group gets the union of all the tags available for its products.
 
@@ -218,12 +230,16 @@ Project group gets the union of all the tags available for its products.
     >>> thunderbird.official_bug_tags = [u'baz']
     >>> login('no-priv@xxxxxxxxxxxxx')
     >>> mozilla = getUtility(IProjectGroupSet).getByName('mozilla')
-    >>> list(mozilla.official_bug_tags)
-    [u'bar', u'baz', u'foo']
+    >>> for tag in mozilla.official_bug_tags:
+    ...     print(tag)
+    bar
+    baz
+    foo
     >>> login(ANONYMOUS)
 
 Milestone gets the tags of the relevant product.
 
-    >>> firefox.getMilestone('1.0').official_bug_tags
-    [u'bar', u'foo']
-
+    >>> for tag in firefox.getMilestone('1.0').official_bug_tags:
+    ...     print(tag)
+    bar
+    foo
diff --git a/lib/lp/bugs/doc/product-update-remote-product.txt b/lib/lp/bugs/doc/product-update-remote-product.txt
index 4491245..7372652 100644
--- a/lib/lp/bugs/doc/product-update-remote-product.txt
+++ b/lib/lp/bugs/doc/product-update-remote-product.txt
@@ -33,8 +33,8 @@ creates the ExternalBugTracker for the given BugTracker.
     >>> rt_external = updater._getExternalBugTracker(rt)
     >>> rt_external.__class__.__name__
     'RequestTracker'
-    >>> rt_external.baseurl
-    u'http://rt.example.com'
+    >>> print(rt_external.baseurl)
+    http://rt.example.com
 
 For testing, _getExternalBugTracker() can be overridden to return an
 ExternalBugTracker that doesn't require network access.
@@ -42,7 +42,8 @@ ExternalBugTracker that doesn't require network access.
     >>> class FakeExternalBugTracker:
     ...
     ...     def initializeRemoteBugDB(self, bug_ids):
-    ...         print("Initializing DB for bugs: %s." % bug_ids)
+    ...         print("Initializing DB for bugs: [%s]." %
+    ...             ", ".join("'%s'" % bug_id for bug_id in bug_ids))
     ...
     ...     def getRemoteProduct(self, remote_bug):
     ...         return 'product-for-bug-%s' % remote_bug
@@ -145,7 +146,7 @@ given one are ignored.
     ...     rt_bugtask.bugwatch = rt_bugwatch
 
     >>> updater.updateByBugTrackerType(BugTrackerType.RT)
-    Initializing DB for bugs: [u'84'].
+    Initializing DB for bugs: ['84'].
 
     >>> print(rt_product.remote_product)
     product-for-bug-84
@@ -198,11 +199,11 @@ after each product's remote_product has been updated.
     ...     FakeTransaction(log_calls=True), BufferLogger())
     >>> updater.print_method_calls = False
     >>> updater.updateByBugTrackerType(BugTrackerType.RT)
-    Initializing DB for bugs: [u'84'].
+    Initializing DB for bugs: ['84'].
     COMMIT
-    Initializing DB for bugs: [u'84'].
+    Initializing DB for bugs: ['84'].
     COMMIT
-    Initializing DB for bugs: [u'84'].
+    Initializing DB for bugs: ['84'].
     COMMIT
 
 
@@ -248,7 +249,7 @@ tracker don't break the run for all bug trackers.
     >>> updater.updateByBugTrackerType(BugTrackerType.RT)
     INFO  1 projects using RT needing updating.
     DEBUG Trying to update fooix
-    Initializing DB for bugs: [u'42'].
+    Initializing DB for bugs: ['42'].
     ERROR Unable to set remote_product for 'fooix': Didn't find bug 42.
 
 AssertionErrors are also handled.
diff --git a/lib/lp/bugs/doc/products-with-no-remote-product.txt b/lib/lp/bugs/doc/products-with-no-remote-product.txt
index a339187..6646de1 100644
--- a/lib/lp/bugs/doc/products-with-no-remote-product.txt
+++ b/lib/lp/bugs/doc/products-with-no-remote-product.txt
@@ -24,8 +24,9 @@ IProductSet.getProductsWithNoneRemoteProduct() is used for this.
     None
 
     >>> products = getUtility(IProductSet).getProductsWithNoneRemoteProduct()
-    >>> [product.name for product in products]
-    [u'no-remote-product']
+    >>> for product in products:
+    ...     print(product.name)
+    no-remote-product
 
 When we update remote_product automatically, different heuristics are
 used depending on which bug tracker is used. Therefore the list of
@@ -42,5 +43,6 @@ products can be filtered by bug tracker type.
     >>> trac_product.bugtracker = trac
     >>> products = getUtility(IProductSet).getProductsWithNoneRemoteProduct(
     ...     bugtracker_type=BugTrackerType.BUGZILLA)
-    >>> [product.name for product in products]
-    [u'bugzilla-product']
+    >>> for product in products:
+    ...     print(product.name)
+    bugzilla-product
diff --git a/lib/lp/bugs/doc/vocabularies.txt b/lib/lp/bugs/doc/vocabularies.txt
index 17a7ea1..bfd449b 100644
--- a/lib/lp/bugs/doc/vocabularies.txt
+++ b/lib/lp/bugs/doc/vocabularies.txt
@@ -147,8 +147,8 @@ This means that if we iterate through the vocabulary with bug one, only
 the trunk will be nominatable:
 
     >>> firefox_bug_one = bug_one.bugtasks[0]
-    >>> firefox_bug_one.target.name
-    u'firefox'
+    >>> print(firefox_bug_one.target.name)
+    firefox
     >>> series_vocabulary = vocabulary_registry.get(
     ...     firefox_bug_one, 'BugNominatableSeries')
     >>> for term in series_vocabulary:
@@ -168,8 +168,8 @@ No series is targeted or nominated on bug 4:
 So if we give bug four to the vocabulary, all series will be returned:
 
     >>> firefox_bug_four = bug_four.bugtasks[0]
-    >>> firefox_bug_four.target.name
-    u'firefox'
+    >>> print(firefox_bug_four.target.name)
+    firefox
     >>> series_vocabulary = vocabulary_registry.get(
     ...     firefox_bug_four, 'BugNominatableSeries')
     >>> for term in series_vocabulary:
@@ -198,8 +198,8 @@ Bug one is nominated for Ubuntu Hoary:
 So Hoary isn't included in the vocabulary:
 
     >>> ubuntu_bug_one = bug_one.bugtasks[1]
-    >>> ubuntu_bug_one.distribution.name
-    u'ubuntu'
+    >>> print(ubuntu_bug_one.distribution.name)
+    ubuntu
     >>> series_vocabulary = vocabulary_registry.get(
     ...     ubuntu_bug_one, 'BugNominatableSeries')
     >>> for term in series_vocabulary:
@@ -224,8 +224,8 @@ The same is true for bug two, where the bug is targeted to Hoary.
     hoary
 
     >>> ubuntu_bug_two = bug_two.bugtasks[1]
-    >>> ubuntu_bug_two.distribution.name
-    u'ubuntu'
+    >>> print(ubuntu_bug_two.distribution.name)
+    ubuntu
     >>> series_vocabulary = vocabulary_registry.get(
     ...     ubuntu_bug_two, 'BugNominatableSeries')
     >>> for term in series_vocabulary:
@@ -302,8 +302,8 @@ treated differently.
     >>> email_bugtracker = getUtility(IBugTrackerSet).getByName('email')
     >>> email_bugwatch = getUtility(IBugWatchSet).createBugWatch(
     ...     bug_twelve, launchbag.user, email_bugtracker, '')
-    >>> email_bugwatch.url
-    u'mailto:bugs@xxxxxxxxxxx'
+    >>> print(email_bugwatch.url)
+    mailto:bugs@xxxxxxxxxxx
 
 The title is rendered differently compared to other bug watches.
 
diff --git a/lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt b/lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt
index cdc40ea..a0b911d 100644
--- a/lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt
+++ b/lib/lp/bugs/stories/bug-privacy/xx-bug-privacy.txt
@@ -58,6 +58,7 @@ Foo Bar sees the private bug they filed.
 
 Foo Bar is subscribed to the bug.
 
+    >>> from operator import attrgetter
     >>> from zope.component import getUtility
     >>> from lp.testing import login, logout
     >>> from lp.bugs.interfaces.bug import IBugSet
@@ -66,8 +67,10 @@ Foo Bar is subscribed to the bug.
 
     >>> bug = getUtility(IBugSet).get(bug_id)
 
-    >>> sorted(subscriber.name for subscriber in bug.getDirectSubscribers())
-    [u'name16']
+    >>> for subscriber in sorted(
+    ...         bug.getDirectSubscribers(), key=attrgetter('name')):
+    ...     print(subscriber.name)
+    name16
 
     >>> logout()
 
diff --git a/lib/lp/bugs/stories/bugs/bug-add-subscriber.txt b/lib/lp/bugs/stories/bugs/bug-add-subscriber.txt
index ab35f8e..31783eb 100644
--- a/lib/lp/bugs/stories/bugs/bug-add-subscriber.txt
+++ b/lib/lp/bugs/stories/bugs/bug-add-subscriber.txt
@@ -58,8 +58,8 @@ They are notified that David Allouche has been subscribed.
 
     >>> for tag in find_tags_by_class(user_browser.contents,
     ...     'informational message'):
-    ...     extract_text(tag)
-    u'David Allouche has been subscribed to this bug.'
+    ...     print(extract_text(tag))
+    David Allouche has been subscribed to this bug.
 
 The subscribers portlet now contains the new subscriber's name.
 
@@ -102,8 +102,8 @@ He is notified that Landscape developers team has been subscribed.
 
     >>> for tag in find_tags_by_class(user_browser.contents,
     ...     'informational message'):
-    ...     extract_text(tag)
-    u'Landscape Developers team has been subscribed to this bug.'
+    ...     print(extract_text(tag))
+    Landscape Developers team has been subscribed to this bug.
 
 The subscribers portlet displays the new subscribed team.
 
diff --git a/lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt b/lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt
index 5a53e48..a8b0cb3 100644
--- a/lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt
+++ b/lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt
@@ -150,8 +150,8 @@ are replaced by a single space.
 
     >>> attachments_text = text_bug[text_bug.find('attachments:'):]
     >>> attachment_2 = attachments_text.split('\n')[2]
-    >>> attachment_2
-    u' http://bugs.launchpad.test/.../file%20with%20space.txt text/plain;
+    >>> six.ensure_str(attachment_2)
+    ' http://bugs.launchpad.test/.../file%20with%20space.txt text/plain;
     name="file with space.txt"'
 
 
diff --git a/lib/lp/bugs/stories/bugs/xx-unique-ids-on-bug-page.txt b/lib/lp/bugs/stories/bugs/xx-unique-ids-on-bug-page.txt
index 305362c..960dca1 100644
--- a/lib/lp/bugs/stories/bugs/xx-unique-ids-on-bug-page.txt
+++ b/lib/lp/bugs/stories/bugs/xx-unique-ids-on-bug-page.txt
@@ -29,6 +29,8 @@ Still, the ids of the fields are unique.
 (The "Email me..." option has the same id everywhere since the
 user is not subscribing to the bugtask, but to the bug.)
 
-    >>> non_unique_ids
-    [u'subscribe', u'subscribe']
+    >>> for item in non_unique_ids:
+    ...     print(item)
+    subscribe
+    subscribe
 
diff --git a/lib/lp/bugs/stories/bugtask-management/xx-edit-email-address-bugtask.txt b/lib/lp/bugs/stories/bugtask-management/xx-edit-email-address-bugtask.txt
index 58653d8..f42e154 100644
--- a/lib/lp/bugs/stories/bugtask-management/xx-edit-email-address-bugtask.txt
+++ b/lib/lp/bugs/stories/bugtask-management/xx-edit-email-address-bugtask.txt
@@ -63,8 +63,8 @@ The product owner cannot see the Status or Importance widgets:
     >>> gnome_terminal = getUtility(IProductSet).getByName("gnome-terminal")
 
     >>> login(ANONYMOUS)
-    >>> gnome_terminal.owner.name
-    u'name12'
+    >>> print(gnome_terminal.owner.name)
+    name12
     >>> print_widget_visibility(
     ...     user=gnome_terminal.owner,
     ...     url=('http://bugs.launchpad.test/'
@@ -122,8 +122,8 @@ The owner can see the Status and Importance widgets.
     >>> alsa_utils = getUtility(IProductSet).getByName("alsa-utils")
 
     >>> login(ANONYMOUS)
-    >>> alsa_utils.owner.name
-    u'mark'
+    >>> print(alsa_utils.owner.name)
+    mark
 
     >>> print_widget_visibility(
     ...     user=alsa_utils.owner,
diff --git a/lib/lp/bugs/stories/feeds/xx-bug-atom.txt b/lib/lp/bugs/stories/feeds/xx-bug-atom.txt
index fff5ba9..b36d6c8 100644
--- a/lib/lp/bugs/stories/feeds/xx-bug-atom.txt
+++ b/lib/lp/bugs/stories/feeds/xx-bug-atom.txt
@@ -27,8 +27,9 @@ point to the bugs themselves.
 
     >>> browser.open('http://feeds.launchpad.test/jokosher/latest-bugs.atom')
     >>> _ = feedparser.parse(browser.contents)
-    >>> BeautifulSoup(browser.contents, 'xml').title.contents
-    [u'Bugs in Jokosher']
+    >>> for element in BeautifulSoup(browser.contents, 'xml').title.contents:
+    ...     print(element)
+    Bugs in Jokosher
     >>> browser.url
     'http://feeds.launchpad.test/jokosher/latest-bugs.atom'
 
@@ -83,8 +84,9 @@ as the latest bugs feed for a product.
 
     >>> browser.open('http://feeds.launchpad.test/mozilla/latest-bugs.atom')
     >>> _ = feedparser.parse(browser.contents)
-    >>> BeautifulSoup(browser.contents, 'xml').title.contents
-    [u'Bugs in The Mozilla Project']
+    >>> for element in BeautifulSoup(browser.contents, 'xml').title.contents:
+    ...     print(element)
+    Bugs in The Mozilla Project
     >>> browser.url
     'http://feeds.launchpad.test/mozilla/latest-bugs.atom'
 
@@ -143,8 +145,9 @@ of content as the latest bugs feed for a product.
 
     >>> browser.open('http://feeds.launchpad.test/ubuntu/latest-bugs.atom')
     >>> _ = feedparser.parse(browser.contents)
-    >>> BeautifulSoup(browser.contents, 'xml').title.contents
-    [u'Bugs in Ubuntu']
+    >>> for element in BeautifulSoup(browser.contents, 'xml').title.contents:
+    ...     print(element)
+    Bugs in Ubuntu
     >>> browser.url
     'http://feeds.launchpad.test/ubuntu/latest-bugs.atom'
 
@@ -213,8 +216,11 @@ The bug should be included in the feed.
 Private teams should show as '-'.
 
     >>> soup = BeautifulSoup(entry.find('content').text, 'xml')
-    >>> print([tr.find_all('td')[4].text for tr in soup.find_all('tr')[1:4]])
-    [u'Mark Shuttleworth', u'-', u'-']
+    >>> for tr in soup.find_all('tr')[1:4]:
+    ...     print(tr.find_all('td')[4].text)
+    Mark Shuttleworth
+    -
+    -
 
 == Latest bugs for a source package ==
 
@@ -225,8 +231,9 @@ type of content as the latest bugs feed for a product.
     ...     'http://feeds.launchpad.test/ubuntu/+source/thunderbird'
     ...     '/latest-bugs.atom')
     >>> _ = feedparser.parse(browser.contents)
-    >>> BeautifulSoup(browser.contents, 'xml').title.contents
-    [u'Bugs in thunderbird in Ubuntu']
+    >>> for element in BeautifulSoup(browser.contents, 'xml').title.contents:
+    ...     print(element)
+    Bugs in thunderbird in Ubuntu
     >>> browser.url
     'http://feeds.launchpad.test/ubuntu/+source/thunderbird/latest-bugs.atom'
     >>> soup = BeautifulSoup(
@@ -256,8 +263,9 @@ type of content as the latest bugs feed for a product.
     >>> browser.open(
     ...     'http://feeds.launchpad.test/ubuntu/hoary/latest-bugs.atom')
     >>> _ = feedparser.parse(browser.contents)
-    >>> BeautifulSoup(browser.contents, 'xml').title.contents
-    [u'Bugs in Hoary']
+    >>> for element in BeautifulSoup(browser.contents, 'xml').title.contents:
+    ...     print(element)
+    Bugs in Hoary
     >>> browser.url
     'http://feeds.launchpad.test/ubuntu/hoary/latest-bugs.atom'
 
@@ -295,8 +303,9 @@ type of content as the latest bugs feed for a product.
     >>> browser.open(
     ...     'http://feeds.launchpad.test/firefox/1.0/latest-bugs.atom')
     >>> _ = feedparser.parse(browser.contents)
-    >>> BeautifulSoup(browser.contents, 'xml').title.contents
-    [u'Bugs in 1.0']
+    >>> for element in BeautifulSoup(browser.contents, 'xml').title.contents:
+    ...     print(element)
+    Bugs in 1.0
     >>> browser.url
     'http://feeds.launchpad.test/firefox/1.0/latest-bugs.atom'
 
@@ -332,8 +341,9 @@ This feed gets the latest bugs for a person.
 
     >>> browser.open('http://feeds.launchpad.test/~name16/latest-bugs.atom')
     >>> _ = feedparser.parse(browser.contents)
-    >>> BeautifulSoup(browser.contents, 'xml').title.contents
-    [u'Bugs for Foo Bar']
+    >>> for element in BeautifulSoup(browser.contents, 'xml').title.contents:
+    ...     print(element)
+    Bugs for Foo Bar
     >>> browser.url
     'http://feeds.launchpad.test/~name16/latest-bugs.atom'
 
@@ -406,8 +416,9 @@ some results.
     >>> browser.open(
     ...    'http://feeds.launchpad.test/~simple-team/latest-bugs.atom')
     >>> _ = feedparser.parse(browser.contents)
-    >>> BeautifulSoup(browser.contents, 'xml').title.contents
-    [u'Bugs for Simple Team']
+    >>> for element in BeautifulSoup(browser.contents, 'xml').title.contents:
+    ...     print(element)
+    Bugs for Simple Team
 
     >>> soup = BeautifulSoup(
     ...     browser.contents, 'xml', parse_only=SoupStrainer('id'))
@@ -433,8 +444,9 @@ This feed gets the latest bugs reported against any target.
 
     >>> browser.open('http://feeds.launchpad.test/bugs/latest-bugs.atom')
     >>> _ = feedparser.parse(browser.contents)
-    >>> BeautifulSoup(browser.contents, 'xml').title.contents
-    [u'Launchpad bugs']
+    >>> for element in BeautifulSoup(browser.contents, 'xml').title.contents:
+    ...     print(element)
+    Launchpad bugs
     >>> browser.url
     'http://feeds.launchpad.test/bugs/latest-bugs.atom'
 
@@ -498,8 +510,9 @@ The bug search feed can be tested after setting is_bug_search_feed_active
 to True.
 
     >>> browser.open(url)
-    >>> BeautifulSoup(browser.contents, 'xml').title.contents
-    [u'Bugs from custom search']
+    >>> for element in BeautifulSoup(browser.contents, 'xml').title.contents:
+    ...     print(element)
+    Bugs from custom search
 
     >>> soup = BeautifulSoup(
     ...     browser.contents, 'xml', parse_only=SoupStrainer('id'))
@@ -543,8 +556,9 @@ This feed shows the status of a single bug.
 
     >>> browser.open('http://feeds.launchpad.test/bugs/1/bug.atom')
     >>> _ = feedparser.parse(browser.contents)
-    >>> BeautifulSoup(browser.contents, 'xml').title.contents
-    [u'Bug 1']
+    >>> for element in BeautifulSoup(browser.contents, 'xml').title.contents:
+    ...     print(element)
+    Bug 1
     >>> entries = parse_entries(browser.contents)
     >>> print(len(entries))
     1
diff --git a/lib/lp/bugs/stories/standalone/xx-show-distribution-cve-report.txt b/lib/lp/bugs/stories/standalone/xx-show-distribution-cve-report.txt
index 1020f76..2b62bcd 100644
--- a/lib/lp/bugs/stories/standalone/xx-show-distribution-cve-report.txt
+++ b/lib/lp/bugs/stories/standalone/xx-show-distribution-cve-report.txt
@@ -8,9 +8,10 @@ distro bugs linked to CVEs.
 
 The report lists the open and closed bugs.
 
-    >>> [extract_text(h2) for h2 in main.findAll('h2')]
-    [u'Open bugs',
-     u'Resolved bugs']
+    >>> for h2 in main.findAll('h2'):
+    ...     print(extract_text(h2))
+    Open bugs
+    Resolved bugs
 
 For distros having series, no report is shown for the complete
 distro, since it's probably too big.
@@ -26,11 +27,12 @@ Instead, the links for the specific series reports are shown.
     CVE reports in releases of Ubuntu
 
     >>> series_list = main.find('div', 'top-portlet').ul
-    >>> [extract_text(li) for li in series_list.findAll('li')]
-    [u'Breezy Badger Autotest',
-     u'Grumpy',
-     u'Hoary',
-     u'Warty']
+    >>> for li in series_list.findAll('li'):
+    ...     print(extract_text(li))
+    Breezy Badger Autotest
+    Grumpy
+    Hoary
+    Warty
 
     >>> browser.getLink('Breezy Badger Autotest').click()
     >>> browser.url
diff --git a/lib/lp/bugs/stories/upstream-bugprivacy/xx-upstream-bug-privacy.txt b/lib/lp/bugs/stories/upstream-bugprivacy/xx-upstream-bug-privacy.txt
index bdd8788..99395be 100644
--- a/lib/lp/bugs/stories/upstream-bugprivacy/xx-upstream-bug-privacy.txt
+++ b/lib/lp/bugs/stories/upstream-bugprivacy/xx-upstream-bug-privacy.txt
@@ -21,6 +21,8 @@ contact, the maintainer will be subscribed to the bug instead.
 
 Now the reporter is subscribed.
 
+    >>> from operator import attrgetter
+
     >>> from zope.component import getUtility
 
     >>> from lp.testing import login, logout
@@ -30,8 +32,10 @@ Now the reporter is subscribed.
 
     >>> bug = getUtility(IBugSet).get(bug_id)
 
-    >>> sorted(subscriber.name for subscriber in bug.getDirectSubscribers())
-    [u'name16']
+    >>> for subscriber in sorted(
+    ...         bug.getDirectSubscribers(), key=attrgetter('name')):
+    ...     print(subscriber.name)
+    name16
 
     >>> logout()
 
diff --git a/lib/lp/bugs/stories/webservice/xx-bug-target.txt b/lib/lp/bugs/stories/webservice/xx-bug-target.txt
index b49afd2..f4e4c45 100644
--- a/lib/lp/bugs/stories/webservice/xx-bug-target.txt
+++ b/lib/lp/bugs/stories/webservice/xx-bug-target.txt
@@ -125,8 +125,10 @@ We can also access official tags as a list.
     HTTP/1.1 209 Content Returned...
 
     >>> tags_test_product = webservice.get('/tags-test-product').jsonBody()
-    >>> tags_test_product['official_bug_tags']
-    [u'bar', u'foo']
+    >>> for tag in tags_test_product['official_bug_tags']:
+    ...     print(tag)
+    bar
+    foo
 
     >>> login('foo.bar@xxxxxxxxxxxxx')
     >>> distribution = factory.makeDistribution(name='testix')
diff --git a/lib/lp/bugs/stories/webservice/xx-bug.txt b/lib/lp/bugs/stories/webservice/xx-bug.txt
index c6611bd..1828357 100644
--- a/lib/lp/bugs/stories/webservice/xx-bug.txt
+++ b/lib/lp/bugs/stories/webservice/xx-bug.txt
@@ -65,8 +65,8 @@ Bugs have relationships to other bugs, like "duplicate_of".
 
     >>> bug_six_url = duplicates_of_five[0]['self_link']
     >>> bug_six = webservice.get(bug_six_url).jsonBody()
-    >>> bug_six['duplicate_of_link']
-    u'http://.../bugs/5'
+    >>> print(bug_six['duplicate_of_link'])
+    http://.../bugs/5
 
 To create a new bug we use the createBug operation. This operation
 takes a target parameter which must be a either a Product, a
@@ -246,8 +246,8 @@ URLs are based on their position with respect to the
 bug. /firefox/+bug/5/comments/0 is the first message for bug 5, and it's
 different from /firefox/+bug/1/comments/0.
 
-    >>> messages[0]['self_link']
-    u'http://.../firefox/+bug/5/comments/0'
+    >>> print(messages[0]['self_link'])
+    http://.../firefox/+bug/5/comments/0
 
     >>> message = webservice.get(messages[0]['self_link']).jsonBody()
     >>> message == messages[0]
@@ -367,16 +367,16 @@ It's possible to change the task's assignee.
 The task's importance can be modified directly.
 
     >>> body = webservice.get(bugtask_path).jsonBody()
-    >>> body['importance']
-    u'Low'
+    >>> print(body['importance'])
+    Low
 
     >>> patch = {u'importance': u'High'}
     >>> print(webservice.patch(bugtask_path, 'application/json', dumps(patch)))
     HTTP/1.1 209 Content Returned...
 
     >>> body = webservice.get(bugtask_path).jsonBody()
-    >>> body['importance']
-    u'High'
+    >>> print(body['importance'])
+    High
 
 Only bug supervisors or people who can otherwise edit the bugtask's
 pillar are authorised to edit the importance.
@@ -386,8 +386,8 @@ pillar are authorised to edit the importance.
     HTTP/1.1 401 Unauthorized...
 
     >>> body = webservice.get(bugtask_path).jsonBody()
-    >>> body['importance']
-    u'High'
+    >>> print(body['importance'])
+    High
 
 The task's status can also be modified directly.
 
@@ -1094,16 +1094,16 @@ The collection of bug watches is not exposed as a resource:
 
 We can modify the remote bug.
 
-    >>> bug_watch['remote_bug']
-    u'2000'
+    >>> print(bug_watch['remote_bug'])
+    2000
 
     >>> patch = {u'remote_bug': u'1234'}
     >>> response = webservice.patch(
     ...     bug_watch_2000['self_link'], 'application/json', dumps(patch))
 
     >>> bug_watch = webservice.get(bug_watch_2000['self_link']).jsonBody()
-    >>> bug_watch['remote_bug']
-    u'1234'
+    >>> print(bug_watch['remote_bug'])
+    1234
 
 But we can't change other things, like the URL.
 
diff --git a/lib/lp/bugs/tests/trac-xmlrpc-transport.txt b/lib/lp/bugs/tests/trac-xmlrpc-transport.txt
index ce339b9..66de8e7 100644
--- a/lib/lp/bugs/tests/trac-xmlrpc-transport.txt
+++ b/lib/lp/bugs/tests/trac-xmlrpc-transport.txt
@@ -88,8 +88,8 @@ Specifying a level of 0 and no criteria will return the IDs of all the
 bugs, along with a time snapshot as returned by time_snapshot().
 
     >>> time_snapshot, bugs = trac_transport.bug_info(level=0)
-    >>> print(bugs)
-    [{'id': u'1'}, {'id': u'2'}, {'id': u'3'}]
+    >>> print(pretty(bugs))
+    [{'id': '1'}, {'id': '2'}, {'id': '3'}]
 
 Specifying a level of 1 will return each bug's metadata, not including
 its last modified time.
@@ -125,9 +125,9 @@ We'll add some sample comments to demonstrate this.
 
     >>> time_snapshot, bugs = trac_transport.bug_info(level=2)
     >>> for bug in bugs:
-    ...     print("%s: %s" % (bug['id'], bug['comments']))
-    1: [u'1-1']
-    2: [u'2-1', u'2-2']
+    ...     print("%s: %s" % (bug['id'], pretty(bug['comments'])))
+    1: ['1-1']
+    2: ['2-1', '2-2']
     3: []
 
 We'll also define a helper function to print comments out.
@@ -178,8 +178,8 @@ so we'll convert a datetime into one for the purposes of this test.
     >>> time_snapshot, bugs = trac_transport.bug_info(
     ...     level=0, criteria=criteria)
 
-    >>> print(bugs)
-    [{'id': u'1'}, {'id': u'3'}]
+    >>> print(pretty(bugs))
+    [{'id': '1'}, {'id': '3'}]
 
 The bugs key in the criteria dict allows us to specify a list of bug IDs
 to return.
@@ -188,8 +188,8 @@ to return.
     >>> time_snapshot, bugs = trac_transport.bug_info(
     ...     level=0, criteria=criteria)
 
-    >>> print(bugs)
-    [{'id': u'1'}, {'id': u'2'}]
+    >>> print(pretty(bugs))
+    [{'id': '1'}, {'id': '2'}]
 
 If a bug doesn't exist, it will be returned with a status of
 'missing'.
@@ -198,8 +198,8 @@ If a bug doesn't exist, it will be returned with a status of
     >>> time_snapshot, bugs = trac_transport.bug_info(
     ...     level=0, criteria=criteria)
 
-    >>> print(bugs)
-    [{'status': 'missing', 'id': u'11'}, {'status': 'missing', 'id': u'12'}]
+    >>> print(pretty(bugs))
+    [{'id': '11', 'status': 'missing'}, {'id': '12', 'status': 'missing'}]
 
 Combining the bugs and modified_since fields in the criteria dict will
 result in only the bugs modified since the modified_since time whose IDs
@@ -211,8 +211,8 @@ are in the bugs list being returned.
     >>> time_snapshot, bugs = trac_transport.bug_info(
     ...     level=0, criteria=criteria)
 
-    >>> print(bugs)
-    [{'id': u'1'}]
+    >>> print(pretty(bugs))
+    [{'id': '1'}]
 
 
 == launchpad.get_comments() ==