← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:registry-pagetests-future-imports into launchpad:master

 

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

Commit message:
Convert lp.registry pagetests to preferred __future__ imports

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

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

This is very large, but with the exception of the change to lib/lp/registry/tests/test_doc.py it consists entirely of mechanical print function conversions.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:registry-pagetests-future-imports into launchpad:master.
diff --git a/lib/lp/registry/stories/announcements/xx-announcements.txt b/lib/lp/registry/stories/announcements/xx-announcements.txt
index e0ffc0b..9a12076 100644
--- a/lib/lp/registry/stories/announcements/xx-announcements.txt
+++ b/lib/lp/registry/stories/announcements/xx-announcements.txt
@@ -83,22 +83,22 @@ pillar.
     >>> priv_browser = setupBrowser(auth="Basic mark@xxxxxxxxxxx:test")
     >>> priv_browser.open('http://launchpad.test/ubuntu')
     >>> link = priv_browser.getLink('Make announcement')
-    >>> print link.text
+    >>> print(link.text)
     Make announcement
 
     >>> priv_browser.getLink('Read all announcements').click()
     >>> link = priv_browser.getLink('Make announcement')
-    >>> print link.text
+    >>> print(link.text)
     Make announcement
 
     >>> priv_browser.open('http://launchpad.test/firefox')
     >>> link = priv_browser.getLink('Make announcement')
-    >>> print link.text
+    >>> print(link.text)
     Make announcement
 
     >>> priv_browser.getLink('Read all announcements').click()
     >>> link = priv_browser.getLink('Make announcement')
-    >>> print link.text
+    >>> print(link.text)
     Make announcement
 
 
@@ -118,9 +118,9 @@ announcement:
 Making the announcement takes the user back to the main page for the
 project.
 
-    >>> print priv_browser.url
+    >>> print(priv_browser.url)
     http://launchpad.test/apache
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     Apache in Launchpad
 
 We'll repeat the process for Tomcat, an IProduct that is part of the
@@ -134,8 +134,8 @@ announcement so there is a "Latest news" portlet. Let's render the
 portlet, taking care not to render today's date which would timebomb our
 script.
 
-    >>> print latest_news(priv_browser.contents).encode(
-    ...     'ascii', 'backslashreplace')
+    >>> print(latest_news(priv_browser.contents).encode(
+    ...     'ascii', 'backslashreplace'))
     Announcements
     Apache announcement headline...
     Read all announcements
@@ -154,13 +154,13 @@ work too:
     ...     name='field.publication_date.announcement_date').value = (
     ...          '2007-11-24 09:00:00')
     >>> priv_browser.getControl('Make announcement').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     Tomcat in Launchpad
 
 And check out the results:
 
-    >>> print latest_news(priv_browser.contents).encode(
-    ...     'ascii', 'backslashreplace')
+    >>> print(latest_news(priv_browser.contents).encode(
+    ...     'ascii', 'backslashreplace'))
     Announcements
     Apache announcement headline ...
     Tomcat announcement headline on 2007-11-24 ...
@@ -169,7 +169,7 @@ And check out the results:
 
 Let's make sure that the announcement is presented as a link.
 
-    >>> print priv_browser.getLink('Tomcat announcement headline').url
+    >>> print(priv_browser.getLink('Tomcat announcement headline').url)
     http://launchpad.test/tomcat/+announcement/...
 
 We'll repeat the process for Derby, an IProduct that is part of the
@@ -185,7 +185,7 @@ announcement immediately:
     >>> priv_browser.getControl('Summary').value = (
     ...     'Derby announcement summary')
     >>> priv_browser.getControl('Make announcement').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     Derby in Launchpad
     >>> 'Derby announcement' in latest_news(priv_browser.contents)
     True
@@ -205,7 +205,7 @@ date in the future when the announcement will be made:
     ...     name='field.publication_date.announcement_date').value = (
     ...          '2021-12-24 09:00:00')
     >>> priv_browser.getControl('Make announcement').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     Jokosher in Launchpad
     >>> 'Jokosher announcement' in latest_news(priv_browser.contents)
     True
@@ -221,7 +221,7 @@ a date for the announcement at all:
     ...     'Kubuntu announcement summary')
     >>> priv_browser.getControl('some time in the future').click()
     >>> priv_browser.getControl('Make announcement').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     Kubuntu in Launchpad
     >>> "Kubuntu announcement" in latest_news(priv_browser.contents)
     True
@@ -235,7 +235,7 @@ And finally for RedHat, an IDistribution, with immediate announcement:
     >>> priv_browser.getControl('Summary').value = (
     ...     'RedHat announcement summary')
     >>> priv_browser.getControl('Make announcement').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     Red Hat in Launchpad
     >>> "RedHat announcement" in latest_news(priv_browser.contents)
     True
@@ -264,21 +264,21 @@ published:
 
     >>> anon_browser.open('http://launchpad.test/apache/+announcements')
     >>> anon_browser.getLink('Derby announcement headline').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Derby announcement headline : Derby
 
 The page shows the announcement and it has a link back to the announcements
 page that any user can navigate.
 
     >>> content = find_main_content(anon_browser.contents)
-    >>> print extract_text(content.h1)
+    >>> print(extract_text(content.h1))
     Derby announcement headline
 
-    >>> print extract_text(content.findAll('p')[1])
+    >>> print(extract_text(content.findAll('p')[1]))
     Derby announcement summary
 
     >>> anon_browser.getLink("Read all announcements").click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     News and announcements...
 
 
@@ -366,9 +366,9 @@ We can publish this announcement immediately.
 
     >>> priv_browser.getLink('Kubuntu announcement headline').click()
     >>> priv_browser.getLink('Publish announcement').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     Publish announcement : Kubuntu announcement headline : Kubuntu
-    >>> print priv_browser.url
+    >>> print(priv_browser.url)
     http://launchpad.test/kubuntu/+announceme.../+publish
     >>> radio = priv_browser.getControl(name="field.publication_date.action")
     >>> radio.value = ['immediately']
@@ -376,7 +376,7 @@ We can publish this announcement immediately.
 
 Doing so takes us back to the list of announcements.
 
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     News and announcements...
 
 And since the announcement has been made, the everybody can now see
@@ -464,7 +464,7 @@ hosted in Launchpad:
 The announcements are batched so only the latest four are shown,
 leaving Tomcat out:
 
-    >>> print extract_text(anon_browser.contents)
+    >>> print(extract_text(anon_browser.contents))
     Announcements from all projects hosted in Launchpad
     ...
     1...4 of 25 results
@@ -486,34 +486,34 @@ The announcement listing page does not have editing links.  They are
 available on the individual announcement pages.
 
     >>> priv_browser.open('http://launchpad.test/tomcat/+announcements')
-    >>> print priv_browser.getLink('Read more').url
+    >>> print(priv_browser.getLink('Read more').url)
     http://apache.org/announcement/rocking/
     >>> priv_browser.getLink('Apache announcement headline').click()
     >>> priv_browser.getLink('Modify announcement').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     Modify announcement : Apache announcement headline : Apache
     >>> headline = priv_browser.getControl('Headline')
-    >>> print headline.value
+    >>> print(headline.value)
     Apache announcement headline
     >>> headline.value = 'Modified headline'
     >>> summary = priv_browser.getControl('Summary')
-    >>> print summary.value
+    >>> print(summary.value)
     Apache announcement summary
     >>> summary.value = 'Modified summary'
     >>> url = priv_browser.getControl('URL')
-    >>> print url.value
+    >>> print(url.value)
     http://apache.org/announcement/rocking/
     >>> url.value = (
     ...     'http://apache.org/modified/url/')
     >>> priv_browser.getControl('Modify').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     News and announcements...
     >>> priv_browser.open('http://launchpad.test/tomcat/+announcements')
     >>> 'Modified headline' in announcements(priv_browser.contents)
     True
     >>> 'Modified summary' in announcements(priv_browser.contents)
     True
-    >>> print priv_browser.getLink('Read more').url
+    >>> print(priv_browser.getLink('Read more').url)
     http://apache.org/modified/url/
 
 
@@ -530,14 +530,14 @@ You can retract an announcement which was previously announced.
     >>> priv_browser.getLink('Kubuntu announcement headline').click()
     >>> priv_browser.getLink('Delete announcement').click()
     >>> priv_browser.getLink('retracting the announcement').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     Retract announcement : Kubuntu announcement headline : Kubuntu
 
 Actually clicking "Retract" takes us back to the listing page. The item
 is shown as having been retracted if you are a privileged user.
 
     >>> priv_browser.getControl('Retract').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     News and announcements...
     >>> 'Kubuntu announcement ' in announcements(priv_browser.contents)
     True
@@ -562,14 +562,14 @@ Once something has been retracted, it can be published again.
 
     >>> priv_browser.getLink('Kubuntu announcement headline').click()
     >>> priv_browser.getLink('Publish announcement').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     Publish announcement : Kubuntu announcement headline : Kubuntu
     >>> radio = priv_browser.getControl(name="field.publication_date.action")
     >>> radio.value = ['immediately']
     >>> priv_browser.getControl(
     ...     name="field.publication_date.announcement_date").value = ''
     >>> priv_browser.getControl('Publish').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     News and announcements...
 
 And once again it is visible to unprivileged users:
@@ -591,11 +591,11 @@ it.
     >>> priv_browser.open('http://launchpad.test/kubuntu/+announcements')
     >>> priv_browser.getLink('Kubuntu announcement headline').click()
     >>> priv_browser.getLink('Move announcement').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     Move announcement : Kubuntu announcement headline : Kubuntu
     >>> priv_browser.getControl('For').value = 'guadalinex'
     >>> priv_browser.getControl('Retarget').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     News and announcements...
     >>> 'Kubuntu announcement' in announcements(priv_browser.contents)
     True
@@ -608,14 +608,14 @@ not be able to move it.
     >>> kamion_browser.open('http://launchpad.test/guadalinex/+announcements')
     >>> kamion_browser.getLink('Kubuntu announcement headline').click()
     >>> kamion_browser.getLink('Move announcement').click()
-    >>> print kamion_browser.title
+    >>> print(kamion_browser.title)
     Move announcement : Kubuntu announcement headline : GuadaLinex
     >>> kamion_browser.getControl('For').value = 'kubuntu'
     >>> kamion_browser.getControl('Retarget').click()
     >>> "don't have permission" in extract_text(
     ...     find_main_content(kamion_browser.contents))
     True
-    >>> print kamion_browser.title
+    >>> print(kamion_browser.title)
     Move announcement : Kubuntu announcement headline : GuadaLinex
 
 
@@ -642,11 +642,11 @@ domain.
     >>> strainer = SoupStrainer('link', rel='self')
     >>> links = parse_links(nopriv_browser.contents, rel='self')
     >>> for link in links:
-    ...     print link
+    ...     print(link)
     <link href="http://feeds.launchpad.test/netapplet/announcements.atom"; rel="self"/>
 
     >>> for id_ in parse_ids(nopriv_browser.contents):
-    ...     print extract_text(id_)
+    ...     print(extract_text(id_))
     tag:launchpad.net,2005-03-10:/netapplet/+announcements
 
 The feeds include only published announcements. The Jokosher
@@ -672,7 +672,7 @@ Retracted items do not show up either.
     >>> 'Kubuntu announcement headline' in nopriv_browser.contents
     True
     >>> for id_ in parse_ids(nopriv_browser.contents):
-    ...     print extract_text(id_)
+    ...     print(extract_text(id_))
     tag:launchpad.net,2006-10-16:/guadalinex/+announcements
     tag:launchpad.net,...:/+announcement/...
 
@@ -683,7 +683,7 @@ Retracted items do not show up either.
     >>> priv_browser.getLink('Kubuntu announcement headline').click()
     >>> priv_browser.getLink('Delete announcement').click()
     >>> priv_browser.getLink('retracting the announcement').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     Retract announcement : Kubuntu announcement headline : GuadaLinex
     >>> priv_browser.getControl('Retract').click()
     >>> nopriv_browser.reload()
@@ -706,7 +706,7 @@ products.
     >>> 'Derby announcement headline' in nopriv_browser.contents
     True
     >>> for id_ in parse_ids(nopriv_browser.contents):
-    ...     print extract_text(id_)
+    ...     print(extract_text(id_))
     tag:launchpad.net,2004-09-24:/apache/+announcements
     tag:launchpad.net,...:/+announcement/...
     tag:launchpad.net,...:/+announcement/...
@@ -715,7 +715,7 @@ products.
     >>> strainer = SoupStrainer('link', rel='self')
     >>> links = parse_links(nopriv_browser.contents, rel='self')
     >>> for link in links:
-    ...     print link
+    ...     print(link)
     <link href="http://feeds.launchpad.test/apache/announcements.atom"; rel="self"/>
 
 Finally, there is a feed for all announcements across all projects
@@ -781,10 +781,10 @@ An owner can permanently delete an announcement.
     False
     >>> kamion_browser.getLink('Kubuntu announcement headline').click()
     >>> kamion_browser.getLink('Delete announcement').click()
-    >>> print kamion_browser.title
+    >>> print(kamion_browser.title)
     Delete announcement : Kubuntu announcement headline : GuadaLinex
     >>> kamion_browser.getControl('Delete').click()
-    >>> print priv_browser.title
+    >>> print(priv_browser.title)
     News and announcements...
     >>> no_announcements(kamion_browser.contents)
     True
diff --git a/lib/lp/registry/stories/distribution/xx-distribution-launchpad-usage.txt b/lib/lp/registry/stories/distribution/xx-distribution-launchpad-usage.txt
index 07a4613..f8dc2c9 100644
--- a/lib/lp/registry/stories/distribution/xx-distribution-launchpad-usage.txt
+++ b/lib/lp/registry/stories/distribution/xx-distribution-launchpad-usage.txt
@@ -33,23 +33,23 @@ The distribution's registrant can access the page and change the usage.
     >>> registrant = setupBrowser(
     ...     auth='Basic celso.providelo@xxxxxxxxxxxxx:test')
     >>> registrant.open('http://launchpad.test/ubuntu/+edit')
-    >>> print registrant.url
+    >>> print(registrant.url)
     http://launchpad.test/ubuntu/+edit
 
-    >>> print registrant.getControl(name='field.translations_usage').value[0]
+    >>> print(registrant.getControl(name='field.translations_usage').value[0])
     LAUNCHPAD
-    >>> print registrant.getControl(name='field.official_malone').value
+    >>> print(registrant.getControl(name='field.official_malone').value)
     True
-    >>> print registrant.getControl(name='field.enable_bug_expiration').value
+    >>> print(registrant.getControl(name='field.enable_bug_expiration').value)
     True
-    >>> print registrant.getControl(name='field.blueprints_usage').value[0]
+    >>> print(registrant.getControl(name='field.blueprints_usage').value[0])
     LAUNCHPAD
-    >>> print registrant.getControl(name='field.answers_usage').value[0]
+    >>> print(registrant.getControl(name='field.answers_usage').value[0])
     LAUNCHPAD
-    >>> print bool(
-    ...     registrant.getControl(name='field.require_virtualized').value)
+    >>> print(bool(
+    ...     registrant.getControl(name='field.require_virtualized').value))
     False
-    >>> print registrant.getControl(name='field.processors').value
+    >>> print(registrant.getControl(name='field.processors').value)
     ['386', 'amd64', 'hppa']
 
     >>> registrant.getControl(name='field.translations_usage').value = [
@@ -60,19 +60,19 @@ Just like Launchpad administrators can.
 
     >>> admin_browser.open('http://launchpad.test/ubuntu')
     >>> admin_browser.getLink('Change details').click()
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     Change Ubuntu details...
-    >>> print admin_browser.getControl(
-    ...     name='field.translations_usage').value[0]
+    >>> print(admin_browser.getControl(
+    ...     name='field.translations_usage').value[0])
     UNKNOWN
-    >>> print admin_browser.getControl(name='field.official_malone').value
+    >>> print(admin_browser.getControl(name='field.official_malone').value)
     True
-    >>> print admin_browser.getControl(
-    ...     name='field.enable_bug_expiration').value
+    >>> print(admin_browser.getControl(
+    ...     name='field.enable_bug_expiration').value)
     True
-    >>> print admin_browser.getControl(name='field.blueprints_usage').value[0]
+    >>> print(admin_browser.getControl(name='field.blueprints_usage').value[0])
     LAUNCHPAD
-    >>> print admin_browser.getControl(name='field.answers_usage').value[0]
+    >>> print(admin_browser.getControl(name='field.answers_usage').value[0])
     LAUNCHPAD
 
     >>> admin_browser.getControl(
@@ -84,13 +84,13 @@ Just like Launchpad administrators can.
     ...     name='field.answers_usage').value = ['UNKNOWN']
     >>> admin_browser.getControl('Change', index=3).click()
 
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://launchpad.test/ubuntu
 
 Only administators can configure the publisher for the distribution:
 
     >>> admin_browser.getLink('Configure publisher').click()
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     Publisher configuration for...
 
     >>> admin_browser.getControl(
@@ -101,7 +101,7 @@ Only administators can configure the publisher for the distribution:
     ...     name='field.copy_base_url').value = "http://copy.base.url/";
     >>> admin_browser.getControl('Save').click()
 
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://launchpad.test/ubuntu
 
 
@@ -130,11 +130,11 @@ do not change the expiration check box, and they do the whole
 operation before the page complete loading.
 
     >>> admin_browser.getLink('Change details').click()
-    >>> print admin_browser.getControl(name='field.official_malone').value
+    >>> print(admin_browser.getControl(name='field.official_malone').value)
     True
 
-    >>> print admin_browser.getControl(
-    ...     name='field.enable_bug_expiration').value
+    >>> print(admin_browser.getControl(
+    ...     name='field.enable_bug_expiration').value)
     True
 
     >>> admin_browser.getControl(name='field.official_malone').value = False
diff --git a/lib/lp/registry/stories/distribution/xx-distribution-overview.txt b/lib/lp/registry/stories/distribution/xx-distribution-overview.txt
index cc5ddcf..cb14899 100644
--- a/lib/lp/registry/stories/distribution/xx-distribution-overview.txt
+++ b/lib/lp/registry/stories/distribution/xx-distribution-overview.txt
@@ -14,7 +14,7 @@ Distribution listings
 There is a listing of all distributions at /distros/:
 
     >>> anon_browser.open('http://launchpad.test/distros/')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Distributions registered in Launchpad
 
 
@@ -28,7 +28,7 @@ series.
 Some distributions have listings of major versions, for example Debian:
 
     >>> anon_browser.open('http://launchpad.test/debian')
-    >>> print extract_text(find_tag_by_id(anon_browser.contents, 'sandm'))
+    >>> print(extract_text(find_tag_by_id(anon_browser.contents, 'sandm')))
     Active series and milestones
     3.1 “Sarge” series - frozen
     3.0 “Woody” series - current
@@ -44,7 +44,7 @@ rules.
     >>> admin_browser.getControl("Register Milestone").click()
 
     >>> anon_browser.open('http://launchpad.test/debian')
-    >>> print extract_text(find_tag_by_id(anon_browser.contents, 'sandm'))
+    >>> print(extract_text(find_tag_by_id(anon_browser.contents, 'sandm')))
     Active series and milestones
     ...
         Milestones: testmilestone, 3.1, and 3.1-rc1
@@ -77,15 +77,15 @@ to the distribution series and milestones pages respectively.
 Others do not have any series so the portlet is not shown.
 
     >>> anon_browser.open('http://launchpad.test/gentoo')
-    >>> print find_tag_by_id(anon_browser.contents, 'sandm')
+    >>> print(find_tag_by_id(anon_browser.contents, 'sandm'))
     None
 
 The 5 latest derivatives are displayed on the home page
 along with a link to list all of them.
 
     >>> anon_browser.open('http://launchpad.test/ubuntu')
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'derivatives'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'derivatives')))
     Latest derivatives
     9.9.9
     “Hoary Mock” series
@@ -111,8 +111,8 @@ If there are no derivatives, the link to the derivatives page is
 not there.
 
     >>> anon_browser.open('http://launchpad.test/ubuntutest')
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'derivatives'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'derivatives')))
     Latest derivatives
     No derivatives.
 
@@ -132,10 +132,10 @@ If there is a development series alias, it becomes a redirect.
     ...     ubuntu = getUtility(IDistributionSet).getByName(u"ubuntu")
     ...     ubuntu.development_series_alias = "devel"
     >>> anon_browser.open("http://launchpad.test/ubuntu/devel";)
-    >>> print anon_browser.url
+    >>> print(anon_browser.url)
     http://launchpad.test/ubuntu/hoary
     >>> anon_browser.open("http://launchpad.test/ubuntu/devel/+builds";)
-    >>> print anon_browser.url
+    >>> print(anon_browser.url)
     http://launchpad.test/ubuntu/hoary/+builds
 
 
@@ -146,13 +146,13 @@ The distroseries pages presents the registration information.
 
     >>> anon_browser.open('http://launchpad.test/ubuntu')
 
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'registration'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'registration')))
     Registered by
     Registry Administrators
     on 2006-10-16
 
-    >>> print anon_browser.getLink('Ubuntu Team').url
+    >>> print(anon_browser.getLink('Ubuntu Team').url)
     http://launchpad.test/~ubuntu-team
 
 
@@ -164,11 +164,11 @@ Displaying the page for that URL is nonsensical (it looks like the PPA
 index page), so the view redirect it to the distribution index page.
 
     >>> anon_browser.open("http://launchpad.test/ubuntu/+archive/primary";)
-    >>> print anon_browser.url
+    >>> print(anon_browser.url)
     http://launchpad.test/ubuntu
 
     >>> anon_browser.open("http://launchpad.test/ubuntu/+archive/partner";)
-    >>> print anon_browser.url
+    >>> print(anon_browser.url)
     http://launchpad.test/ubuntu
 
 Any attempt to access an incomplete URL (missing the archive name
diff --git a/lib/lp/registry/stories/distributionmirror/xx-distribution-countrymirrors.txt b/lib/lp/registry/stories/distributionmirror/xx-distribution-countrymirrors.txt
index eea6781..b81fa99 100644
--- a/lib/lp/registry/stories/distributionmirror/xx-distribution-countrymirrors.txt
+++ b/lib/lp/registry/stories/distributionmirror/xx-distribution-countrymirrors.txt
@@ -11,15 +11,15 @@ archive mirrors, plus the canonical one.
 
     >>> browser.addHeader('X_FORWARDED_FOR', '83.196.46.77')
     >>> browser.open('http://launchpad.test/ubuntu/+countrymirrors-archive')
-    >>> print browser.headers['content-type']
+    >>> print(browser.headers['content-type'])
     text/plain;charset=utf-8
-    >>> print browser.headers['X-Generated-For-Country']
+    >>> print(browser.headers['X-Generated-For-Country'])
     France
-    >>> print browser.headers['X-Generated-For-IP']
+    >>> print(browser.headers['X-Generated-For-IP'])
     83.196.46.77
-    >>> print browser.headers['X-REQUEST-HTTP_X_FORWARDED_FOR']
+    >>> print(browser.headers['X-REQUEST-HTTP_X_FORWARDED_FOR'])
     83.196.46.77
-    >>> print browser.headers['X-REQUEST-REMOTE_ADDR']
+    >>> print(browser.headers['X-REQUEST-REMOTE_ADDR'])
     127.0.0.1
     >>> for url in sorted(browser.contents.split('\n')):
     ...     print(url)
@@ -32,7 +32,7 @@ canonical mirror.
 
     >>> anon_browser.open(
     ...     'http://launchpad.test/ubuntu/+countrymirrors-archive')
-    >>> print anon_browser.headers['X-Generated-For-Country']
+    >>> print(anon_browser.headers['X-Generated-For-Country'])
     Unknown
     >>> for url in sorted(anon_browser.contents.split('\n')):
     ...     print(url)
diff --git a/lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt b/lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt
index 19885a6..c694d2b 100644
--- a/lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt
+++ b/lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt
@@ -13,7 +13,7 @@ on their status and content.
     ...     mirrors = []
     ...     for tr in header.findNextSiblings('tr'):
     ...         if 'head' in str(tr.attrs):
-    ...             print "%s: %s" % (country, mirrors)
+    ...             print("%s: %s" % (country, mirrors))
     ...             country = extract_text(tr.find('th'))
     ...             if country == 'Total':
     ...                 break
@@ -39,7 +39,7 @@ The archive mirrors display the "freshness", how far behind they are.
 
     >>> browser.open('http://launchpad.test/ubuntu')
     >>> browser.getLink('Archive mirrors').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Mirrors :...
     >>> print_mirrors_by_countries(browser.contents)
     Antarctica:
@@ -89,7 +89,7 @@ seen by distro owners, mirror admins of the distro or launchpad admins.
     >>> browser.url
     'http://launchpad.test/ubuntu/+disabledmirrors'
 
-    >>> print find_tag_by_id(browser.contents, 'maincontent').renderContents()
+    >>> print(find_tag_by_id(browser.contents, 'maincontent').renderContents())
     <BLANKLINE>
     ...We don't know of any Disabled Mirrors for this distribution...
 
diff --git a/lib/lp/registry/stories/distroseries/distroseries-admin.txt b/lib/lp/registry/stories/distroseries/distroseries-admin.txt
index 1edd1b1..33303f5 100644
--- a/lib/lp/registry/stories/distroseries/distroseries-admin.txt
+++ b/lib/lp/registry/stories/distroseries/distroseries-admin.txt
@@ -8,29 +8,29 @@ Launchpad administrators can edit distroseries via two different
 pages: 'Change details' and 'Administer'.
 
     >>> admin_browser.open('http://launchpad.test/ubuntu/hoary')
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     Hoary (5.04)...
     >>> admin_browser.getLink('Change details').click()
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://launchpad.test/ubuntu/hoary/+edit
 
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     Edit The Hoary Hedgehog Release...
 
     >>> admin_browser.getControl(
     ...     'Display name', index=0).value = 'Happy'
     >>> admin_browser.getControl('Change').click()
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     Happy (5.04)...
 
 A separate administration page is available via the 'Administer' link.
 
     >>> admin_browser.open('http://launchpad.test/ubuntu/hoary')
     >>> admin_browser.getLink('Administer').click()
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://launchpad.test/ubuntu/hoary/+admin
 
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     Administer The Hoary Hedgehog Release...
 
     >>> admin_browser.getControl(
@@ -38,9 +38,9 @@ A separate administration page is available via the 'Administer' link.
     >>> admin_browser.getControl(
     ...     'Version', index=0).value = '5.05'
     >>> admin_browser.getControl('Change').click()
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://launchpad.test/ubuntu/happy
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     Happy (5.05)...
 
 
@@ -71,10 +71,10 @@ Registry experts do have access to the administration page.
 
     >>> registry_browser.open('http://launchpad.test/ubuntu/happy')
     >>> registry_browser.getLink('Administer').click()
-    >>> print registry_browser.url
+    >>> print(registry_browser.url)
     http://launchpad.test/ubuntu/happy/+admin
 
-    >>> print registry_browser.title
+    >>> print(registry_browser.title)
     Administer The Hoary Hedgehog Release...
 
     >>> registry_browser.getControl(
@@ -82,7 +82,7 @@ Registry experts do have access to the administration page.
     >>> registry_browser.getControl(
     ...     'Version', index=0).value = '5.04'
     >>> registry_browser.getControl('Change').click()
-    >>> print registry_browser.url
+    >>> print(registry_browser.url)
     http://launchpad.test/ubuntu/hoary
-    >>> print registry_browser.title
+    >>> print(registry_browser.title)
     Happy (5.04)...
diff --git a/lib/lp/registry/stories/distroseries/xx-distroseries-index.txt b/lib/lp/registry/stories/distroseries/xx-distroseries-index.txt
index 9be9c1e..f5eaa6f 100644
--- a/lib/lp/registry/stories/distroseries/xx-distroseries-index.txt
+++ b/lib/lp/registry/stories/distroseries/xx-distroseries-index.txt
@@ -6,7 +6,7 @@ this distribution series.
 
     >>> user_browser.open('http://launchpad.test/ubuntu/hoary')
     >>> user_browser.getLink('Help translate').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Hoary (5.04) : Translations : Ubuntu
 
 
@@ -18,17 +18,17 @@ its main 'heading'.
 
     >>> anon_browser.open('http://launchpad.test/ubuntu/warty')
 
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'registration'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'registration')))
     Registered by
     Ubuntu Team on
     2006-10-16
 
-    >>> print extract_text(find_main_content(anon_browser.contents))
+    >>> print(extract_text(find_main_content(anon_browser.contents)))
     Warty
     ...
 
-    >>> print anon_browser.getLink('Ubuntu Team').url
+    >>> print(anon_browser.getLink('Ubuntu Team').url)
     http://launchpad.test/~ubuntu-team
 
 
@@ -38,8 +38,8 @@ Details portlet
 The distroseries page contains a details portlet giving more information
 on the series' details.
 
-    >>> print extract_text(
-    ...     find_portlet(anon_browser.contents, 'Series information'))
+    >>> print(extract_text(
+    ...     find_portlet(anon_browser.contents, 'Series information')))
     Series information
     Distribution: Ubuntu
     Series: Warty (4.10)
@@ -55,8 +55,8 @@ On series that have no source or binary packages, the portlet will
 change its text slightly to annouce this:
 
     >>> anon_browser.open('http://launchpad.test/debian/sarge')
-    >>> print extract_text(
-    ...     find_portlet(anon_browser.contents, 'Series information'))
+    >>> print(extract_text(
+    ...     find_portlet(anon_browser.contents, 'Series information')))
     Series information
     Distribution: Debian
     Series: Sarge (3.1)
@@ -94,8 +94,8 @@ the series derived from this series:
     ...         for child in children]
 
     >>> anon_browser.open('http://launchpad.test/debian/sarge')
-    >>> print extract_text(
-    ...     find_portlet(anon_browser.contents, 'Series information'))
+    >>> print(extract_text(
+    ...     find_portlet(anon_browser.contents, 'Series information')))
     Series information
     Distribution: Debian
     Series: Sarge (3.1)
@@ -116,10 +116,10 @@ series, we can create structural bug subscriptions.
 
     >>> admin_browser.open('http://launchpad.test/ubuntu/warty')
     >>> admin_browser.getLink('Subscribe to bug mail').click()
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://launchpad.test/ubuntu/warty/+subscribe
 
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     Subscribe : Warty (4.10) : Bugs : Ubuntu
 
 
@@ -132,8 +132,8 @@ upstream packaging.
     >>> # Note that warty's sourcecount is stale in sample data
     >>> # which causes -2 need linking.
     >>> anon_browser.open('http://launchpad.test/ubuntu/warty')
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'series-packaging'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'series-packaging')))
     Upstream packaging
     5 source packages are linked to registered upstream projects.
     3 need linking.
diff --git a/lib/lp/registry/stories/distroseries/xx-show-distroseries-packaging.txt b/lib/lp/registry/stories/distroseries/xx-show-distroseries-packaging.txt
index f557d62..2ac0e42 100644
--- a/lib/lp/registry/stories/distroseries/xx-show-distroseries-packaging.txt
+++ b/lib/lp/registry/stories/distroseries/xx-show-distroseries-packaging.txt
@@ -6,14 +6,14 @@ series +index page.
 
     >>> anon_browser.open('http://launchpad.test/ubuntu/hoary')
     >>> anon_browser.getLink('All upstream links').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     All upstream links : ...
 
 The page lists the upstream packaging links.
 
     >>> content = find_main_content(anon_browser.contents)
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'packagings'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'packagings')))
     Source Package   Upstream Project   Upstream Contributor Connections
     netapplet
       NetApplet The Novell Network Applet
@@ -30,7 +30,7 @@ The page lists the upstream packaging links.
 Any use can see that this page is related to the needs packaging report. It
 is linked, but the link to this page is not enabled.
 
-    >>> print extract_text(find_tag_by_id(content, 'related-pages'))
+    >>> print(extract_text(find_tag_by_id(content, 'related-pages')))
     Needs upstream links     All upstream links
 
     >>> anon_browser.getLink('Needs upstream links')
@@ -47,14 +47,14 @@ links packages. Users can also hack the URL to set their own batch size.
 
     >>> anon_browser.open(
     ...     'http://launchpad.test/ubuntu/hoary/+packaging?start=0&batch=1')
-    >>> print extract_text(find_tag_by_id(
-    ...     anon_browser.contents, 'packagings'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     anon_browser.contents, 'packagings')))
     Source Package   Upstream Project   Upstream Contributor Connections
     netapplet ...
 
     >>> anon_browser.getLink('Next', index=0).click()
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'packagings'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'packagings')))
     Source Package   Upstream Project   Upstream Contributor Connections
     evolution ...
 
@@ -77,11 +77,11 @@ packages with the greatest need are listed first.
 
     >>> anon_browser.open('http://launchpad.test/ubuntu/hoary')
     >>> anon_browser.getLink('Needs upstream links').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Needs upstream links : ...
 
     >>> content = find_main_content(anon_browser.contents)
-    >>> print extract_text(find_tag_by_id(content, 'packages'))
+    >>> print(extract_text(find_tag_by_id(content, 'packages')))
     Source Package       Bugs     Translations
     pmount               No bugs  64 strings
     linux-source-2.6.15  1 bug    No strings
@@ -103,13 +103,13 @@ pages.
 
 The listing is batched so that the user can visit the next page listings.
 
-    >>> print extract_text(find_tag_by_id(content, 'listing-navigation'))
+    >>> print(extract_text(find_tag_by_id(content, 'listing-navigation')))
     1 ... 5 of 5 packages	First ... Previous ... Next ... Last
 
 Any use can see that this page is related to the packaging report. It is
 linked, but the link to this page is not enabled.
 
-    >>> print extract_text(find_tag_by_id(content, 'related-pages'))
+    >>> print(extract_text(find_tag_by_id(content, 'related-pages')))
     Needs upstream links     All upstream links
 
     >>> anon_browser.getLink('All upstream links')
diff --git a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
index b7ab998..5b2819a 100644
--- a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
+++ b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
@@ -28,7 +28,7 @@ Start out with a clean page containing no imported keys:
     >>> browser = setupBrowserFreshLogin(name12)
     >>> browser.open("http://launchpad.test/~name12";)
     >>> browser.getLink(url='+editpgpkeys').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Change your OpenPGP keys...
 
     >>> browser.getControl(name='DEACTIVATE_GPGKEY')
@@ -62,7 +62,7 @@ followed by ASCII armored encrypted confirmation instructions.  Ensure that
 the clear text instructions contain the expected URLs pointing to more help.
 
     >>> cipher_body = msg.get_payload(decode=True)
-    >>> print cipher_body
+    >>> print(cipher_body)
     Hello,
     <BLANKLINE>
     This message contains the instructions for confirming registration of an
@@ -123,7 +123,7 @@ Go to the link sent by email, to validate the email address.
 
 Get redirected to +validategpg, and confirm token:
 
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/token/.../+validategpg
     >>> browser.getControl('Continue').click()
 
@@ -146,9 +146,9 @@ Verify that the key was imported with the "can encrypt" flag set:
     >>> from lp.registry.model.gpgkey import GPGKey
     >>> key = GPGKey.selectOneBy(
     ...     fingerprint='A419AE861E88BC9E04B9C26FBA2B9389DFD20543')
-    >>> print key.owner.name
+    >>> print(key.owner.name)
     name12
-    >>> print key.can_encrypt
+    >>> print(key.can_encrypt)
     True
 
 
@@ -188,7 +188,7 @@ ability to decrypt text with this key.
 The email does contain some information about the key, and a token URL
 Sample Person should visit to verify their ownership of the key.
 
-    >>> print body
+    >>> print(body)
     <BLANKLINE>
     Hello,
     ...
@@ -231,7 +231,7 @@ Let's look at the text.
 
     >>> verification_content = find_main_content(
     ...     browser.contents).pre.string
-    >>> print verification_content
+    >>> print(verification_content)
     Please register 447DBF38C4F9C4ED752246B77D88913717B05A8F to the
     Launchpad user name12.  2005-04-01 12:00:00 UTC
 
@@ -354,7 +354,7 @@ Sample person has never signed a code of conduct.
 
     >>> browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/~name12/+codesofconduct')
-    >>> print extract_text(find_main_content(browser.contents))
+    >>> print(extract_text(find_main_content(browser.contents)))
     Codes of Conduct for Sample Person
     Launchpad records codes of conduct you sign as commitments to the
     principles of collaboration, tolerance and open communication that
@@ -401,11 +401,11 @@ that there is a new version available.
 
     >>> browser.open('http://launchpad.test/codeofconduct/1.0/+sign')
     >>> browser.getLink('the current version').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/codeofconduct/2.0
 
     >>> browser.getLink('Sign it').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/codeofconduct/2.0/+sign
 
 
@@ -419,13 +419,13 @@ appear in the same order.
     >>> reformatted_coc = read_file('reformatted_20_coc.asc')
     >>> browser.getControl('Signed Code').value = reformatted_coc
     >>> browser.getControl('Continue').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/~name12/+codesofconduct
 
 And now Sample Person's Codes of Conduct page shows that they've signed it.
 
     >>> browser.open('http://launchpad.test/~name12/+codesofconduct')
-    >>> print extract_text(find_main_content(browser.contents))
+    >>> print(extract_text(find_main_content(browser.contents)))
     Codes of Conduct for Sample Person
     Launchpad records codes of conduct you sign as commitments to the
     principles of collaboration, tolerance and open communication that
@@ -444,7 +444,7 @@ Now Sample Person will deactivate their key...
     >>> browser.url
     'http://launchpad.test/~name12/+editpgpkeys'
 
-    >>> print browser.contents
+    >>> print(browser.contents)
     <...
     ...Your active keys...
     ...1024D/A419AE861E88BC9E04B9C26FBA2B9389DFD20543...
@@ -454,7 +454,7 @@ Now Sample Person will deactivate their key...
 
     >>> browser.getControl('Deactivate Key').click()
     >>> for tag in find_main_content(browser.contents)('p', 'error message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     No key(s) selected for deactivation.
 
 
@@ -465,7 +465,7 @@ Now they select the checkbox and deactivate it.
     >>> browser.getControl('Deactivate Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Deactivated key(s): 1024D/A419AE861E88BC9E04B9C26FBA2B9389DFD20543
 
 
@@ -475,7 +475,7 @@ Sample Person already has a deactivated key.
     >>> browser.url
     'http://launchpad.test/~name12/+editpgpkeys'
 
-    >>> print browser.contents
+    >>> print(browser.contents)
     <...
     ...Deactivated keys...
     ...1024D/A419AE861E88BC9E04B9C26FBA2B9389DFD20543...
@@ -486,7 +486,7 @@ Now they'll request their key to be reactivated.
     >>> browser.getControl('Reactivate Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'error message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     No key(s) selected for reactivation.
 
     >>> browser.getControl(
@@ -494,7 +494,7 @@ Now they'll request their key to be reactivated.
     >>> browser.getControl('Reactivate Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     A message has been sent to test@xxxxxxxxxxxxx with instructions to reactivate...
 
 
@@ -518,7 +518,7 @@ token type (+validategpg).
     >>> browser.url == '%s/+validategpg' % token_url
     True
 
-    >>> print browser.contents
+    >>> print(browser.contents)
     <...
     ...Confirm the OpenPGP key...A419AE861E88BC9E04B9C26FBA2B9389DFD20543...
     ...Sample Person...
@@ -530,7 +530,7 @@ Now Sample Person confirms the reactivation.
     >>> browser.url
     'http://launchpad.test/~name12'
 
-    >>> print browser.contents
+    >>> print(browser.contents)
     <...
     ...Key 1024D/A419AE861E88BC9E04B9C26FBA2B9389DFD20543 successfully
     reactivated...
@@ -539,7 +539,7 @@ Now Sample Person confirms the reactivation.
 And now we can see the key listed as one of Sample Person's active keys.
 
     >>> browser.open('http://launchpad.test/~name12/+editpgpkeys')
-    >>> print browser.contents
+    >>> print(browser.contents)
     <...
     ...Your active keys...
     ...1024D/A419AE861E88BC9E04B9C26FBA2B9389DFD20543...
@@ -567,7 +567,7 @@ Try to import a key which is already imported:
   False
   >>> stub.test_emails
   []
-  >>> print browser.contents
+  >>> print(browser.contents)
   <BLANKLINE>
   ...
   ...has already been imported...
@@ -578,11 +578,11 @@ Try to import a key which is already imported:
 
 Ensure we are raising 404 error instead of System Error
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... POST /codeofconduct/donkey HTTP/1.1
   ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
   ... Referer: https://launchpad.test/
-  ... """)
+  ... """))
   HTTP/1.1 404 Not Found
   ...
 
@@ -613,7 +613,7 @@ Ensure the CoC was acknowledge by searching in the CoC Admin Console:
     >>> admin_browser.getControl(name='searchfor').value = ["all"]
     >>> admin_browser.getControl(name='name').value = "mark"
     >>> admin_browser.getControl(name='search').click()
-    >>> print extract_text(find_tag_by_id(admin_browser.contents, 'matches'))
+    >>> print(extract_text(find_tag_by_id(admin_browser.contents, 'matches')))
     Mark ... paper submission accepted by Foo Bar [ACTIVE]
 
 Test if the advertisement email was sent:
@@ -622,7 +622,7 @@ Test if the advertisement email was sent:
     >>> from lp.services.mail import stub
     >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
     >>> msg = email.message_from_string(raw_msg)
-    >>> print msg.get_payload(decode=True)
+    >>> print(msg.get_payload(decode=True))
     <BLANKLINE>
     ...
     User: 'Mark Shuttleworth'
diff --git a/lib/lp/registry/stories/gpg-coc/xx-ubuntu-codeofconduct-signer.txt b/lib/lp/registry/stories/gpg-coc/xx-ubuntu-codeofconduct-signer.txt
index 7bcc4c2..58dc5c4 100644
--- a/lib/lp/registry/stories/gpg-coc/xx-ubuntu-codeofconduct-signer.txt
+++ b/lib/lp/registry/stories/gpg-coc/xx-ubuntu-codeofconduct-signer.txt
@@ -7,21 +7,21 @@ Administrators can see the signed Ubuntu Code of Conducts of any given person.
     >>> admin_browser.open('http://launchpad.test/~name16')
     >>> admin_browser.url
     'http://launchpad.test/~name16'
-    >>> print extract_text(
-    ...     find_tag_by_id(admin_browser.contents, 'ubuntu-coc'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'ubuntu-coc')))
     Signed Ubuntu Code of Conduct: Yes
 
     >>> admin_browser.getLink(url='+codesofconduct').click()
     >>> signatures = find_tags_by_class(admin_browser.contents, 'signature')
     >>> for signature in signatures:
-    ...     print extract_text(signature)
+    ...     print(extract_text(signature))
     2005-09-27: digitally signed by Foo Bar
     (1024D/ABCDEF0123456789ABCDDCBA0000111112345678)
 
 A regular user can't see the link to Foo Bar's signed codes of conduct.
 
     >>> browser.open('http://launchpad.test/~name16')
-    >>> print extract_text(find_tag_by_id(browser.contents, 'ubuntu-coc'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'ubuntu-coc')))
     Signed Ubuntu Code of Conduct: Yes
 
     >>> browser.getLink(url='+codesofconduct')
@@ -34,7 +34,7 @@ link to the Ubuntu Code of Conduct forms.
 
     >>> browser.addHeader('Authorization', 'Basic no-priv@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/~no-priv')
-    >>> print extract_text(find_tag_by_id(browser.contents, 'ubuntu-coc'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'ubuntu-coc')))
     Signed Ubuntu Code of Conduct: No
 
     >>> browser.getLink(url='codeofconduct')
diff --git a/lib/lp/registry/stories/location/personlocation-edit.txt b/lib/lp/registry/stories/location/personlocation-edit.txt
index 53a5d66..a70877e 100644
--- a/lib/lp/registry/stories/location/personlocation-edit.txt
+++ b/lib/lp/registry/stories/location/personlocation-edit.txt
@@ -36,5 +36,5 @@ selected value.
 
     >>> self_browser.open('http://launchpad.test/~zzz')
     >>> self_browser.getLink('Set location and time zone').click()
-    >>> print self_browser.getControl(name='field.time_zone').value
+    >>> print(self_browser.getControl(name='field.time_zone').value)
     ['Europe/Madrid']
diff --git a/lib/lp/registry/stories/mailinglists/hosted-email-address.txt b/lib/lp/registry/stories/mailinglists/hosted-email-address.txt
index ec44e44..f9cebb1 100644
--- a/lib/lp/registry/stories/mailinglists/hosted-email-address.txt
+++ b/lib/lp/registry/stories/mailinglists/hosted-email-address.txt
@@ -38,7 +38,7 @@ Launchpad sends that address a confirmation message.
     >>> user_browser.getControl(
     ...     name='field.contact_address').value = 'bar@xxxxxxxxxxx'
     >>> user_browser.getControl('Change').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Aardvarks in Launchpad
     >>> print_feedback_messages(user_browser.contents)
     A confirmation message has been sent to...
@@ -47,9 +47,9 @@ Launchpad sends that address a confirmation message.
     []
     >>> import email
     >>> msg = email.message_from_string(raw_msg)
-    >>> print msg['From']
+    >>> print(msg['From'])
     Launchpad Email Validator <noreply@xxxxxxxxxxxxx>
-    >>> print msg['Subject']
+    >>> print(msg['Subject'])
     Launchpad: Validate your team's contact email address
 
 When the confirmation token url is followed, the external email address is
@@ -59,10 +59,10 @@ confirmed.
     ...     get_token_url_from_email)
     >>> token_url = get_token_url_from_email(raw_msg)
     >>> user_browser.open(token_url)
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Confirm email address
     >>> user_browser.getControl('Continue').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Aardvarks in Launchpad
     >>> print_feedback_messages(user_browser.contents)
     Email address successfully confirmed.
diff --git a/lib/lp/registry/stories/mailinglists/lifecycle.txt b/lib/lp/registry/stories/mailinglists/lifecycle.txt
index ca20341..fccd031 100644
--- a/lib/lp/registry/stories/mailinglists/lifecycle.txt
+++ b/lib/lp/registry/stories/mailinglists/lifecycle.txt
@@ -15,19 +15,19 @@ The owner of Landscape Developers applies for a mailing list.
     >>> browser.open('http://launchpad.test/~landscape-developers')
     >>> browser.getLink(url='+mailinglist').click()
     >>> from lp.services.helpers import backslashreplace
-    >>> print backslashreplace(browser.title)
+    >>> print(backslashreplace(browser.title))
     Mailing list configuration...
 
 They think for a second whether their mailing list is for Ubuntu or not.
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'ubuntu-notice'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'ubuntu-notice')))
     Ubuntu does not use Launchpad to host its mailing lists. Create them
     at lists.ubuntu.com instead.
-    >>> print browser.getLink('lists.ubuntu.com')
+    >>> print(browser.getLink('lists.ubuntu.com'))
     <Link text='lists.ubuntu.com' url='https://lists.ubuntu.com'>
 
     >>> browser.getControl('Create new Mailing List').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Landscape Developers in Launchpad
     >>> print_feedback_messages(browser.contents)
     The mailing list is being created and will be available for use in a few minutes.
@@ -52,7 +52,7 @@ Just because a mailing list has been applied for does not mean it has an
 archive link yet.
 
     >>> browser.open('http://launchpad.test/~landscape-developers')
-    >>> print find_tag_by_id(browser.contents, 'mailing-list-archive')
+    >>> print(find_tag_by_id(browser.contents, 'mailing-list-archive'))
     None
 
 Even after the list has been created, it still cannot be used as the contact
@@ -60,7 +60,7 @@ address until Mailman has acknowledged successful creation.
 
     >>> browser.reload()
     >>> browser.getLink(url='+mailinglist').click()
-    >>> print mailing_list_status_message(browser.contents)
+    >>> print(mailing_list_status_message(browser.contents))
     This team's mailing list will be available within a few minutes.
 
 Mailman eventually wakes up and creates the mailing list.
@@ -78,14 +78,14 @@ is true even if no messages have yet been posted to the mailing list (since
 the archiver will display an informative message to that effect).
 
     >>> browser.open('http://launchpad.test/~landscape-developers')
-    >>> print extract_link_from_tag(
-    ...     find_tag_by_id(browser.contents, 'mailing-list-archive'))
+    >>> print(extract_link_from_tag(
+    ...     find_tag_by_id(browser.contents, 'mailing-list-archive')))
     http://lists.launchpad.test/landscape-developers
 
 The team's overview page also displays the posting address.
 
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'mailing-list-posting-address'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'mailing-list-posting-address')))
     landscape-developers@xxxxxxxxxxxxxxxxxxxx
 
 Now that the mailing list is active, it can be used as the team's contact
@@ -94,8 +94,8 @@ address.
     >>> from lp.testing.pages import strip_label
 
     >>> browser.getLink(url='+mailinglist').click()
-    >>> print extract_text(find_tag_by_id(browser.contents,
-    ...                                   'mailing_list_not_contact_address'))
+    >>> print(extract_text(find_tag_by_id(browser.contents,
+    ...                                   'mailing_list_not_contact_address')))
     The mailing list is not set as the team contact address. You can
     set it.
 
@@ -110,7 +110,7 @@ address.
 
 The mailing list's configuration screen is also now available.
 
-    >>> print browser.getLink(url='+mailinglist').url
+    >>> print(browser.getLink(url='+mailinglist').url)
     http://launchpad.test/~landscape-developers/+mailinglist
 
 When the mailing list is not the team's contact address, the mailing
@@ -120,8 +120,8 @@ list configuration screen displays a message to this effect.
     >>> browser.getControl('Change').click()
 
     >>> browser.getLink(url='+mailinglist').click()
-    >>> print extract_text(find_tag_by_id(browser.contents,
-    ...                                   'mailing_list_not_contact_address'))
+    >>> print(extract_text(find_tag_by_id(browser.contents,
+    ...                                   'mailing_list_not_contact_address')))
     The mailing list is not set as the team contact address. You can
     set it.
 
@@ -130,7 +130,7 @@ The message contains a link to the contact address screen.
     >>> browser.getLink('set it').click()
     >>> browser.getControl('The Launchpad mailing list').selected = True
     >>> browser.getControl('Change').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Landscape Developers in Launchpad
 
 When the mailing list is the team's contact address, the message does
@@ -169,15 +169,15 @@ to 'each user individually'.
     >>> act()
     >>> browser.open(
     ...     'http://launchpad.test/~landscape-developers/+mailinglist')
-    >>> print mailing_list_status_message(browser.contents)
+    >>> print(mailing_list_status_message(browser.contents))
     This team's mailing list has been deactivated.
 
 A deactivated mailing list still has a link to its archive, because archives
 are never deleted.
 
     >>> browser.open('http://launchpad.test/~landscape-developers')
-    >>> print extract_link_from_tag(
-    ...     find_tag_by_id(browser.contents, 'mailing-list-archive'))
+    >>> print(extract_link_from_tag(
+    ...     find_tag_by_id(browser.contents, 'mailing-list-archive')))
     http://lists.launchpad.test/landscape-developers
 
 An inactive mailing list can be reactivated.
@@ -205,8 +205,8 @@ This does not restore the mailing list as the team's contact method:
 Of course, the reactivated list still has a link to its archive.
 
     >>> browser.open('http://launchpad.test/~landscape-developers')
-    >>> print extract_link_from_tag(
-    ...     find_tag_by_id(browser.contents, 'mailing-list-archive'))
+    >>> print(extract_link_from_tag(
+    ...     find_tag_by_id(browser.contents, 'mailing-list-archive')))
     http://lists.launchpad.test/landscape-developers
 
 The archive link is only available for public mailing lists as shown above,
@@ -222,8 +222,8 @@ and for private mailing lists for team members.
 The owner of the list can see archive link.
 
     >>> user_browser.open('http://launchpad.test/~bassists')
-    >>> print extract_link_from_tag(
-    ...     find_tag_by_id(user_browser.contents, 'mailing-list-archive'))
+    >>> print(extract_link_from_tag(
+    ...     find_tag_by_id(user_browser.contents, 'mailing-list-archive')))
     http://lists.launchpad.test/bassists
 
 Anonymous users cannot see the link, because they cannot even see the
@@ -255,15 +255,15 @@ Members who are not owners can see the link.
     >>> admin_browser.getControl('Add Member').click()
 
     >>> cprov_browser.open('http://launchpad.test/~bassists')
-    >>> print extract_link_from_tag(
-    ...     find_tag_by_id(cprov_browser.contents, 'mailing-list-archive'))
+    >>> print(extract_link_from_tag(
+    ...     find_tag_by_id(cprov_browser.contents, 'mailing-list-archive')))
     http://lists.launchpad.test/bassists
 
 Admins who are not members of the team can see the link too.
 
     >>> admin_browser.open('http://launchpad.test/~bassists')
-    >>> print extract_link_from_tag(
-    ...     find_tag_by_id(admin_browser.contents, 'mailing-list-archive'))
+    >>> print(extract_link_from_tag(
+    ...     find_tag_by_id(admin_browser.contents, 'mailing-list-archive')))
     http://lists.launchpad.test/bassists
 
 
@@ -287,14 +287,14 @@ to delete the archives of an INACTIVE list, this must be done manually.
     >>> def print_list_state(team_name=u'aardvarks'):
     ...     login('foo.bar@xxxxxxxxxxxxx')
     ...     mailing_list = getUtility(IMailingListSet).get(team_name)
-    ...     print mailing_list.status.name
+    ...     print(mailing_list.status.name)
     ...     logout()
 
 The team owner can see that they can purge or reactivate mailing list.
 
     >>> user_browser.open('http://launchpad.test/~aardvarks/+mailinglist')
     >>> user_browser.getControl('Create new Mailing List').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Aardvarks in Launchpad
     >>> act()
     >>> user_browser.open('http://launchpad.test/~aardvarks/+mailinglist')
@@ -310,7 +310,7 @@ The team owner can see that they can purge or reactivate mailing list.
     ...     return tag.p.contents[0].strip()
 
     >>> user_browser.getLink(url='+mailinglist').click()
-    >>> print purge_text(user_browser)
+    >>> print(purge_text(user_browser))
     You can purge this mailing list...
 
     >>> user_browser.getControl('Reactivate this Mailing List')
@@ -338,7 +338,7 @@ administrator, can purge a list.
 
     >>> expert_browser = setupBrowser('Basic test@xxxxxxxxxxxxx:test')
     >>> expert_browser.open('http://launchpad.test/~aardvarks/+mailinglist')
-    >>> print purge_text(expert_browser)
+    >>> print(purge_text(expert_browser))
     You can purge this mailing list...
 
 A constructing, modified, updating, or deactivating or mod-failed list cannot
@@ -361,9 +361,9 @@ be purged.
     ...         set_list_state(u'aardvarks', status)
     ...         print_list_state()
     ...         admin_browser.open(url)
-    ...         print purge_text(admin_browser)
+    ...         print(purge_text(admin_browser))
     ...         expert_browser.open(url)
-    ...         print purge_text(expert_browser)
+    ...         print(purge_text(expert_browser))
 
 A purged list acts as if it doesn't even exist.
 
diff --git a/lib/lp/registry/stories/mailinglists/subscriptions.txt b/lib/lp/registry/stories/mailinglists/subscriptions.txt
index d993e0a..9cbc162 100644
--- a/lib/lp/registry/stories/mailinglists/subscriptions.txt
+++ b/lib/lp/registry/stories/mailinglists/subscriptions.txt
@@ -92,7 +92,7 @@ it's currently the team contact method.
     >>> carlos_browser.getLink(url="+editmailinglists").click()
 
     >>> from lp.services.helpers import backslashreplace
-    >>> print backslashreplace(carlos_browser.title)
+    >>> print(backslashreplace(carlos_browser.title))
     Change your mailing list subscriptions...
 
     >>> admins = carlos_browser.getControl(name='field.subscription.admins')
@@ -103,9 +103,9 @@ it's currently the team contact method.
     ['Preferred address', "Don't subscribe",
      'carlos@xxxxxxxxxxxxx', 'carlos@xxxxxxxx']
 
-    >>> print admins.value
+    >>> print(admins.value)
     ["Don't subscribe"]
-    >>> print rosetta_admins.value
+    >>> print(rosetta_admins.value)
     ["Don't subscribe"]
 
 However, testing-spanish-team's list doesn't show up because its creation has
@@ -133,9 +133,9 @@ explicitly with whatever is his preferred email address.
     >>> admins = carlos_browser.getControl(name='field.subscription.admins')
     >>> rosetta_admins = carlos_browser.getControl(
     ...     name='field.subscription.rosetta-admins')
-    >>> print admins.value
+    >>> print(admins.value)
     ['Preferred address']
-    >>> print rosetta_admins.value
+    >>> print(rosetta_admins.value)
     ["Don't subscribe"]
 
 Carlos can subscribe to a list using any of his validated addresses
@@ -148,9 +148,9 @@ explicitly.
     >>> admins = carlos_browser.getControl(name='field.subscription.admins')
     >>> rosetta_admins = carlos_browser.getControl(
     ...     name='field.subscription.rosetta-admins')
-    >>> print admins.value
+    >>> print(admins.value)
     ['carlos@xxxxxxxxxxxxx']
-    >>> print rosetta_admins.value
+    >>> print(rosetta_admins.value)
     ['carlos@xxxxxxxx']
 
 He can switch from one address to another, or from a specific address
@@ -163,9 +163,9 @@ to the preferred address.
     >>> admins = carlos_browser.getControl(name='field.subscription.admins')
     >>> rosetta_admins = carlos_browser.getControl(
     ...     name='field.subscription.rosetta-admins')
-    >>> print admins.value
+    >>> print(admins.value)
     ['Preferred address']
-    >>> print rosetta_admins.value
+    >>> print(rosetta_admins.value)
     ['carlos@xxxxxxxxxxxxx']
 
 Finally, he can unsubscribe from any mailing list by setting the subscription
@@ -181,9 +181,9 @@ menu item to "Don't subscribe".
     >>> admins = carlos_browser.getControl(name='field.subscription.admins')
     >>> rosetta_admins = carlos_browser.getControl(
     ...     name='field.subscription.rosetta-admins')
-    >>> print admins.value
+    >>> print(admins.value)
     ["Don't subscribe"]
-    >>> print rosetta_admins.value
+    >>> print(rosetta_admins.value)
     ["Don't subscribe"]
 
 
@@ -200,7 +200,7 @@ Admins team, and he should know if the list is available.
 
     >>> carlos_browser.open('http://launchpad.test/~carlos')
     >>> carlos_browser.getLink(url="+editmailinglists").click()
-    >>> print backslashreplace(carlos_browser.title)
+    >>> print(backslashreplace(carlos_browser.title))
     Change your mailing list subscriptions...
 
     >>> rosetta_admins = carlos_browser.getControl(
@@ -223,7 +223,7 @@ Now Jdub can apply for team membership and mailing list access.
     'http://launchpad.test/~rosetta-admins'
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Your request to join Rosetta Administrators is awaiting approval.
     Your mailing list subscription is awaiting approval.
 
@@ -237,7 +237,7 @@ screen.
     >>> jdub_browser = setupBrowserFreshLogin(jdub)
     >>> jdub_browser.open('http://launchpad.test/~jdub')
     >>> jdub_browser.getLink(url="+editmailinglists").click()
-    >>> print jdub_browser.title
+    >>> print(jdub_browser.title)
     Change your mailing list subscriptions...
 
     >>> jdub_browser.getControl(
@@ -254,7 +254,7 @@ been approved for the team.
     >>> admin_browser.getLink('All members').click()
     >>> admin_browser.getLink(
     ...     url='/~rosetta-admins/+member/jdub').click()
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://launchpad.test/~rosetta-admins/+member/jdub
     >>> admin_browser.getControl(name='approve').click()
 
@@ -262,7 +262,7 @@ His mailing list subscription is now available to be managed.
 
     >>> jdub_browser.open('http://launchpad.test/~jdub')
     >>> jdub_browser.getLink(url="+editmailinglists").click()
-    >>> print jdub_browser.title
+    >>> print(jdub_browser.title)
     Change your mailing list subscriptions...
 
     >>> rosetta_team = jdub_browser.getControl(
@@ -280,7 +280,7 @@ list is not presented.
 
     >>> browser.open('http://launchpad.test/~rosetta-admins')
     >>> browser.getLink('Join the team').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/~rosetta-admins/+join
 
     >>> browser.getControl(name='mailinglist_subscribe')
@@ -294,7 +294,7 @@ for teams that don't have mailing lists.
 
     >>> browser.open('http://launchpad.test/~testing-spanish-team')
     >>> browser.getLink('Join the team').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/~testing-spanish-team/+join
 
     >>> browser.getControl(name='mailinglist_subscribe')
@@ -309,7 +309,7 @@ if we visit the URL directly, as the link is not present on the Team
 Overview.)
 
     >>> browser.open('http://launchpad.test/~launchpad/+join')
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/~launchpad/+join
 
     >>> browser.getControl(name='mailinglist_subscribe')
@@ -330,7 +330,7 @@ page, because he is not subscribed to the team mailing list.
 
     >>> carlos_browser.open('http://launchpad.test/~admins')
     >>> carlos_browser.getLink('Subscribe to mailing list').click()
-    >>> print carlos_browser.url
+    >>> print(carlos_browser.url)
     http://launchpad.test/~carlos/+editmailinglists
 
 The unsubscribe link is visible for the rosetta admins team, which
@@ -343,11 +343,11 @@ has an active mailing list.
     ...     name='field.subscription.rosetta-admins')
     >>> rosetta_admins.value = ['Preferred address']
     >>> carlos_browser.getControl('Update Subscriptions').click()
-    >>> print rosetta_admins.value
+    >>> print(rosetta_admins.value)
     ['Preferred address']
     >>> for tag in find_tags_by_class(
     ...     carlos_browser.contents, 'informational'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Subscriptions updated.
 
     >>> carlos_browser.open('http://launchpad.test/~rosetta-admins')
@@ -361,8 +361,8 @@ Clicking the link will unsubscribe you from the list immediately.
     You have been unsubscribed from the team mailing list.
 
     >>> carlos_browser.open('http://launchpad.test/~rosetta-admins')
-    >>> print extract_text(
-    ...     find_tag_by_id(carlos_browser.contents, 'mailing-lists'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(carlos_browser.contents, 'mailing-lists')))
     Mailing list...
     Subscribe to mailing list...
 
@@ -370,8 +370,8 @@ The Ubuntu translators team, which does not have any lists configured,
 does not show either link.
 
     >>> carlos_browser.open('http://launchpad.test/~ubuntu-translators')
-    >>> print extract_text(
-    ...     find_portlet(carlos_browser.contents, 'Mailing list'))
+    >>> print(extract_text(
+    ...     find_portlet(carlos_browser.contents, 'Mailing list')))
     Mailing list
     This team does not use Launchpad to host a mailing list.
     Create a mailing list
@@ -395,8 +395,8 @@ mailing list subscribers, if there is an active mailing list.  The
 rosetta admins team has such a list and carlos is the owner.
 
     >>> carlos_browser.open('http://launchpad.test/~rosetta-admins')
-    >>> print extract_text(
-    ...     find_portlet(carlos_browser.contents, 'Mailing list'))
+    >>> print(extract_text(
+    ...     find_portlet(carlos_browser.contents, 'Mailing list')))
     Mailing list
     rosetta-admins@xxxxxxxxxxxxxxxxxxxx
     Policy: You must be a team member to subscribe to the team mailing list.
@@ -409,11 +409,11 @@ The mailing list for Rosetta Admins has no subscribers.
 because his membership on Rosetta Admins hasn't been approved)
 
     >>> carlos_browser.getLink('View subscribers').click()
-    >>> print carlos_browser.title
+    >>> print(carlos_browser.title)
     Mailing list subscribers for the Rosetta Administrators team...
 
-    >>> print extract_text(
-    ...     find_tag_by_id(carlos_browser.contents, 'subscribers'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(carlos_browser.contents, 'subscribers')))
     Nobody has subscribed to this team's mailing list yet.
 
 If it had subscribers, though, they'd be shown on that page, in a batched
@@ -439,15 +439,15 @@ list.
     >>> rosetta_admins.mailing_list.subscribe(jordi)
     >>> logout()
     >>> carlos_browser.reload()
-    >>> print extract_text(
-    ...     find_tag_by_id(carlos_browser.contents, 'subscribers'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(carlos_browser.contents, 'subscribers')))
     The following people are subscribed...
     Guilherme Salgado
     1 of 2 results...
 
     >>> carlos_browser.getLink('Next').click()
-    >>> print extract_text(
-    ...    find_tag_by_id(carlos_browser.contents, 'subscribers'))
+    >>> print(extract_text(
+    ...    find_tag_by_id(carlos_browser.contents, 'subscribers')))
     The following people are subscribed...
     Jordi Mallach
     2 of 2 results...
@@ -474,7 +474,7 @@ list based on a setting in the person's Email preferences page.
 
     >>> carlos_browser.open('http://launchpad.test/~carlos')
     >>> carlos_browser.getLink(url="+editmailinglists").click()
-    >>> print backslashreplace(carlos_browser.title)
+    >>> print(backslashreplace(carlos_browser.title))
     Change your mailing list subscriptions...
 
 Carlos's default setting, 'Ask me when I join a team', is still in place.
@@ -534,8 +534,8 @@ team they are a member of creates a new mailing list.  This notification
 offers them to join the new mailing list.  This page informs them of this
 behaviour.
 
-    >>> print extract_text(
-    ...     find_tag_by_id(carlos_browser.contents, 'notification-info'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(carlos_browser.contents, 'notification-info')))
     When a team you are a member of creates a new mailing list, you will
     receive an email notification offering you the opportunity to join the new
     mailing list. Launchpad can also automatically subscribe you to a team's
@@ -562,10 +562,10 @@ subscription settings will see the box checked by default.
 
     >>> browser.open('http://launchpad.test/~rosetta-admins')
     >>> browser.getLink('Join the team').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/~rosetta-admins/+join
 
-    >>> print browser.getControl(name='field.mailinglist_subscribe').value
+    >>> print(browser.getControl(name='field.mailinglist_subscribe').value)
     True
 
     # Change James' setting
@@ -578,7 +578,7 @@ subscription settings will see the box checked by default.
 
     >>> browser.open('http://launchpad.test/~rosetta-admins')
     >>> browser.getLink('Join the team').click()
-    >>> print browser.getControl(name='field.mailinglist_subscribe').value
+    >>> print(browser.getControl(name='field.mailinglist_subscribe').value)
     True
 
 Users who have chosen to never be auto-subscribed to mailing
@@ -594,8 +594,8 @@ lists will not have the box checked.
 
     >>> browser.open('http://launchpad.test/~rosetta-admins')
     >>> browser.getLink('Join the team').click()
-    >>> print bool(
-    ...     browser.getControl(name='field.mailinglist_subscribe').value)
+    >>> print(bool(
+    ...     browser.getControl(name='field.mailinglist_subscribe').value))
     False
 
     # Restore James' setting.
diff --git a/lib/lp/registry/stories/mailinglists/welcome-message.txt b/lib/lp/registry/stories/mailinglists/welcome-message.txt
index 2565616..d7dd43d 100644
--- a/lib/lp/registry/stories/mailinglists/welcome-message.txt
+++ b/lib/lp/registry/stories/mailinglists/welcome-message.txt
@@ -22,7 +22,7 @@ Changes to the welcome message take effect as soon as Mailman can act on it.
 
     >>> user_browser.getLink('Configure mailing list').click()
     >>> welcome_message = user_browser.getControl('Welcome message')
-    >>> print welcome_message.value
+    >>> print(welcome_message.value)
     Welcome to the Aardvarks.
 
     >>> from lp.registry.tests import mailinglists_helper
@@ -55,6 +55,6 @@ What if Mailman failed to apply the change?
     >>> logout()
 
     >>> user_browser.open('http://launchpad.test/~aardvarks/+mailinglist')
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'mailing_list_status_message').strong)
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'mailing_list_status_message').strong))
     This team's mailing list is in an inconsistent state...
diff --git a/lib/lp/registry/stories/milestone/object-milestones.txt b/lib/lp/registry/stories/milestone/object-milestones.txt
index eb0efe0..5eaf227 100644
--- a/lib/lp/registry/stories/milestone/object-milestones.txt
+++ b/lib/lp/registry/stories/milestone/object-milestones.txt
@@ -46,7 +46,7 @@ Distributions
     >>> anon_browser.url
     'http://launchpad.test/debian/+milestones'
 
-    >>> print all_milestones(anon_browser)
+    >>> print(all_milestones(anon_browser))
     Debian 3.1      Woody ...
     Debian 3.1-rc1  Woody ...
 
@@ -55,12 +55,12 @@ Distribution Series
 ...................
 
     >>> anon_browser.open('http://launchpad.test/debian/woody/+milestones')
-    >>> print all_milestones(anon_browser)
+    >>> print(all_milestones(anon_browser))
     Debian 3.1      ...
     Debian 3.1-rc1  ...
 
     >>> anon_browser.open('http://launchpad.test/debian/sarge/+milestones')
-    >>> print all_milestones(anon_browser)
+    >>> print(all_milestones(anon_browser))
     None
 
 
@@ -75,7 +75,7 @@ counts (because they are costly to retrieve).
     >>> anon_browser.url
     'http://launchpad.test/firefox/+milestones'
 
-    >>> print all_milestones(anon_browser)
+    >>> print(all_milestones(anon_browser))
     Mozilla Firefox 1.0.0 "First Stable Release"    1.0    None        ...
     Mozilla Firefox 0.9.2 "One (secure) Tree Hill"  trunk  None        ...
     Mozilla Firefox 0.9.1 "One Tree Hill (v2)"      trunk  None        ...
@@ -88,7 +88,7 @@ link to the project groups's milestone's page.
 
     >>> anon_browser.getLink(
     ...     'View milestones for The Mozilla Project').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Milestones : The Mozilla Project
 
 
@@ -96,14 +96,14 @@ Product Series
 ..............
 
     >>> anon_browser.open('http://launchpad.test/firefox/trunk/+milestones')
-    >>> print all_milestones(anon_browser)
+    >>> print(all_milestones(anon_browser))
     Mozilla Firefox 0.9.2  ...
     Mozilla Firefox 0.9.1  ...
     Mozilla Firefox 0.9    ...
     Mozilla Firefox 1.0    ...
 
     >>> anon_browser.open('http://launchpad.test/firefox/1.0/+milestones')
-    >>> print all_milestones(anon_browser)
+    >>> print(all_milestones(anon_browser))
     Mozilla Firefox 1.0.0 ...
 
 
@@ -123,7 +123,7 @@ the bug and blueprint counts (because they are costly to retrieve).
     >>> logout()
     >>> anon_browser.open('http://launchpad.test/gnome')
     >>> anon_browser.getLink('See all milestones').click()
-    >>> print all_milestones(anon_browser)
+    >>> print(all_milestones(anon_browser))
     GNOME 2.1.6  None        This is an inactive milestone
     GNOME 1.0    None        This is an inactive milestone
     GNOME 1.3    A date      This is an inactive milestone
@@ -139,17 +139,17 @@ Pages for the individual milestones show all specifications and bugtasks
 associated with that milestone for products of this project:
 
     >>> anon_browser.getLink('1.1', index=1).click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     1.1 : GNOME
 
     >>> specs = find_tag_by_id(anon_browser.contents, 'milestone_specs')
-    >>> print extract_text(specs)
+    >>> print(extract_text(specs))
     Blueprint Project Priority Assignee Delivery
     Title evolution specification   Evolution  High  Unknown
     Title gnomebaker specification  gnomebaker High  Unknown
 
     >>> bugtasks = find_tag_by_id(anon_browser.contents, 'milestone_bugtasks')
-    >>> print extract_text(bugtasks)
+    >>> print(extract_text(bugtasks))
     Bug report Project Importance Assignee Status ...
     Milestone test bug for evolution  Evolution  Undecided Confirmed ...
     Milestone test bug for gnomebaker gnomebaker Undecided Confirmed ...
@@ -158,7 +158,7 @@ associated with that milestone for products of this project:
 A project milestone page has the same navigation as the project:
 
     >>> anon_browser.open('http://launchpad.test/firefox/+milestone/1.0')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     1.0 : Mozilla Firefox
 
     >>> print_location(anon_browser.contents)
@@ -176,7 +176,7 @@ Similarly, a distribution milestone page has the same navigation as the
 distribution:
 
     >>> anon_browser.open('http://launchpad.test/debian/+milestone/3.1')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     3.1 : Debian
 
     >>> print_location(anon_browser.contents)
@@ -251,42 +251,42 @@ Mozilla Firefox product, to complement the existing series "1.0":
 
     >>> browser.open('http://launchpad.test/firefox')
     >>> browser.getLink('Register a series').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Register a new Mozilla Firefox release series...
 
     >>> browser.getControl('Name').value = '2.0'
     >>> browser.getControl('Summary').value = 'The Firefox 2.0 Series'
     >>> browser.getControl('Register Series').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Series 2.0 : Mozilla Firefox
 
 We'll also create a new test milestone within the "trunk" series:
 
     >>> browser.open('http://launchpad.test/firefox/trunk')
-    >>> print browser.title
+    >>> print(browser.title)
     Series trunk : Mozilla Firefox
 
     >>> browser.getLink('Create milestone').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Register a new milestone...
 
     >>> milestone = 'test-milestone'
     >>> browser.getControl('Name').value = milestone
     >>> browser.getControl('Date Targeted').value = '2100-08-08'
     >>> browser.getControl('Register Milestone').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Series trunk : Mozilla Firefox
 
     >>> browser.open('http://launchpad.test/firefox/trunk')
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'series-trunk'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'series-trunk')))
     Version                         Expected    Released              Summary
     Mozilla Firefox 0.9.2...        Set date    Change details 2004-10-16  ...
     Mozilla Firefox...              Set date    Change details 2004-10-16  ...
     Mozilla Firefox test-milestone  2100-08-08  Release now ...
 
     >>> browser.getLink('test-milestone').click()
-    >>> print browser.title
+    >>> print(browser.title)
     test-milestone : Mozilla Firefox
 
     >>> milestone_url = browser.url
@@ -295,11 +295,11 @@ Let's target an existing bug to both series "1.0" and series "2.0":
 
     >>> from lp.services.helpers import backslashreplace
     >>> browser.open(bug_1_url)
-    >>> print backslashreplace(browser.title)
+    >>> print(backslashreplace(browser.title))
     Bug #...Test Bug 1... : Bugs : Mozilla Firefox
 
     >>> browser.getLink('Target to series').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Target bug #... to series...
 
     >>> browser.getControl('1.0').selected = True
@@ -308,7 +308,7 @@ Let's target an existing bug to both series "1.0" and series "2.0":
 
 The bug page now lists a bug task for each series:
 
-    >>> print extract_text(first_tag_by_class(browser.contents, 'listing'))
+    >>> print(extract_text(first_tag_by_class(browser.contents, 'listing')))
     Affects Status Importance ...
     1.0 ... New    Undecided  ...
     2.0 ... New    Undecided  ...
@@ -332,7 +332,7 @@ milestone:
     >>> browser.getControl('Importance').value = ['Critical']
     >>> browser.getControl('Save Changes').click()
 
-    >>> print extract_text(first_tag_by_class(browser.contents, 'listing'))
+    >>> print(extract_text(first_tag_by_class(browser.contents, 'listing')))
     Affects Status Importance ...
     1.0 ... New    Critical   ...
 
@@ -345,7 +345,7 @@ method. However this time we'll use a different importance:
     >>> browser.getControl('Importance').value = ['High']
     >>> browser.getControl('Save Changes').click()
 
-    >>> print extract_text(first_tag_by_class(browser.contents, 'listing'))
+    >>> print(extract_text(first_tag_by_class(browser.contents, 'listing')))
     Affects Status Importance ...
     2.0 ... New    High       ...
 
@@ -354,14 +354,14 @@ listing:
 
     >>> browser.open(milestone_url)
     >>> bug_table = find_tag_by_id(browser.contents, 'milestone_bugtasks')
-    >>> print extract_text(bug_table )
+    >>> print(extract_text(bug_table ))
     Bug report       Importance  Assignee  Status
     #... Test Bug 1  Critical              New
     #... Test Bug 1  High                  New
 
 Each bugtask has one or more badges.
 
-    >>> print bug_table.findAll('tr')[1]
+    >>> print(bug_table.findAll('tr')[1])
     <tr>...Test Bug 1...<a...alt="milestone test-milestone"...
       class="sprite milestone"...>...
 
@@ -382,8 +382,8 @@ that milestone's bug listing:
     >>> browser.getControl('Save Changes').click()
 
     >>> browser.open(milestone_url)
-    >>> print extract_text(find_tag_by_id(browser.contents,
-    ...                                   'milestone_bugtasks'))
+    >>> print(extract_text(find_tag_by_id(browser.contents,
+    ...                                   'milestone_bugtasks')))
     Bug report...
     Test Bug 2...
 
@@ -392,25 +392,25 @@ bug still appears in the milestone's bug listing:
 
     >>> browser.open(bug_2_url)
     >>> browser.getLink('Target to series').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://bugs.launchpad.test/firefox/+bug/.../+nominate
 
     >>> browser.getControl('Trunk').selected = True
     >>> browser.getControl('Target').click()
-    >>> print extract_text(first_tag_by_class(browser.contents, 'listing'))
+    >>> print(extract_text(first_tag_by_class(browser.contents, 'listing')))
     Affects             Status                  ...
     Mozilla Firefox ... Status tracked in Trunk ...
 
     >>> browser.open(milestone_url)
     >>> bugtasks = extract_text(find_tag_by_id(browser.contents,
     ...                                        'milestone_bugtasks'))
-    >>> print bugtasks
+    >>> print(bugtasks)
     Bug report...
     Test Bug 2...
 
 Moreover, the bug appears only once in the listing:
 
-    >>> print bugtasks.count('Test Bug 2')
+    >>> print(bugtasks.count('Test Bug 2'))
     1
 
 
diff --git a/lib/lp/registry/stories/milestone/xx-create-milestone-on-distribution.txt b/lib/lp/registry/stories/milestone/xx-create-milestone-on-distribution.txt
index 70b52f8..d1a8eb1 100644
--- a/lib/lp/registry/stories/milestone/xx-create-milestone-on-distribution.txt
+++ b/lib/lp/registry/stories/milestone/xx-create-milestone-on-distribution.txt
@@ -26,8 +26,8 @@ redirect to the Ubuntu Hoary page showing the milestone we added.
     >>> name12_browser.getControl('Register Milestone').click()
     >>> name12_browser.url
     'http://launchpad.test/ubuntu/hoary'
-    >>> print extract_text(
-    ...     find_tag_by_id(name12_browser.contents, 'series-hoary'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(name12_browser.contents, 'series-hoary')))
     Version ...
     sounder01 ...
 
diff --git a/lib/lp/registry/stories/milestone/xx-milestone-add-and-edit.txt b/lib/lp/registry/stories/milestone/xx-milestone-add-and-edit.txt
index a6159c8..2a229aa 100644
--- a/lib/lp/registry/stories/milestone/xx-milestone-add-and-edit.txt
+++ b/lib/lp/registry/stories/milestone/xx-milestone-add-and-edit.txt
@@ -70,9 +70,9 @@ can create structural bug subscriptions.
 
     >>> user_browser.open('http://launchpad.test/firefox/+milestone/1.0')
     >>> user_browser.getLink('Subscribe to bug mail').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://launchpad.test/firefox/+milestone/1.0/+subscribe
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Subscribe : 1.0 : Bugs : Mozilla Firefox
 
 But we can't subscribe to project milestones, since they are not real objects.
@@ -91,10 +91,10 @@ the series.
 
     >>> driver_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
     >>> driver_browser.open('http://launchpad.test/firefox/trunk')
-    >>> print driver_browser.title
+    >>> print(driver_browser.title)
     Series trunk : Mozilla Firefox
-    >>> print extract_text(find_tag_by_id(
-    ...     driver_browser.contents, 'series-trunk'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     driver_browser.contents, 'series-trunk')))
     Version                   Expected  Released                   Summary
     Mozilla Firefox 0.9.2...  Set date  Change details 2004-10-16
     ...
@@ -109,17 +109,17 @@ A user with launchpad.Edit rights for a release can see the delete link and
 access the delete page. Sample Person is the driver so they have those rights.
 
     >>> driver_browser.getLink('0.9.2').click()
-    >>> print driver_browser.title
+    >>> print(driver_browser.title)
     0.9.2 "One (secure) Tree Hill" : Mozilla Firefox
 
     >>> driver_browser.getLink('Delete milestone').click()
-    >>> print driver_browser.title
+    >>> print(driver_browser.title)
     Delete Mozilla Firefox 0.9.2...
 
 The 0.9.2 release has a release and files associated with it. Sample
 Person reads that they will be deleted too.
 
-    >>> print extract_text(find_main_content(driver_browser.contents))
+    >>> print(extract_text(find_main_content(driver_browser.contents)))
     Delete Mozilla Firefox 0.9.2 "One (secure) Tree Hill"
     ...
     The associated 0.9.2 release "One (secure) Tree Hill" and its files
@@ -130,13 +130,13 @@ Sample Person chooses the delete button, then reads that the action is
 successful.
 
     >>> driver_browser.getControl('Delete Milestone').click()
-    >>> print driver_browser.title
+    >>> print(driver_browser.title)
     Series trunk : Mozilla Firefox
 
     >>> print_feedback_messages(driver_browser.contents)
     Milestone 0.9.2 deleted.
-    >>> print extract_text(find_tag_by_id(
-    ...     driver_browser.contents, 'series-trunk'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     driver_browser.contents, 'series-trunk')))
     Version                Expected   Released     Summary
     Mozilla Firefox 0.9.1 ...
     Mozilla Firefox 0.9 ...
diff --git a/lib/lp/registry/stories/milestone/xx-milestone-description.txt b/lib/lp/registry/stories/milestone/xx-milestone-description.txt
index 27db913..d131f2b 100644
--- a/lib/lp/registry/stories/milestone/xx-milestone-description.txt
+++ b/lib/lp/registry/stories/milestone/xx-milestone-description.txt
@@ -28,7 +28,7 @@ The summary appears on the milestone index page.
 
     >>> test_browser.open('http://launchpad.test/ubuntu/+milestone/milestone1')
     >>> tag = find_tag_by_id(test_browser.contents, 'description')
-    >>> print extract_text(tag)
+    >>> print(extract_text(tag))
     Summary of first Ubuntu milestone.
 
 We can edit the summary after creating the milestone.
@@ -43,7 +43,7 @@ And see that it is indeed modified on the milestone page.
 
     >>> test_browser.open('http://launchpad.test/ubuntu/+milestone/milestone1')
     >>> tag = find_tag_by_id(test_browser.contents, 'description')
-    >>> print extract_text(tag)
+    >>> print(extract_text(tag))
     Modified summary of first Ubuntu milestone.
 
 
@@ -63,7 +63,7 @@ The summary appears on the milestone index page.
     >>> test_browser.open(
     ...     'http://launchpad.test/alsa-utils/+milestone/milestone1')
     >>> tag = find_tag_by_id(test_browser.contents, 'description')
-    >>> print extract_text(tag)
+    >>> print(extract_text(tag))
     Summary of first alsa-utils milestone.
 
 We can edit the summary after creating the milestone.
@@ -79,5 +79,5 @@ And see that it is indeed modified on the milestone page.
     >>> test_browser.open(
     ...     'http://launchpad.test/alsa-utils/+milestone/milestone1')
     >>> tag = find_tag_by_id(test_browser.contents, 'description')
-    >>> print extract_text(tag)
+    >>> print(extract_text(tag))
     Modified summary of first alsa-utils milestone.
diff --git a/lib/lp/registry/stories/object/xx-karmacontext-topcontributors.txt b/lib/lp/registry/stories/object/xx-karmacontext-topcontributors.txt
index cacfde8..1e99a5e 100644
--- a/lib/lp/registry/stories/object/xx-karmacontext-topcontributors.txt
+++ b/lib/lp/registry/stories/object/xx-karmacontext-topcontributors.txt
@@ -19,7 +19,7 @@ The top contributors page can be reached from the top contributors portlet.
     True
 
     >>> anon_browser.getLink('More contributors').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Top Ubuntu Contributors...
 
 
@@ -34,7 +34,7 @@ The top contributors page can be reached from the top contributors portlet.
     True
 
     >>> anon_browser.getLink('More contributors').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Top Mozilla Firefox Contributors...
 
 
@@ -49,5 +49,5 @@ The top contributors page can be reached from the top contributors portlet.
     True
 
     >>> anon_browser.getLink('More contributors').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Top The Mozilla Project Contributors...
diff --git a/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.txt b/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.txt
index 746dcc6..dcccb46 100644
--- a/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.txt
+++ b/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.txt
@@ -22,9 +22,9 @@ deletion succeeds and the usual informational message is displayed.
     >>> first_browser.getControl('Unlink').click()
     >>> content = first_browser.contents
     >>> for tag in find_tags_by_class(content, 'error'):
-    ...     print extract_text(tag)
+    ...     print(extract_text(tag))
     >>> for tag in find_tags_by_class(content, 'informational'):
-    ...     print extract_text(tag)
+    ...     print(extract_text(tag))
     Removed upstream association between alsa-utils trunk series and Warty.
 
 A few minutes later, the user sees the same packaging association in the
@@ -37,7 +37,7 @@ succeed.
     ...     url='/ubuntu/warty/+source/alsa-utils/+remove-packaging').click()
     >>> content = second_browser.contents
     >>> for tag in find_tags_by_class(content, 'informational'):
-    ...     print extract_text(tag)
+    ...     print(extract_text(tag))
     >>> for tag in find_tags_by_class(content, 'error'):
-    ...     print extract_text(tag)
+    ...     print(extract_text(tag))
     This upstream association was deleted already.
diff --git a/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.txt b/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.txt
index ca22f47..a1f0e86 100644
--- a/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.txt
+++ b/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.txt
@@ -8,7 +8,7 @@ Any user can see the summary of the binaries built from the current version
 of the package.
 
     >>> anon_browser.open('http://launchpad.test/ubuntu/+source/pmount')
-    >>> print extract_text(find_tag_by_id(anon_browser.contents, 'summary'))
+    >>> print(extract_text(find_tag_by_id(anon_browser.contents, 'summary')))
     pmount: pmount shortdesc
 
 This page includes a table that lists all the releases of this source
@@ -17,7 +17,7 @@ source package in each series of this distribution.
 
     >>> anon_browser.open('http://launchpad.test/ubuntu/+source/alsa-utils')
     >>> content = anon_browser.contents
-    >>> print extract_text(find_tag_by_id(content, 'packages_list'))
+    >>> print(extract_text(find_tag_by_id(content, 'packages_list')))
     The Hoary Hedgehog Release (active development)     Set upstream link
       1.0.9a-4ubuntu1 release (main) 2005-09-15
     The Warty Warthog Release (current stable release) alsa-utils trunk series
@@ -35,7 +35,7 @@ packaging links.
     >>> user_browser.open('http://launchpad.test/ubuntu/+source/alsa-utils')
     >>> link = user_browser.getLink(
     ...     url='/ubuntu/warty/+source/alsa-utils/+remove-packaging')
-    >>> print link
+    >>> print(link)
     <Link text='Remove upstream link'...
 
 This button is not displayed to anonymous users.
@@ -54,11 +54,11 @@ Clicking this button deletes the corresponding packaging association.
     >>> user_browser.getControl('Unlink').click()
     >>> content = user_browser.contents
     >>> for tag in find_tags_by_class(content, 'error'):
-    ...     print extract_text(tag)
+    ...     print(extract_text(tag))
     >>> for tag in find_tags_by_class(content, 'informational'):
-    ...     print extract_text(tag)
+    ...     print(extract_text(tag))
     Removed upstream association between alsa-utils trunk series and Warty.
-    >>> print extract_text(find_tag_by_id(content, 'packages_list'))
+    >>> print(extract_text(find_tag_by_id(content, 'packages_list')))
     The Hoary Hedgehog Release (active development)     Set upstream link
       1.0.9a-4ubuntu1 release (main) 2005-09-15
     The Warty Warthog Release (current stable release)  Set upstream link
diff --git a/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt b/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt
index 7f33abc..0457b89 100644
--- a/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt
+++ b/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt
@@ -15,7 +15,7 @@ and sees that pmount is not linked.
 
     >>> user_browser.open(
     ...     'http://launchpad.test/ubuntu/hoary/+needs-packaging')
-    >>> print extract_text(find_tag_by_id(user_browser.contents, 'packages'))
+    >>> print(extract_text(find_tag_by_id(user_browser.contents, 'packages')))
     Source Package      Bugs    Translations
     pmount          No bugs     64 strings ...
 
@@ -23,8 +23,8 @@ They look at the pmount source package page in Hoary and read that the
 upstream project is not set.
 
     >>> user_browser.getLink('pmount').click()
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'no-upstreams'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'no-upstreams')))
     Launchpad...
     There are no projects registered in Launchpad that are a potential
     match for this source package. Can you help us find one?
@@ -42,16 +42,16 @@ project. They set the upstream packaging link and see that it is set.
     >>> user_browser.getControl('Continue').click()
     >>> user_browser.getControl(name='field.productseries').value = ['trunk']
     >>> user_browser.getControl("Change").click()
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'upstreams'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'upstreams')))
     The Mozilla Project...Mozilla Thunderbird...trunk...
 
 They see the "Show upstream links" link and take a look at the project's
 packaging in distributions.
 
     >>> user_browser.getLink('Show upstream links').click()
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'distribution-series'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'distribution-series')))
     Distribution series  Source package  Version  Project series
     Hoary (5.04)         pmount          0.1-2    Mozilla Thunderbird trunk...
 
@@ -61,8 +61,8 @@ link to all versions and follows it to the distro source package page.
     >>> user_browser.getLink('pmount').click()
     >>> user_browser.getLink(
     ...     'All versions of pmount source in Ubuntu').click()
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'packages_list'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'packages_list')))
     The Hoary Hedgehog Release (active development) ...
       0.1-2  release (main) 2005-08-24
 
@@ -79,15 +79,15 @@ step of the multistep form.
     >>> user_browser.getControl(
     ...     'Register the upstream project').selected = True
     >>> user_browser.getControl("Link to Upstream Project").click()
-    >>> print user_browser.getControl(name='field.name').value
+    >>> print(user_browser.getControl(name='field.name').value)
     bonkers
-    >>> print user_browser.getControl(name='field.display_name').value
+    >>> print(user_browser.getControl(name='field.display_name').value)
     Bonkers
-    >>> print user_browser.getControl(name='field.summary').value
+    >>> print(user_browser.getControl(name='field.summary').value)
     summary for flubber-bin
     summary for flubber-lib
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'step-title'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'step-title')))
     Step 2 (of 2): Check for duplicate projects
 
 When No Privileges Person selects "Choose another upstream project" and
@@ -99,19 +99,19 @@ then finds out that the project doesn't exist, they use the
     >>> user_browser.getControl(
     ...     'Choose another upstream project').selected = True
     >>> user_browser.getControl("Link to Upstream Project").click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://launchpad.test/youbuntu/busy/+source/bonkers/+edit-packaging
 
     >>> user_browser.getLink("Register the upstream project").click()
-    >>> print user_browser.getControl(name='field.name').value
+    >>> print(user_browser.getControl(name='field.name').value)
     bonkers
-    >>> print user_browser.getControl(name='field.display_name').value
+    >>> print(user_browser.getControl(name='field.display_name').value)
     Bonkers
-    >>> print user_browser.getControl(name='field.summary').value
+    >>> print(user_browser.getControl(name='field.summary').value)
     summary for flubber-bin
     summary for flubber-lib
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'step-title'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'step-title')))
     Step 2 (of 2): Check for duplicate projects
 
 After No Privileges Person selects the licences, the user is redirected back
@@ -120,14 +120,14 @@ to the source package page and an informational message will be displayed.
     >>> user_browser.getControl(name='field.licenses').value = ['BSD']
     >>> user_browser.getControl(
     ...     "Complete registration and link to bonkers package").click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://launchpad.test/youbuntu/busy/+source/bonkers
     >>> for tag in find_tags_by_class(
     ...     user_browser.contents, 'informational message'):
-    ...     print extract_text(tag)
+    ...     print(extract_text(tag))
     Linked Bonkers project to bonkers source package.
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'upstreams'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'upstreams')))
     Bonkers ⇒ trunk
     Change upstream link
     Remove upstream link...
diff --git a/lib/lp/registry/stories/person/xx-add-sshkey.txt b/lib/lp/registry/stories/person/xx-add-sshkey.txt
index f3d42bc..47e4140 100644
--- a/lib/lp/registry/stories/person/xx-add-sshkey.txt
+++ b/lib/lp/registry/stories/person/xx-add-sshkey.txt
@@ -11,19 +11,19 @@ Profile page
 User keys are shown on profile pages, and any user can see that.
 
     >>> anon_browser.open('http://launchpad.test/~name12')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Sample Person in Launchpad
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'sshkeys'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'sshkeys')))
     SSH keys: andrew@trogdor
 
 Salgado does not have a key, so we omit the 'SSH keys' section for anonymous
 users.
 
     >>> anon_browser.open('http://launchpad.test/~salgado')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Guilherme Salgado in Launchpad
-    >>> print find_tag_by_id(anon_browser.contents, 'sshkeys')
+    >>> print(find_tag_by_id(anon_browser.contents, 'sshkeys'))
     None
 
 Salgado sees a message explaining that he can register his ssh key.
@@ -37,10 +37,10 @@ Salgado sees a message explaining that he can register his ssh key.
     >>> logout()
     >>> browser = setupBrowserFreshLogin(salgado)
     >>> browser.open('http://launchpad.test/~salgado')
-    >>> print browser.title
+    >>> print(browser.title)
     Guilherme Salgado in Launchpad
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'sshkeys'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'sshkeys')))
     SSH keys: Add an SSH key
     No SSH keys registered.
 
@@ -53,7 +53,7 @@ an already uploaded one. Salgado sees a link on his profile page to update
 his SSH keys. The page allows him to add a key.
 
     >>> browser.getLink('Add an SSH key').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Change your SSH keys...
 
 Any key must be of the form "keytype keytext comment", where keytype must be
@@ -65,21 +65,21 @@ message will be shown.
     >>> browser.getControl(name='sshkey').value = sshkey
     >>> browser.getControl('Import Public Key').click()
     >>> for tag in find_main_content(browser.contents)('p', 'error message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Invalid public key
 
     >>> sshkey = "ssh-rsa foo"
     >>> browser.getControl(name='sshkey').value = sshkey
     >>> browser.getControl('Import Public Key').click()
     >>> for tag in find_main_content(browser.contents)('p', 'error message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Invalid public key
 
     >>> sshkey = "ssh-xsa foo comment"
     >>> browser.getControl(name='sshkey').value = sshkey
     >>> browser.getControl('Import Public Key').click()
     >>> for tag in find_main_content(browser.contents)('p', 'error message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Invalid public key
 
 
@@ -91,7 +91,7 @@ format.
     >>> browser.getControl('Import Public Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     SSH public key added.
 
     >>> sshkey = "ssh-dss AAAAB3NzaC1kc3MAAAEBAObOoy3fScSSQPHE/V6tPGoFzo5y1JRjDLcs8CNcvIHh9L27Qdj6h18AXn6MUCvvSCKm49aHpp1Xe14a6fmEIesjz6VopPWGENaOwRmhH6zfqM6imKUXQ0sq9p0znYb0TMjyRC0/AmqYneUF6FA2mVXygkGAkp/vDRPFQhPwnHpVD9TVPxHBQdHgM3bTo2TT+GoL7kw/s32ZiAH4OPvN5fN7bCkQWoUs/ySfoNbISMdvdtq07Rra2Biwzgjjs0ZcKbMicbDyYCe4gXlqK4wqcDfcwgrdqdG6NM0LUdekarWjnv0pMb6ttUl4U7e7Nf+eGkiTVItlppC8DyrnqC9SKCUAAAAVAOlEYNobJottyObVWQcrU8eAP4T5AAABAQDmJmL4DcQ1GVvw1Pjy57V0WUyGrOVBRVz7BwYBIvMA7xJCCvzd47mYWrWJkjV6O3tw2vG5oZx+BXE+ve8O6jL89CrwqncoUS8WHCojRmuUHTmtCCiRBCH+/68HMCusO3Blk//kQSsaqfIn+8Xa56Vr2SweSUlLgjvb51+89JJ13oDlUvdftW2GZu+grbmojqcoJ1LVAI5n0qsDItsFid46f8XfNzPeksasY9JbY5fKq/xf1KcgXL2F9XwmrDjFCuI4/xkJWNfGwaLKC/cbrJ1xmvPLl1/Hm5kNqgrwpNwHVOwyYSCUqXroU5PnpE9uydHUhjhtU2K2Hj0i7fOyxoxyAAABAQCpXKgd6lpTAEKm7ECY3TbJaTXPkNvAwg/2ud+PrtefHrVFFWrXrblSQhnmnc6ut8G3BsDzCljAIV2v+XcdOo+m8EViLf+Bi+gfbAIz4vdVepwQ2XHWUOTKk90i7Xqg4mUUDRIVw9ioNF0GAHbNlJTK3FWC3gstbCJU2hyV3UzgB95b6zqpUHeyn1RK4VAFYGY9fCIdZNy926HEart6uO/N6cO1ETw5B63kI8fTBjU7HLGgGXRjOv1APAqvKgry3tQD2WYkVJGRyYLjDK9d8nStUpwN5swI1xx2IWAbD+UCsRXAixn8s3mvpBD/jbnWjrzEensBc96jtiAsx2P5oXEd salgado@canario"
@@ -99,7 +99,7 @@ format.
     >>> browser.getControl('Import Public Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     SSH public key added.
 
     >>> sshkey = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJseCUmxVG7D6qh4JmhLp0Du4kScScJ9PtZ0LGHYHaURnRw9tbX1wwURAio8og6dbnT75CQ3TbUE/xJhxI0aFXE= salgado@canario"
@@ -107,7 +107,7 @@ format.
     >>> browser.getControl('Import Public Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     SSH public key added.
 
     >>> sshkey = "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBDUR0E0zCHRHJER6uzjfE/o0HAHFLcq/n8lp0duThpeIPsmo+wr3vHHuAAyOddOgkuQC8Lj8FzHlrOEYgXL6qa7FvpviE9YWUgmqVDa/yJbL/m6Mg8fvSIXlDJKmvOSv6g== salgado@canario"
@@ -115,7 +115,7 @@ format.
     >>> browser.getControl('Import Public Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     SSH public key added.
 
     >>> sshkey = "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAB3rpD+Ozb/kwUOqCZUXSiruAkIx6sNZLJyjJ0zxVTZSannaysCLxMQ/IiVxCd59+U2NaLduMzd93JcYDRlX3M5+AApY+3JjfSPo01Sb17HTLNSYU3RZWx0A3XJxm/YN+x/iuYZ3IziuAKeYMsNsdfHlO4/IWjw4Ruy0enW+QhWaY2qAQ== salgado@canario"
@@ -123,7 +123,7 @@ format.
     >>> browser.getControl('Import Public Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     SSH public key added.
 
 Launchpad administrators are not allowed to poke at other user's ssh keys.
@@ -141,9 +141,9 @@ Salgado chooses to remove one of his ssh keys from Launchpad. The link
 to edit his keys is on the page.
 
     >>> browser.open('http://launchpad.test/~salgado')
-    >>> print browser.title
+    >>> print(browser.title)
     Guilherme Salgado in Launchpad
-    >>> print extract_text(find_tag_by_id(browser.contents, 'sshkeys'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'sshkeys')))
     SSH keys: Update SSH keys
     salgado@canario
     salgado@canario
@@ -151,13 +151,13 @@ to edit his keys is on the page.
     salgado@canario
     salgado@canario
     >>> browser.getLink('Update SSH keys').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Change your SSH keys...
 
     >>> browser.getControl('Remove', index=0).click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Key ... removed
 
 If Salgado tries to remove a key that doesn't exist or one that doesn't
@@ -167,7 +167,7 @@ belong to him, it will fail with an error message.
     >>> browser.getControl('Remove', index=0).click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'error message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Cannot remove a key that doesn't exist
 
     >>> browser.getControl(name='key', index=0).value = '1'
diff --git a/lib/lp/registry/stories/person/xx-admin-person-review.txt b/lib/lp/registry/stories/person/xx-admin-person-review.txt
index 1173f52..58c7a88 100644
--- a/lib/lp/registry/stories/person/xx-admin-person-review.txt
+++ b/lib/lp/registry/stories/person/xx-admin-person-review.txt
@@ -10,10 +10,10 @@ Registry admins can review users and update some of their information.
 
     >>> expert_browser.open('http://launchpad.test/~salgado')
     >>> expert_browser.getLink('Administer').click()
-    >>> print expert_browser.url
+    >>> print(expert_browser.url)
     http://launchpad.test/~salgado/+review
 
-    >>> print expert_browser.title
+    >>> print(expert_browser.title)
     Review person...
 
     >>> expert_browser.getControl('Name', index=0).value = 'no-way'
@@ -21,7 +21,7 @@ Registry admins can review users and update some of their information.
     >>> expert_browser.getControl(
     ...     name='field.personal_standing_reason').value = 'good guy'
     >>> expert_browser.getControl('Change').click()
-    >>> print expert_browser.url
+    >>> print(expert_browser.url)
     http://launchpad.test/~no-way
 
 Registry experts can't change the displayname.
@@ -40,9 +40,9 @@ But Launchpad admins can.
     >>> admin_browser.getControl(
     ...     'Display Name', index=0).value = 'The one and only Salgado'
     >>> admin_browser.getControl('Change').click()
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     The one and only Salgado in Launchpad
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://launchpad.test/~salgado
 
 
@@ -53,7 +53,7 @@ The review page has a link to the review account page.
 
     >>> expert_browser.open('http://launchpad.test/~salgado')
     >>> expert_browser.getLink('Administer Account').click()
-    >>> print expert_browser.title
+    >>> print(expert_browser.title)
     Review person's account...
 
 The +reviewaccount page displays account information that is normally
@@ -61,7 +61,7 @@ hidden from the UI.
 
     >>> content = find_main_content(expert_browser.contents)
     >>> for tr in content.find(id='summary').findAll('tr'):
-    ...     print extract_text(tr)
+    ...     print(extract_text(tr))
     Created: 2005-06-06
     Creation reason: Created by the owner themselves, coming from Launchpad.
     OpenID identifiers: salgado_oid
@@ -72,5 +72,5 @@ hidden from the UI.
 The page also contains a link back to the +review page.
 
     >>> link = expert_browser.getLink(url='+review')
-    >>> print link.text
+    >>> print(link.text)
     edit[IMG] Review the user's Launchpad information
diff --git a/lib/lp/registry/stories/person/xx-approve-members.txt b/lib/lp/registry/stories/person/xx-approve-members.txt
index 5e1eebc..ee879bb 100644
--- a/lib/lp/registry/stories/person/xx-approve-members.txt
+++ b/lib/lp/registry/stories/person/xx-approve-members.txt
@@ -11,7 +11,7 @@ proposed members at once.
 
 Let's have a look at the proposed members that we have.
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'member-list'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'member-list')))
     Andrew Bennetts (Applied on ...)     Approve Decline Hold
     Sample Person (Applied on ...)       Approve Decline Hold
 
@@ -29,7 +29,7 @@ Person's. A comment is also sent to the applying users and the team admins.
     >>> len(stub.test_emails)
     12
     >>> for from_addr, to_addrs, raw_msg in sorted(stub.test_emails):
-    ...     print to_addrs
+    ...     print(to_addrs)
     ['andrew.bennetts@xxxxxxxxxxxxxxx']
     ['colin.watson@xxxxxxxxxxxxxxx']
     ['colin.watson@xxxxxxxxxxxxxxx']
@@ -42,7 +42,7 @@ Person's. A comment is also sent to the applying users and the team admins.
     ['mark@xxxxxxxxxxx']
     ['mark@xxxxxxxxxxx']
     ['test@xxxxxxxxxxxxx']
-    >>> print raw_msg
+    >>> print(raw_msg)
     Content-Type: text/plain; charset="utf-8"
     ...
     Mark Shuttleworth said:
@@ -54,7 +54,7 @@ as an inactive one.
 
     >>> browser.url
     'http://launchpad.test/~ubuntu-team/+members'
-    >>> print extract_text(find_tag_by_id(browser.contents, 'activemembers'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'activemembers')))
     Name Member since Expires Status
     ...
     Andrew Bennetts
diff --git a/lib/lp/registry/stories/person/xx-deactivate-account.txt b/lib/lp/registry/stories/person/xx-deactivate-account.txt
index 4267cc7..9b41ec4 100644
--- a/lib/lp/registry/stories/person/xx-deactivate-account.txt
+++ b/lib/lp/registry/stories/person/xx-deactivate-account.txt
@@ -56,8 +56,8 @@ And now the Launchpad page for Sample Person person will clearly say they
 do not use Launchpad.
 
     >>> browser.open('http://launchpad.test/~name12-deactivatedaccount')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'not-lp-user-or-team'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'not-lp-user-or-team')))
     Sample Person does not use Launchpad.
 
 The bugs that were assigned to Sample Person will no longer have an
@@ -65,7 +65,7 @@ assignee.
 
     >>> browser.open('http://launchpad.test/debian/+source/'
     ...     'mozilla-firefox/+bug/3')
-    >>> print extract_text(find_main_content(browser.contents))
+    >>> print(extract_text(find_main_content(browser.contents)))
     Bug Title Test
     ...
     Assigned to
@@ -76,7 +76,7 @@ Although teams have NOACCOUNT as their account_status, they are teams and so
 it makes no sense to say they don't use Launchpad.
 
     >>> browser.open('http://launchpad.test/~ubuntu-team')
-    >>> print find_tag_by_id(browser.contents, 'not-lp-user-or-team')
+    >>> print(find_tag_by_id(browser.contents, 'not-lp-user-or-team'))
     None
 
 The action of deactivating an account is something that can only be done by
diff --git a/lib/lp/registry/stories/person/xx-people-index.txt b/lib/lp/registry/stories/person/xx-people-index.txt
index e09d112..ea6f71d 100644
--- a/lib/lp/registry/stories/person/xx-people-index.txt
+++ b/lib/lp/registry/stories/person/xx-people-index.txt
@@ -1,9 +1,9 @@
 
   Test /people.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /people HTTP/1.1
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   Content-Length: ...
   Content-Type: text/html;charset=utf-8
diff --git a/lib/lp/registry/stories/person/xx-people-search.txt b/lib/lp/registry/stories/person/xx-people-search.txt
index 702092b..c9b4b9a 100644
--- a/lib/lp/registry/stories/person/xx-people-search.txt
+++ b/lib/lp/registry/stories/person/xx-people-search.txt
@@ -5,7 +5,7 @@ Searching for people
 --------------------
 
     >>> browser.open('http://launchpad.test/people')
-    >>> print browser.title
+    >>> print(browser.title)
     People and teams in Launchpad
 
 Search for all people and teams with the string "foo bar".  There
@@ -14,13 +14,13 @@ should just be the one person named "Foo Bar" found.
     >>> browser.getControl(name='name').value = 'foo bar'
     >>> browser.getControl('Search').click()
     >>> listing = find_tag_by_id(browser.contents, 'people-results')
-    >>> print extract_text(listing)
+    >>> print(extract_text(listing))
     Name          Launchpad ID  Karma
     Foo Bar       name16        241
 
 The listing is sortable.
 
-    >>> print ' '.join(listing['class'])
+    >>> print(' '.join(listing['class']))
     listing sortable
 
 Search for all people and teams like "launchpad" the users sees three
@@ -29,7 +29,7 @@ columns of people and teams..
     >>> browser.getControl(name='name').value = 'launchpad'
     >>> browser.getControl('Search').click()
     >>> listing = find_tag_by_id(browser.contents, 'people-results')
-    >>> print extract_text(listing, formatter='html')
+    >>> print(extract_text(listing, formatter='html'))
     Name                         Launchpad ID              Karma
     Julian Edwards               launchpad-julian-edwards  0
     Launchpad Administrators     admins                    &mdash;
@@ -45,7 +45,7 @@ shown. There are only two columns because teams cannot have karma.
     >>> browser.getControl(name='searchfor').value = ['teamsonly']
     >>> browser.getControl('Search').click()
     >>> listing = find_tag_by_id(browser.contents, 'people-results')
-    >>> print extract_text(listing)
+    >>> print(extract_text(listing))
     Name                         Launchpad ID
     Launchpad Administrators     admins
     Launchpad Beta Testers       launchpad-beta-testers
@@ -59,7 +59,7 @@ Restrict the search to people and only individuals are listed.
     >>> browser.getControl(name='searchfor').value = ['peopleonly']
     >>> browser.getControl('Search').click()
     >>> listing = find_tag_by_id(browser.contents, 'people-results')
-    >>> print extract_text(listing)
+    >>> print(extract_text(listing))
     Name                         Launchpad ID              Karma
     Julian Edwards               launchpad-julian-edwards  0
     Launchpad Beta Testers Owner launchpad-beta-owner      0
diff --git a/lib/lp/registry/stories/person/xx-person-claim-merge.txt b/lib/lp/registry/stories/person/xx-person-claim-merge.txt
index 1b723b6..caad3d3 100644
--- a/lib/lp/registry/stories/person/xx-person-claim-merge.txt
+++ b/lib/lp/registry/stories/person/xx-person-claim-merge.txt
@@ -7,7 +7,7 @@ exception that will redirect the user to the login page.
 
     >>> anon_browser.open('http://launchpad.test/~matsubara')
     >>> link = anon_browser.getLink("Are you Diogo Matsubara?")
-    >>> print link.url
+    >>> print(link.url)
     http://launchpad.test/people/+requestmerge?field.dupe_person=matsubara
     >>> link.click()
     Traceback (most recent call last):
diff --git a/lib/lp/registry/stories/person/xx-person-edit-jabber-ids.txt b/lib/lp/registry/stories/person/xx-person-edit-jabber-ids.txt
index ae19df1..f2a6112 100644
--- a/lib/lp/registry/stories/person/xx-person-edit-jabber-ids.txt
+++ b/lib/lp/registry/stories/person/xx-person-edit-jabber-ids.txt
@@ -13,7 +13,7 @@ profile page and uses the 'Update Jabber IDs' link.
 
     >>> user_browser.open('http://launchpad.test/~no-priv')
     >>> user_browser.getLink('Update Jabber IDs').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     No Privileges Person's Jabber IDs...
 
 The user enters the Jabber ID in the text field and clicks on the
@@ -29,7 +29,7 @@ an error is displayed and the user can enter another one:
 
     >>> def show_errors(browser):
     ...     for error in find_tags_by_class(browser.contents, 'error'):
-    ...         print extract_text(error)
+    ...         print(extract_text(error))
     >>> show_errors(user_browser)
     There is 1 error.
     New Jabber user ID:
@@ -46,7 +46,7 @@ it will be associated with their account.
     >>> def show_jabberids(browser):
     ...     tags = find_tag_by_id(browser.contents, 'jabber-ids')
     ...     for dd in tags.findAll('dd'):
-    ...         print extract_text(dd)
+    ...         print(extract_text(dd))
 
     >>> show_jabberids(user_browser)
     no-priv@xxxxxxxxxx
diff --git a/lib/lp/registry/stories/person/xx-person-edit.txt b/lib/lp/registry/stories/person/xx-person-edit.txt
index 54b12b6..4769324 100644
--- a/lib/lp/registry/stories/person/xx-person-edit.txt
+++ b/lib/lp/registry/stories/person/xx-person-edit.txt
@@ -23,8 +23,8 @@ Launchpad.
 They can see a link to an FAQ that explains launchpad accounts and passwords.
 Note that this is a link to an FAQ in production databases.
 
-    >>> print browser.getLink(
-    ...     'Learn about your Launchpad account and password').url
+    >>> print(browser.getLink(
+    ...     'Learn about your Launchpad account and password').url)
     http://launchpad.test/launchpad/+faq/51
 
 They noticed that their information is out of date and will update it.
diff --git a/lib/lp/registry/stories/person/xx-person-editgpgkeys-invalid-key.txt b/lib/lp/registry/stories/person/xx-person-editgpgkeys-invalid-key.txt
index 352d3e9..d3f8e52 100644
--- a/lib/lp/registry/stories/person/xx-person-editgpgkeys-invalid-key.txt
+++ b/lib/lp/registry/stories/person/xx-person-editgpgkeys-invalid-key.txt
@@ -38,7 +38,7 @@ Attempts to claim a revoked OpenPGP key fail:
     ...     '84D205F03E1E67096CB54E262BE83793AACCD97C')
     >>> browser.getControl('Import Key').click()
     >>> for tag in find_tags_by_class(browser.contents, 'error message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     <BLANKLINE>
     The key 84D205F03E1E67096CB54E262BE83793AACCD97C cannot be validated
     because it has been publicly revoked.
@@ -54,7 +54,7 @@ Attempts to claim an expired OpenPGP key also fail:
     ...     '0DD64D28E5F41138533495200E3DB4D402F53CC6')
     >>> browser.getControl('Import Key').click()
     >>> for tag in find_tags_by_class(browser.contents, 'error message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     <BLANKLINE>
     The key 0DD64D28E5F41138533495200E3DB4D402F53CC6 cannot be validated
     because it has expired. Change the expiry date (in a terminal, enter
@@ -89,7 +89,7 @@ Try to validate the revoked OpenPGP key:
     ...     'http://launchpad.test/token/%s/+validategpg' % revoked_key_token)
     >>> browser.getControl('Continue').click()
     >>> for tag in find_tags_by_class(browser.contents, 'error message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     There is 1 error.
     The key 84D205F03E1E67096CB54E262BE83793AACCD97C cannot be validated
     because it has been publicly revoked.
@@ -105,7 +105,7 @@ Try to validate the revoked OpenPGP key:
     ...     'http://launchpad.test/token/%s/+validategpg' % expired_key_token)
     >>> browser.getControl('Continue').click()
     >>> for tag in find_tags_by_class(browser.contents, 'error message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     There is 1 error.
     The key 0DD64D28E5F41138533495200E3DB4D402F53CC6 cannot be validated
     because it has expired. Change the expiry date (in a terminal, enter
diff --git a/lib/lp/registry/stories/person/xx-person-home.txt b/lib/lp/registry/stories/person/xx-person-home.txt
index ecc4958..e1de161 100644
--- a/lib/lp/registry/stories/person/xx-person-home.txt
+++ b/lib/lp/registry/stories/person/xx-person-home.txt
@@ -11,7 +11,7 @@ profile for them.
     'Diogo Matsubara does not use Launchpad'
 
     >>> content = find_main_content(browser.contents).find('p')
-    >>> print extract_text(content)
+    >>> print(extract_text(content))
     Diogo Matsubara does not use Launchpad. This page was created on
     2006-12-13 when importing the Portuguese...
 
@@ -23,31 +23,31 @@ Mark has a registered email address, and he has chosen to disclose it to
 the world. Anonymous users cannot see Mark's address
 
     >>> anon_browser.open('http://launchpad.test/~mark')
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'email-addresses'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'email-addresses')))
     Email: Log in for email information.
 
 A logged in user such as Sample Person, can see Mark's addresses.
 
     >>> sample_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
     >>> sample_browser.open('http://launchpad.test/~mark')
-    >>> print extract_text(
-    ...     find_tag_by_id(sample_browser.contents, 'email-addresses'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(sample_browser.contents, 'email-addresses')))
     Email: mark@xxxxxxxxxxx
 
 As for Sample Person, they have chosen not to disclose their email addresses.
 Unprivileged users like No Privileges Person cannot see their addresses:
 
     >>> user_browser.open('http://launchpad.test/~name12')
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'email-addresses'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'email-addresses')))
     Email: No public address provided.
 
 But Foo Bar can:
 
     >>> admin_browser.open('http://launchpad.test/~name12')
-    >>> print extract_text(
-    ...     find_tag_by_id(admin_browser.contents, 'email-addresses'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'email-addresses')))
     Email:
     test@xxxxxxxxxxxxx
     testing@xxxxxxxxxxxxx
@@ -61,25 +61,25 @@ Open ID link
 When a person visits their own page, they'll see their OpenID login URL.
 
     >>> user_browser.open('http://launchpad.test/~no-priv')
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'openid-info'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'openid-info')))
     OpenID login:
     http://launchpad.test/~no-priv...
 
 The URL is followed by a helpful link.
 
-    >>> print user_browser.getLink('OpenID help').url
+    >>> print(user_browser.getLink('OpenID help').url)
     http://launchpad.test/+help-registry/openid.html
 
 However, when the user visits someone else's page, they see no such URL.
 
     >>> user_browser.open('http://launchpad.test/~salgado')
-    >>> print find_tag_by_id(user_browser.contents, 'openid-info')
+    >>> print(find_tag_by_id(user_browser.contents, 'openid-info'))
     None
 
 And there is no helpful link.
 
-    >>> print user_browser.getLink('openid help').url
+    >>> print(user_browser.getLink('openid help').url)
     Traceback (most recent call last):
     ...
     LinkNotFoundError
@@ -91,13 +91,13 @@ Jabber IDs
 A person's jabber IDs are only show to authenticated users.
 
     >>> user_browser.open('http://launchpad.test/~mark')
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'jabber-ids'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'jabber-ids')))
     Jabber: markshuttleworth@xxxxxxxxxx
 
     >>> anon_browser.open('http://launchpad.test/~mark')
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'jabber-ids'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'jabber-ids')))
     Jabber: &lt;email address hidden&gt;
 
 
@@ -109,12 +109,12 @@ just by following the link to that person's OpenPGP keys, only
 authenticated users can see the key fingerprint with a link to the keyserver.
 
     >>> user_browser.open('http://launchpad.test/~name16')
-    >>> print find_tag_by_id(user_browser.contents, 'pgp-keys')
+    >>> print(find_tag_by_id(user_browser.contents, 'pgp-keys'))
     <dl...
     <a href="https://keyserver...
 
     >>> anon_browser.open('http://launchpad.test/~name16')
-    >>> print find_tag_by_id(anon_browser.contents, 'pgp-keys')
+    >>> print(find_tag_by_id(anon_browser.contents, 'pgp-keys'))
     <dl...
     <dd> ABCDEF0123456789ABCDDCBA0000111112345678...
 
@@ -126,7 +126,7 @@ The contact details portlet shows the languages that the user speaks. No
 Privileges Person can see the languages that mark speaks.
 
     >>> user_browser.open('http://launchpad.test/~carlos')
-    >>> print extract_text(find_tag_by_id(user_browser.contents, 'languages'))
+    >>> print(extract_text(find_tag_by_id(user_browser.contents, 'languages')))
     Languages:
     Catalan, English, Spanish
 
@@ -134,7 +134,7 @@ When viewing their own page, No Privileges Person sees their languages and
 can edit them.
 
     >>> user_browser.open('http://launchpad.test/~no-priv')
-    >>> print extract_text(find_tag_by_id(user_browser.contents, 'languages'))
+    >>> print(extract_text(find_tag_by_id(user_browser.contents, 'languages')))
     Languages: Set preferred languages
     English
 
@@ -145,36 +145,36 @@ Summary Pagelets
 A person's homepage also lists Karma and Time zone information:
 
     >>> browser.open('http://launchpad.test/~mark')
-    >>> print extract_text(find_tag_by_id(browser.contents, 'karma'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'karma')))
     Karma: 130 Karma help
 
     >>> browser.open('http://launchpad.test/~ddaa')
-    >>> print extract_text(find_tag_by_id(browser.contents, 'timezone'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'timezone')))
     Time zone: UTC (UTC+0000)
 
 Negative Ubuntu Code of Conduct signatory status is only displayed for
 yourself; others won't see it:
 
-    >>> print find_tag_by_id(browser.contents, 'ubuntu-coc')
+    >>> print(find_tag_by_id(browser.contents, 'ubuntu-coc'))
     None
 
     >>> browser = setupBrowser(auth='Basic mark@xxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/~mark')
-    >>> print extract_text(find_tag_by_id(browser.contents, 'ubuntu-coc'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'ubuntu-coc')))
     Signed Ubuntu Code of Conduct: No
 
 You can grab certain bits of information programatically:
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'karma-total'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'karma-total')))
     130
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'member-since'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'member-since')))
     2005-06-06
 
 Teams don't have member-since; they have created-date:
 
     >>> browser.open('http://launchpad.test/~guadamen')
-    >>> print extract_text(find_tag_by_id(browser.contents, 'created-date'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'created-date')))
     2005-06-06
 
 
@@ -188,11 +188,11 @@ most active and also the areas in which they worked on each project.
     >>> anon_browser.open('http://launchpad.test/~name16')
     >>> table = find_tag_by_id(anon_browser.contents, 'contributions')
     >>> for tr in table.findAll('tr'):
-    ...     print tr.find('th').find('a').renderContents()
+    ...     print(tr.find('th').find('a').renderContents())
     ...     for td in tr.findAll('td'):
     ...         img = td.find('img')
     ...         if img is not None:
-    ...             print "\t", img['title']
+    ...             print("\t", img['title'])
     Evolution
        Bug Management
        Translations in Rosetta
@@ -214,13 +214,13 @@ If the person hasn't made any contributions, the table is not present in
 its page.
 
     >>> anon_browser.open('http://launchpad.test/~jdub')
-    >>> print find_tag_by_id(anon_browser.contents, 'contributions')
+    >>> print(find_tag_by_id(anon_browser.contents, 'contributions'))
     None
 
 The same for teams.
 
     >>> anon_browser.open('http://launchpad.test/~ubuntu-team')
-    >>> print find_tag_by_id(anon_browser.contents, 'contributions')
+    >>> print(find_tag_by_id(anon_browser.contents, 'contributions'))
     None
 
 
@@ -232,10 +232,10 @@ were imported into Launchpad. Any user can see an unclaimed profile and
 a link to request a claim the profile.
 
     >>> anon_browser.open('https://launchpad.test/~jvprat')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Jordi Vilalta does not use Launchpad
 
-    >>> print extract_text(find_main_content(anon_browser.contents))
+    >>> print(extract_text(find_main_content(anon_browser.contents)))
     Jordi Vilalta does not use Launchpad. This page was created on ...
     when importing the Catalan (ca) translation of pmount in Ubuntu Hoary...
 
@@ -257,16 +257,16 @@ users cannot see this, but admins like Foo Bar can.
     >>> logout()
 
     >>> anon_browser.open('https://launchpad.test/~jvprat')
-    >>> print find_tag_by_id(anon_browser.contents, 'email-addresses')
+    >>> print(find_tag_by_id(anon_browser.contents, 'email-addresses'))
     None
 
     >>> user_browser.open('https://launchpad.test/~jvprat')
-    >>> print find_tag_by_id(user_browser.contents, 'email-addresses')
+    >>> print(find_tag_by_id(user_browser.contents, 'email-addresses'))
     None
 
     >>> admin_browser.open('https://launchpad.test/~jvprat')
-    >>> print extract_text(
-    ...     find_tag_by_id(admin_browser.contents, 'email-addresses'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'email-addresses')))
     jvprat@xxxxxxxxxx
     Change email settings
 
diff --git a/lib/lp/registry/stories/person/xx-person-karma.txt b/lib/lp/registry/stories/person/xx-person-karma.txt
index db6931a..fe6181c 100644
--- a/lib/lp/registry/stories/person/xx-person-karma.txt
+++ b/lib/lp/registry/stories/person/xx-person-karma.txt
@@ -7,32 +7,32 @@ the community.  A person's current total karma is available on their
 profile page.
 
     >>> anon_browser.open('http://launchpad.test/~name12')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Sample Person in Launchpad
 
     >>> content = find_main_content(anon_browser.contents)
     >>> karma = find_tag_by_id(content, 'karma-total')
-    >>> print karma.renderContents()
+    >>> print(karma.renderContents())
     138
 
 The total karma points is also a link to the person's karma summary page.
 
     >>> anon_browser.getLink('138').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Karma : Sample Person
 
 Any user can see the categories that a user has earned karma in.
 
     >>> content = find_main_content(anon_browser.contents)
     >>> for row in find_tag_by_id(content, 'karmapoints').findAll('tr'):
-    ...     print extract_text(row)
+    ...     print(extract_text(row))
     Bug Management           94
     Specification Tracking   44
 
 Any user can see that a user has latest actions.
 
     >>> for row in find_tag_by_id(content, 'karmaactions').findAll('tr'):
-    ...     print extract_text(row)
+    ...     print(extract_text(row))
     Date        Action
     2001-11-02  Registered Specification
     2001-11-02  Registered Specification
@@ -43,7 +43,7 @@ The karma page can also show any user that a user has never earned karma.
 
     >>> anon_browser.open('http://launchpad.test/~salgado/+karma')
     >>> content = find_main_content(anon_browser.contents)
-    >>> print extract_text(find_tag_by_id(content, 'no-karma'))
+    >>> print(extract_text(find_tag_by_id(content, 'no-karma')))
     No karma has yet been assigned to Guilherme Salgado. Karma is updated
     daily.
 
@@ -52,10 +52,10 @@ a that user's last karma actions
 
     >>> anon_browser.open('http://launchpad.test/~karl/+karma')
     >>> content = find_main_content(anon_browser.contents)
-    >>> print extract_text(find_tag_by_id(content, 'no-karma'))
+    >>> print(extract_text(find_tag_by_id(content, 'no-karma')))
     Karl Tilbury's karma has expired.
 
     >>> for row in find_tag_by_id(content, 'karmaactions').findAll('tr'):
-    ...     print extract_text(row)
+    ...     print(extract_text(row))
     Date        Action
     2001-08-09  New Bug Filed
diff --git a/lib/lp/registry/stories/person/xx-person-projects.txt b/lib/lp/registry/stories/person/xx-person-projects.txt
index ea1e800..89a323c 100644
--- a/lib/lp/registry/stories/person/xx-person-projects.txt
+++ b/lib/lp/registry/stories/person/xx-person-projects.txt
@@ -8,7 +8,7 @@ team.
     >>> related_projects = find_tag_by_id(
     ...     anon_browser.contents, 'portlet-related-projects')
     >>> for tr in related_projects.findAll('tr'):
-    ...     print extract_text(tr)
+    ...     print(extract_text(tr))
     Ubuntu
     ubuntutest
     Tomcat
@@ -16,12 +16,12 @@ team.
 The +related-projects page displays a table with project names.
 
     >>> anon_browser.getLink('Show related projects').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Related projects : ...Ubuntu Team... team
 
     >>> related_projects = find_tag_by_id(
     ...     anon_browser.contents, 'related-projects')
-    >>> print extract_text(related_projects)
+    >>> print(extract_text(related_projects))
     Name            Owner   Driver      Bug Supervisor
     Ubuntu          yes     no          no
     ubuntutest      yes     no          no
@@ -32,16 +32,16 @@ A person's projects are accessible via a link on the 'Related packages' page.
 
     >>> anon_browser.open('http://launchpad.test/~mark')
     >>> anon_browser.getLink('Related packages').click()
-    >>> print anon_browser.url
+    >>> print(anon_browser.url)
     http://launchpad.test/~mark/+related-packages
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Related packages : Mark Shuttleworth
 
     >>> anon_browser.open('http://launchpad.test/~mark')
     >>> anon_browser.getLink('Related projects').click()
     >>> related_projects = find_tag_by_id(
     ...     anon_browser.contents, 'related-projects')
-    >>> print extract_text(related_projects)
+    >>> print(extract_text(related_projects))
     Name                            Owner   Driver      Bug Supervisor
     Debian                          yes     no          no
     Gentoo                          yes     no          no
diff --git a/lib/lp/registry/stories/person/xx-person-rdf.txt b/lib/lp/registry/stories/person/xx-person-rdf.txt
index 849853f..fa390ab 100644
--- a/lib/lp/registry/stories/person/xx-person-rdf.txt
+++ b/lib/lp/registry/stories/person/xx-person-rdf.txt
@@ -10,7 +10,7 @@ We export FOAF RDF metadata from the /~Person.name/+index document.
     >>> anon_browser.open("http://launchpad.test/~name16";)
     >>> strainer = SoupStrainer(['link'], {'type': ['application/rdf+xml']})
     >>> soup = BeautifulSoup(anon_browser.contents, parse_only=strainer)
-    >>> print soup.renderContents()
+    >>> print(soup.renderContents())
     <link href="+rdf" rel="meta" title="FOAF" type="application/rdf+xml"/>
 
 
@@ -21,7 +21,7 @@ And this is what the FOAF document for an individual actually looks
 like. It includes GPG information, if the user has any.
 
     >>> anon_browser.open("http://launchpad.test/~name16/+rdf";)
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     <?xml version="1.0"...?>
     <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
              xmlns:foaf="http://xmlns.com/foaf/0.1/";
@@ -45,7 +45,7 @@ like. It includes GPG information, if the user has any.
 It also includes SSH keys:
 
     >>> anon_browser.open("http://launchpad.test/~name12/+rdf";)
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     <?xml version="1.0"...?>
     ...
     <foaf:name>Sample Person</foaf:name>
@@ -57,7 +57,7 @@ And it's valid XML and RDF:
 
     >>> from xml.dom.minidom import parseString
     >>> document = parseString(str(anon_browser.contents))
-    >>> print document.documentElement.nodeName
+    >>> print(document.documentElement.nodeName)
     rdf:RDF
 
 
@@ -78,7 +78,7 @@ Now, generate the RDF itself:
 
     >>> from lp.services.helpers import backslashreplace
     >>> anon_browser.open("http://launchpad.test/~testing-spanish-team/+rdf";)
-    >>> print backslashreplace(anon_browser.contents)
+    >>> print(backslashreplace(anon_browser.contents))
     <?xml version="1.0"...?>
     <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
              xmlns:foaf="http://xmlns.com/foaf/0.1/";
@@ -118,7 +118,7 @@ present:
 
 And nothing about them is rendered at all:
 
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     <?xml version="1.0"...?>
     <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
              xmlns:foaf="http://xmlns.com/foaf/0.1/";
diff --git a/lib/lp/registry/stories/person/xx-person-subscriptions.txt b/lib/lp/registry/stories/person/xx-person-subscriptions.txt
index 669d940..8546cb1 100644
--- a/lib/lp/registry/stories/person/xx-person-subscriptions.txt
+++ b/lib/lp/registry/stories/person/xx-person-subscriptions.txt
@@ -8,7 +8,7 @@ Any user can view the direct subscriptions that a person or team has to
 blueprints, branches, bugs, merge proposals and questions.
 
     >>> anon_browser.open('http://launchpad.test/~ubuntu-team/+subscriptions')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Subscriptions : Bugs : “Ubuntu Team” team
 
 The user can see that the Ubuntu Team does not have any direct bug
@@ -39,8 +39,8 @@ Any user can see Webster's bug subscriptions.  The bug subscriptions table
 includes the bug number, title and location.
 
     >>> anon_browser.open('http://launchpad.test/~webster/+subscriptions')
-    >>> print extract_text(find_tag_by_id(
-    ...     anon_browser.contents, 'bug_subscriptions'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     anon_browser.contents, 'bug_subscriptions')))
     Summary
     In
     ...
@@ -77,7 +77,7 @@ subscribed to the bug.  They click cancel which takes them back to their
 
     >>> cancel_link = subscriber_browser.getLink('Cancel')
     >>> cancel_link.click()
-    >>> print subscriber_browser.title
+    >>> print(subscriber_browser.title)
     Subscriptions : Bugs : Webster
 
 They choose to unsubscribe from the bug about Affluenza.
@@ -87,7 +87,7 @@ They choose to unsubscribe from the bug about Affluenza.
     >>> subscriber_browser.getControl(
     ...     "unsubscribe me from this bug").selected = True
     >>> subscriber_browser.getControl("Continue").click()
-    >>> print subscriber_browser.title
+    >>> print(subscriber_browser.title)
     Subscriptions : Bugs : Webster
 
 Webster can see that the bug about Affluenza is no longer listed in their
@@ -95,8 +95,8 @@ direct bug subscriptions.
 
     >>> subscriber_browser.open(
     ...     'http://bugs.launchpad.test/~webster/+subscriptions')
-    >>> print extract_text(find_tag_by_id(
-    ...     subscriber_browser.contents, 'bug_subscriptions'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     subscriber_browser.contents, 'bug_subscriptions')))
     Summary
     In
     ...
@@ -118,11 +118,11 @@ Webster chooses to review the subscriptions for their team America.
 
     >>> subscriber_browser.open(
     ...     'http://bugs.launchpad.test/~america/+subscriptions')
-    >>> print subscriber_browser.title
+    >>> print(subscriber_browser.title)
     Subscriptions : Bugs ...
 
-    >>> print extract_text(find_tag_by_id(
-    ...     subscriber_browser.contents, 'bug_subscriptions'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     subscriber_browser.contents, 'bug_subscriptions')))
     Summary
     In
     ...
@@ -133,8 +133,8 @@ Webster now chooses to unsubscribe team America from the bug about Scofflaw.
 
     >>> subscriber_browser.getLink(id='unsubscribe-subscriber-%s' %
     ...     team.id).click()
-    >>> print extract_text(find_tags_by_class(
-    ...     subscriber_browser.contents, 'value')[0])
+    >>> print(extract_text(find_tags_by_class(
+    ...     subscriber_browser.contents, 'value')[0]))
     subscribe me to this bug, or
     unsubscribe America from this bug.
 
@@ -171,9 +171,9 @@ followed by a link to edit the subscription.
     ...     admin_browser.contents, 'structural-subscriptions')
     >>> for subscription in subscriptions.findAll("li"):
     ...     structure_link, modify_link = subscription.findAll("a")[:2]
-    ...     print "%s <%s>" % (
-    ...         extract_text(structure_link), structure_link.get("href"))
-    ...     print "--> %s" % modify_link.get("href")
+    ...     print("%s <%s>" % (
+    ...         extract_text(structure_link), structure_link.get("href")))
+    ...     print("--> %s" % modify_link.get("href"))
     mozilla-firefox in Ubuntu </ubuntu/+source/mozilla-firefox>
     --> /ubuntu/+source/mozilla-firefox/+subscribe
     pmount in Ubuntu </ubuntu/+source/pmount>
@@ -188,8 +188,8 @@ permission to modify those subscriptions.
     ...     subscriber_browser.contents, 'structural-subscriptions')
     >>> for subscription in subscriptions.findAll("li"):
     ...     structure_link = subscription.find("a")
-    ...     print "%s <%s>" % (
-    ...         extract_text(structure_link), structure_link.get("href"))
+    ...     print("%s <%s>" % (
+    ...         extract_text(structure_link), structure_link.get("href")))
     mozilla-firefox in Ubuntu </ubuntu/+source/mozilla-firefox>
     pmount in Ubuntu </ubuntu/+source/pmount>
 
@@ -204,8 +204,8 @@ structural subscriptions.
 
     >>> subscriber_browser.open(
     ...     "http://launchpad.test/people/+me/+structural-subscriptions";)
-    >>> print extract_text(find_tag_by_id(
-    ...     subscriber_browser.contents, "structural-subscriptions"))
+    >>> print(extract_text(find_tag_by_id(
+    ...     subscriber_browser.contents, "structural-subscriptions")))
     Webster does not have any structural subscriptions.
 
 
@@ -222,14 +222,14 @@ subscription filter.
     ...         browser.contents, 'structural-subscriptions')
     ...     for subscription in subscriptions.findAll("li"):
     ...         structure_link = subscription.find("a")
-    ...         print extract_text(structure_link)
+    ...         print(extract_text(structure_link))
     ...         create_text = subscription.find(text=re.compile("Create"))
     ...         if create_text is None:
-    ...             print "* No create link."
+    ...             print("* No create link.")
     ...         else:
-    ...             print "* %s --> %s" % (
+    ...             print("* %s --> %s" % (
     ...                 create_text.strip(),
-    ...                 create_text.parent.get("href"))
+    ...                 create_text.parent.get("href")))
 
     >>> admin_browser.open(
     ...     "http://launchpad.test/people/+me/+structural-subscriptions";)
@@ -273,9 +273,9 @@ filters are shown with a message stating that there is no filtering.
     ...     subscriptions = find_tag_by_id(
     ...         nigel_browser.contents, 'structural-subscriptions')
     ...     for subscription in subscriptions.findAll("li"):
-    ...         print extract_text(subscription.p)
+    ...         print(extract_text(subscription.p))
     ...         if subscription.dl is not None:
-    ...             print extract_text(subscription.dl)
+    ...             print(extract_text(subscription.dl))
 
     >>> show_nigels_subscriptions()
     Bug mail for Nigel about Scofflaw is filtered;
diff --git a/lib/lp/registry/stories/person/xx-person-working-on.txt b/lib/lp/registry/stories/person/xx-person-working-on.txt
index 651e90b..fe7041f 100644
--- a/lib/lp/registry/stories/person/xx-person-working-on.txt
+++ b/lib/lp/registry/stories/person/xx-person-working-on.txt
@@ -31,8 +31,8 @@ displayed there.
     >>> logout()
 
     >>> anon_browser.open(new_person_url)
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'working-on'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'working-on')))
     All bugs in progress
     Assigned bugs
     ...
diff --git a/lib/lp/registry/stories/person/xx-user-to-user.txt b/lib/lp/registry/stories/person/xx-user-to-user.txt
index db92486..c035e1d 100644
--- a/lib/lp/registry/stories/person/xx-user-to-user.txt
+++ b/lib/lp/registry/stories/person/xx-user-to-user.txt
@@ -11,13 +11,13 @@ wants to contact Salgado.
 
     >>> user_browser.open('http://launchpad.test/~salgado')
     >>> user_browser.getLink('Contact this user').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Contact this user : Guilherme Salgado
 
     >>> user_browser.getControl('Subject').value = 'Hi Salgado'
     >>> user_browser.getControl('Message').value = 'Just saying hello'
     >>> user_browser.getControl('Send').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Guilherme Salgado in Launchpad
 
 Salgado receives the email message from No Priv.
@@ -25,13 +25,13 @@ Salgado receives the email message from No Priv.
     >>> len(stub.test_emails)
     1
     >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
-    >>> print from_addr
+    >>> print(from_addr)
     bounces@xxxxxxxxxxxxx
     >>> len(to_addrs)
     1
-    >>> print to_addrs[0]
+    >>> print(to_addrs[0])
     Guilherme Salgado <guilherme.salgado@xxxxxxxxxxxxx>
-    >>> print raw_msg
+    >>> print(raw_msg)
     Content-Type: text/plain; charset="us-ascii"
     MIME-Version: 1.0
     Content-Transfer-Encoding: 7bit
@@ -47,7 +47,7 @@ mind.
     >>> user_browser.getControl('Subject').value = 'Hi Salgado'
     >>> user_browser.getControl('Message').value = 'Hello again'
     >>> user_browser.getLink('Cancel').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Guilherme Salgado in Launchpad
 
 
@@ -63,7 +63,7 @@ Sample Person has multiple registered and validated email addresses.
 
 By default, Sample can use their preferred email address.
 
-    >>> print browser.getControl('From').value
+    >>> print(browser.getControl('From').value)
     ['test@xxxxxxxxxxxxx']
 
 But they don't have to use their preferred address; they can use one of
@@ -74,7 +74,7 @@ their alternative addresses.
     >>> browser.getControl('Send').click()
 
     >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
-    >>> print raw_msg
+    >>> print(raw_msg)
     Content-Type: text/plain; charset="us-ascii"
     MIME-Version: 1.0
     Content-Transfer-Encoding: 7bit
diff --git a/lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt b/lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt
index d2f0fb2..259de37 100644
--- a/lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt
+++ b/lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt
@@ -65,12 +65,14 @@ But an administrator can still see them if they traverse directly, and
 they'll see an informative message. They can then reactivate them..
 
     >>> admin_browser.open('http://launchpad.test/firefox')
-    >>> print find_tag_by_id(admin_browser.contents, 'project-inactive').renderContents()
+    >>> print(find_tag_by_id(
+    ...     admin_browser.contents, 'project-inactive').renderContents())
      This project is currently inactive...
     >>> toggleProject('firefox')
 
     >>> admin_browser.open('http://launchpad.test/mozilla')
-    >>> print find_tag_by_id(admin_browser.contents, 'project-inactive').renderContents()
+    >>> print(find_tag_by_id(
+    ...     admin_browser.contents, 'project-inactive').renderContents())
      This project is currently inactive...
     >>> toggleProject('mozilla')
 
@@ -80,12 +82,12 @@ And they are back to normal:
     >>> anon_browser.getLink(url='/firefox').click()
     >>> anon_browser.title
     'Mozilla Firefox in Launchpad'
-    >>> print find_tag_by_id(anon_browser.contents, 'project-inactive')
+    >>> print(find_tag_by_id(anon_browser.contents, 'project-inactive'))
     None
 
     >>> anon_browser.open('http://launchpad.test/projects/+index?text=mozilla')
     >>> anon_browser.getLink(url='/mozilla').click()
     >>> anon_browser.title
     'The Mozilla Project in Launchpad'
-    >>> print find_tag_by_id(anon_browser.contents, 'project-inactive')
+    >>> print(find_tag_by_id(anon_browser.contents, 'project-inactive'))
     None
diff --git a/lib/lp/registry/stories/pillar/xx-pillar-sprints.txt b/lib/lp/registry/stories/pillar/xx-pillar-sprints.txt
index c5784e9..940c933 100644
--- a/lib/lp/registry/stories/pillar/xx-pillar-sprints.txt
+++ b/lib/lp/registry/stories/pillar/xx-pillar-sprints.txt
@@ -8,7 +8,7 @@ all events relevant to that pillar.
     ...     maincontent = find_tag_by_id(contents, 'maincontent')
     ...     for link in maincontent.findAll('a'):
     ...         if re.search('/sprints/[a-z0-9]', link['href']) is not None:
-    ...             print link.renderContents()
+    ...             print(link.renderContents())
 
     >>> anon_browser.open('http://launchpad.test/firefox/+sprints')
     >>> print_sprints(anon_browser.contents)
diff --git a/lib/lp/registry/stories/product/xx-launchpad-project-search.txt b/lib/lp/registry/stories/product/xx-launchpad-project-search.txt
index 9e1b3b0..d1595e8 100644
--- a/lib/lp/registry/stories/product/xx-launchpad-project-search.txt
+++ b/lib/lp/registry/stories/product/xx-launchpad-project-search.txt
@@ -12,8 +12,8 @@ distributions.
 
 The user is informed when no matches are found.
 
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'no-matches'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'no-matches')))
     No projects matching ...whizzbar... were found...
 
 Launchpad shows up to config.launchpad.default_batch_size results; if there
@@ -32,16 +32,16 @@ the user to refine their search.
     'http://launchpad.test/projects/+index?text=ubuntu'
 
     >>> content = find_main_content(anon_browser.contents)
-    >>> print extract_text(find_tag_by_id(content, 'search-summary'))
+    >>> print(extract_text(find_tag_by_id(content, 'search-summary')))
     6 projects found matching ...ubuntu..., showing the most relevant 5
 
-    >>> print extract_text(find_tag_by_id(content, 'too-many-matches'))
+    >>> print(extract_text(find_tag_by_id(content, 'too-many-matches')))
     More than 5 projects were found.
     You can do another search with more relevant search terms.
 
 A lower search form is shown when there are matches.
 
-    >>> print find_tag_by_id(content, 'project-search-lower').name
+    >>> print(find_tag_by_id(content, 'project-search-lower').name)
     form
 
 The search results contain projects, project-groups, and distributions.
@@ -50,7 +50,7 @@ The search results contain projects, project-groups, and distributions.
     ...     search_results = find_tag_by_id(
     ...         abrowser.contents, 'search-results')
     ...     for tr in search_results.tbody('tr'):
-    ...         print ' '.join(tr.td.a['class']), extract_text(tr.td.a)
+    ...         print(' '.join(tr.td.a['class']), extract_text(tr.td.a))
     >>> print_search_results(anon_browser)
     sprite distribution Ubuntu
     sprite distribution ubuntutest
@@ -74,7 +74,7 @@ some terms and do another search.
     ''
     >>> project_search.getControl('Search').click()
     >>> empty_search = find_tag_by_id(anon_browser.contents, 'empty-search-string')
-    >>> print empty_search.renderContents()
+    >>> print(empty_search.renderContents())
     <BLANKLINE>
     ...Enter one or more words related to the project you want to find.
     <BLANKLINE>
@@ -93,10 +93,10 @@ A similar page is available for only searching project groups.
     >>> tags = find_tags_by_class(
     ...     anon_browser.contents, "informational message")
     >>> for tag in tags:
-    ...   print tag.renderContents()
+    ...   print(tag.renderContents())
 
 The search results contains only project-groups.
 
-    >>> print extract_text(
-    ...     find_main_content(anon_browser.contents).findAll('p')[1])
+    >>> print(extract_text(
+    ...     find_main_content(anon_browser.contents).findAll('p')[1]))
     1 project group found matching ...gnome...
diff --git a/lib/lp/registry/stories/product/xx-product-add.txt b/lib/lp/registry/stories/product/xx-product-add.txt
index b418979..2a861ca 100644
--- a/lib/lp/registry/stories/product/xx-product-add.txt
+++ b/lib/lp/registry/stories/product/xx-product-add.txt
@@ -6,30 +6,30 @@ either from the Launchpad home page or the projects home page.
 
     >>> user_browser.open('http://launchpad.test')
     >>> user_browser.getLink('Register a project').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Register a project in Launchpad...
 
     >>> user_browser.open('http://launchpad.test/projects')
     >>> user_browser.getLink('Register a project').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Register a project in Launchpad...
 
 The user can see links about things that are often assumed to be related to
 project registration.
 
-    >>> print user_browser.getLink('Register a team').url
+    >>> print(user_browser.getLink('Register a team').url)
     https://help.launchpad.net/Teams
 
-    >>> print user_browser.getLink('Activate a PPA').url
+    >>> print(user_browser.getLink('Activate a PPA').url)
     https://help.launchpad.net/Packaging/PPA
 
-    >>> print user_browser.getLink('Access your personal branches').url
+    >>> print(user_browser.getLink('Access your personal branches').url)
     https://help.launchpad.net/Code/PersonalBranches
 
-    >>> print user_browser.getLink('Translate a project').url
+    >>> print(user_browser.getLink('Translate a project').url)
     https://help.launchpad.net/Translations/YourProject
 
-    >>> print user_browser.getLink('Request a project group').url
+    >>> print(user_browser.getLink('Request a project group').url)
     https://help.launchpad.net/ProjectGroups
 
 
@@ -73,7 +73,7 @@ information correctly this time.
 The project is not yet created; instead, the project basics are successfully
 validated and the second step of the process is entered.
 
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Register a project in Launchpad...
 
 
@@ -85,7 +85,7 @@ modify the project's URL.
 
     >>> user_browser.getControl(name='field.name')
     <Control name='field.name' type='hidden'>
-    >>> print user_browser.getControl(name='field.name').value
+    >>> print(user_browser.getControl(name='field.name').value)
     aardvark
 
 Sample Person is given the opportunity though to change the summary.
@@ -97,30 +97,30 @@ They can also add a longer description.
     ...     'The desktop aardvark is an ornery thing.')
     >>> user_browser.getControl('Python Licence').click()
     >>> user_browser.getControl('Complete Registration').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Aardvark Center in Launchpad
 
 Let's ensure the summary and description are presented.
 
     >>> summary = find_tags_by_class(user_browser.contents,
     ...                              'summary', only_first=True)
-    >>> print extract_text(summary)
+    >>> print(extract_text(summary))
     Control pesky aardvarkian fnords
     >>> desc = find_tags_by_class(user_browser.contents,
     ...                           'description', only_first=True)
-    >>> print extract_text(desc)
+    >>> print(extract_text(desc))
     The desktop aardvark is an ornery thing.
 
 Let's ensure the registrant and maintainer are listed correctly.
 
     >>> registrant = find_tag_by_id(user_browser.contents,
     ...                             'registration')
-    >>> print extract_text(registrant)
+    >>> print(extract_text(registrant))
     Registered...by...No Privileges Person...
 
     >>> maintainer = find_tag_by_id(user_browser.contents,
     ...                             'owner')
-    >>> print extract_text(maintainer)
+    >>> print(extract_text(maintainer))
     Maintainer: No Privileges Person...
 
 
@@ -150,12 +150,12 @@ Registry Admins.
 
     >>> registrant = find_tag_by_id(user_browser.contents,
     ...                             'registration')
-    >>> print extract_text(registrant)
+    >>> print(extract_text(registrant))
     Registered...by...No Privileges Person...
 
     >>> maintainer = find_tag_by_id(user_browser.contents,
     ...                             'owner')
-    >>> print extract_text(maintainer)
+    >>> print(extract_text(maintainer))
     Maintainer: Registry Administrators...
 
 
@@ -167,7 +167,7 @@ Firefox.
 
     >>> user_browser.open('http://launchpad.test')
     >>> user_browser.getLink('Register a project').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Register a project in Launchpad...
 
     >>> user_browser.getControl('Name').value = 'Snowdog'
@@ -183,7 +183,7 @@ Instead of registering their new project, Sample Person decides to participate
 in the Mozilla Project.
 
     >>> user_browser.getLink('The Mozilla Project').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     The Mozilla Project in Launchpad
 
 
@@ -195,8 +195,8 @@ The project registration workflow used to get started at
 location at /projects/+new.
 
     >>> user_browser.open('http://launchpad.test/projects/+new-guided')
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Register a project in Launchpad...
 
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://launchpad.test/projects/+new
diff --git a/lib/lp/registry/stories/product/xx-product-code-trunk.txt b/lib/lp/registry/stories/product/xx-product-code-trunk.txt
index 157bae2..04ba410 100644
--- a/lib/lp/registry/stories/product/xx-product-code-trunk.txt
+++ b/lib/lp/registry/stories/product/xx-product-code-trunk.txt
@@ -24,33 +24,33 @@ Make revisions for the branch so it has a codebrowse link.
     >>> def print_development_focus(browser):
     ...     """Print out the development focus part of the project info."""
     ...     dev_focus = find_tag_by_id(browser.contents, 'development-focus')
-    ...     print extract_text(dev_focus)
-    ...     print "Links:"
+    ...     print(extract_text(dev_focus))
+    ...     print("Links:")
     ...     for a in dev_focus.findAll('a'):
     ...         for content in a.contents:
-    ...             print content
+    ...             print(content)
     ...         title = a.get('title', '')
-    ...         print "%s (%s)" % (title, a['href'])
+    ...         print("%s (%s)" % (title, a['href']))
     >>> def print_code_trunk(browser):
     ...     """Print out code trunk part of the project info."""
     ...     project_info = find_tag_by_id(browser.contents, 'code-info')
     ...     code_trunk = project_info.find(attrs={'id':'code-trunk'})
     ...     try:
-    ...         print extract_text(code_trunk)
+    ...         print(extract_text(code_trunk))
     ...     except TypeError:
     ...         return
-    ...     print "Links:"
+    ...     print("Links:")
     ...     for a in code_trunk.findAll('a'):
     ...         for content in a.contents:
-    ...             print content
+    ...             print(content)
     ...         title = a.get('title', '')
-    ...         print "%s (%s)" % (title, a['href'])
+    ...         print("%s (%s)" % (title, a['href']))
     >>> def print_involvement_portlet(browser):
     ...     involvement = find_tag_by_id(browser.contents, 'involvement')
     ...     for a in involvement.findAll('a'):
     ...         for content in a.contents:
-    ...             print content
-    ...         print a['href']
+    ...             print(content)
+    ...         print(a['href'])
 
 
 Projects without development focus branches
diff --git a/lib/lp/registry/stories/product/xx-product-edit-sourceforge-project.txt b/lib/lp/registry/stories/product/xx-product-edit-sourceforge-project.txt
index 734d465..66bccea 100644
--- a/lib/lp/registry/stories/product/xx-product-edit-sourceforge-project.txt
+++ b/lib/lp/registry/stories/product/xx-product-edit-sourceforge-project.txt
@@ -6,7 +6,7 @@ The setting of the SourceForge project is constrained.
     ...     admin_browser.open('http://launchpad.test/firefox/+edit')
     ...     admin_browser.getControl('Sourceforge Project').value = name
     ...     admin_browser.getControl('Change').click()
-    ...     print admin_browser.url
+    ...     print(admin_browser.url)
     ...     print_feedback_messages(admin_browser.contents)
 
     >>> set_sourceforge_project('1234')
diff --git a/lib/lp/registry/stories/product/xx-product-edit.txt b/lib/lp/registry/stories/product/xx-product-edit.txt
index 8f82dd3..1b6a7c6 100644
--- a/lib/lp/registry/stories/product/xx-product-edit.txt
+++ b/lib/lp/registry/stories/product/xx-product-edit.txt
@@ -7,10 +7,10 @@ Page to edit a product - does the page load as the product owner?
     >>> browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/firefox')
     >>> browser.getLink('Change details').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/firefox/+edit
 
-    >>> print browser.title
+    >>> print(browser.title)
     Change Mozilla Firefox's details...
 
 We try to change the project related to that product. First with a
@@ -19,7 +19,7 @@ invalid project.
     >>> browser.getControl('Part of', index=0).value = 'asdasfasd'
     >>> browser.getControl(name='field.actions.change').click()
     >>> for message in find_tags_by_class(browser.contents, 'error'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     There is 1 error.
     <BLANKLINE>
     ...
@@ -30,12 +30,12 @@ Now we try to edit with a project that exists.
 
     >>> browser.getControl('Part of', index=0).value = 'gnome'
     >>> browser.getControl(name='field.actions.change').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/firefox
 
 Now we test if we edited it successfully.
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'partof'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'partof')))
     Part of: GNOME
 
 
@@ -50,7 +50,7 @@ But administrators can access the page:
     >>> admin_browser.url
     'http://launchpad.test/firefox/+admin'
 
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     Administer Mozilla Firefox...
 
 And in that page they can set aliases to the product.
@@ -82,7 +82,7 @@ First a user adds a product named newproductname.
     >>> user_browser.getControl(name='field.licenses').value = ['GNU_GPL_V2']
     >>> user_browser.getControl(name='field.license_info').value = 'foo'
     >>> user_browser.getControl('Complete Registration').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://launchpad.test/newproductname
 
 Then a product named newproductname2.
@@ -98,7 +98,7 @@ Then a product named newproductname2.
     >>> user_browser.getControl(name='field.licenses').value = ['GNU_GPL_V2']
     >>> user_browser.getControl(name='field.license_info').value = 'foo'
     >>> user_browser.getControl('Complete Registration').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://launchpad.test/newproductname2
 
 Now we try to change newproductname2's name to newproductname.
@@ -108,7 +108,7 @@ Now we try to change newproductname2's name to newproductname.
     >>> admin_browser.getControl('Name').value = 'newproductname'
     >>> admin_browser.getControl(name='field.actions.change').click()
     >>> for message in find_tags_by_class(admin_browser.contents, 'error'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     There is 1 error.
     <BLANKLINE>
     ...
@@ -121,7 +121,7 @@ will be accepted because there is no product called newproductname3
 
     >>> admin_browser.getControl('Name').value = 'newproductname3'
     >>> admin_browser.getControl(name='field.actions.change').click()
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://launchpad.test/newproductname3
 
 
@@ -132,7 +132,7 @@ Administrators can change the owner of a project.
 
     >>> admin_browser.open(
     ...     'http://launchpad.test/newproductname3')
-    >>> print extract_text(find_tag_by_id(admin_browser.contents, 'owner'))
+    >>> print(extract_text(find_tag_by_id(admin_browser.contents, 'owner')))
     Maintainer: No Privileges Person
     ...
 
@@ -140,7 +140,7 @@ Administrators can change the owner of a project.
     ...     'http://launchpad.test/newproductname3/+admin')
     >>> admin_browser.getControl('Maintainer').value = 'cprov'
     >>> admin_browser.getControl(name='field.actions.change').click()
-    >>> print extract_text(find_tag_by_id(admin_browser.contents, 'owner'))
+    >>> print(extract_text(find_tag_by_id(admin_browser.contents, 'owner')))
     Maintainer: Celso Providelo
     ...
 
@@ -150,16 +150,16 @@ is created but we allow admins to change it to correct data.
 
     >>> admin_browser.open(
     ...     'http://launchpad.test/newproductname3')
-    >>> print extract_text(
-    ...     find_tag_by_id(admin_browser.contents, 'registration'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'registration')))
     Registered ... by No Privileges Person
 
     >>> admin_browser.open(
     ...     'http://launchpad.test/newproductname3/+admin')
     >>> admin_browser.getControl('Registrant').value = 'cprov'
     >>> admin_browser.getControl(name='field.actions.change').click()
-    >>> print extract_text(
-    ...     find_tag_by_id(admin_browser.contents, 'registration'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'registration')))
     Registered ... by Celso Providelo
 
 The registrant really should only be a person, not a team, but that
@@ -169,8 +169,8 @@ teams as registrants.
     >>> admin_browser.open('http://launchpad.test/newproductname3/+admin')
     >>> admin_browser.getControl('Registrant').value = 'registry'
     >>> admin_browser.getControl('Change').click()
-    >>> print extract_text(
-    ...     find_tag_by_id(admin_browser.contents, 'registration'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'registration')))
     Registered ... by Registry Administrators
 
 
@@ -213,9 +213,9 @@ But registry experts can change a product name and set an alias.
     >>> browser.getControl('Change').click()
 
     >>> browser.getLink('Administer').click()
-    >>> print browser.getControl('Name').value
+    >>> print(browser.getControl('Name').value)
     trebuchet
-    >>> print browser.getControl('Aliases').value
+    >>> print(browser.getControl('Aliases').value)
     trebucket
 
 
@@ -241,7 +241,7 @@ The Admins and Registry Experts can deactivate a project.
     >>> registry_browser.open('http://launchpad.test/bzr/+review-license')
     >>> registry_browser.getControl(name='field.active').value = False
     >>> registry_browser.getControl(name='field.actions.change').click()
-    >>> print registry_browser.url
+    >>> print(registry_browser.url)
     http://launchpad.test/bzr
 
 The product overview page should show a notice that a product is
@@ -250,27 +250,27 @@ Admins can still see the product, but regular users can't.
 
     >>> registry_browser.open('http://launchpad.test/bzr')
     >>> contents = find_main_content(registry_browser.contents)
-    >>> print extract_text(contents.find(id='project-inactive'))
+    >>> print(extract_text(contents.find(id='project-inactive')))
     This project is currently inactive ...
 
     >>> admin_browser.open('http://launchpad.test/bzr')
     >>> contents = find_main_content(admin_browser.contents)
-    >>> print extract_text(contents.find(id='project-inactive'))
+    >>> print(extract_text(contents.find(id='project-inactive')))
     This project is currently inactive ...
 
 The product can then be reactivated.
 
     >>> registry_browser.getLink('Review project').click()
-    >>> print registry_browser.url
+    >>> print(registry_browser.url)
     http://launchpad.test/bzr/+review-license
 
     >>> registry_browser.getControl(name='field.active').value = True
     >>> registry_browser.getControl(name='field.actions.change').click()
-    >>> print registry_browser.url
+    >>> print(registry_browser.url)
     http://launchpad.test/bzr
 
     >>> contents = find_main_content(registry_browser.contents)
-    >>> print contents.find(id='project-inactive')
+    >>> print(contents.find(id='project-inactive'))
     None
 
 Revert team memberships.
diff --git a/lib/lp/registry/stories/product/xx-product-files.txt b/lib/lp/registry/stories/product/xx-product-files.txt
index fff1d46..673a34c 100644
--- a/lib/lp/registry/stories/product/xx-product-files.txt
+++ b/lib/lp/registry/stories/product/xx-product-files.txt
@@ -9,7 +9,7 @@ Any user can see that a project with no releases has no downloads.
 
     >>> anon_browser.open('http://launchpad.test/aptoncd')
     >>> content = find_tag_by_id(anon_browser.contents, 'downloads')
-    >>> print extract_text(content)
+    >>> print(extract_text(content))
     Downloads
     APTonCD does not have any download files registered with Launchpad.
 
@@ -19,7 +19,7 @@ downloadable files.
 
     >>> anon_browser.open('http://launchpad.test/thunderbird')
     >>> content = find_tag_by_id(anon_browser.contents, 'downloads')
-    >>> print extract_text(content)
+    >>> print(extract_text(content))
     Downloads
     Mozilla Thunderbird does not have any download files
     registered with Launchpad.
@@ -30,14 +30,14 @@ release to add download files.
 
     >>> anon_browser.open('http://launchpad.test/firefox')
     >>> content = find_tag_by_id(anon_browser.contents, 'downloads')
-    >>> print extract_text(content)
+    >>> print(extract_text(content))
     Downloads
     Latest version is 0.9.2
     firefox_0.9.2.orig.tar.gz...
 
     >>> anon_browser.getLink('All downloads').click()
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'project-downloads'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'project-downloads')))
     0.9.2 (One (secure) Tree Hill) release from the trunk series
     released 2004-10-15
     Release information
@@ -56,7 +56,7 @@ otherwise.
     ...     name='prop-prod', information_type=InformationType.PROPRIETARY)
     >>> logout()
     >>> admin_browser.open('http://launchpad.test/prop-prod')
-    >>> print find_tag_by_id(admin_browser.contents, 'downloads')
+    >>> print(find_tag_by_id(admin_browser.contents, 'downloads'))
     None
 
 
@@ -71,7 +71,7 @@ the delete options.
     'http://launchpad.test/firefox/+download'
     >>> content = find_main_content(anon_browser.contents)
     >>> row = content.find('table').find('tr')
-    >>> print extract_text(row)
+    >>> print(extract_text(row))
     File
     Description
     Downloads
@@ -85,7 +85,7 @@ Again, for an authenticated user who is not the firefox product owner.
     'http://launchpad.test/firefox/+download'
     >>> content = find_main_content(non_owner.contents)
     >>> row = content.find('table').find('tr')
-    >>> print extract_text(row)
+    >>> print(extract_text(row))
     File
     Description
     Downloads
@@ -99,7 +99,7 @@ Now, login as a firefox admin and see the delete fields.
     'http://launchpad.test/firefox/+download'
     >>> content = find_main_content(firefox_owner.contents)
     >>> row = content.find('table').find('tr')
-    >>> print extract_text(row)
+    >>> print(extract_text(row))
     File
     Description
     Downloads
@@ -110,11 +110,11 @@ A project owner should not see the delete button when there are no files.
     >>> tbird_owner = setupBrowser(
     ...     auth='Basic foo.bar@xxxxxxxxxxxxx:test')
     >>> tbird_owner.open('http://launchpad.test/thunderbird/+download')
-    >>> print tbird_owner.title
+    >>> print(tbird_owner.title)
     Mozilla Thunderbird project files...
 
     >>> main_content = find_main_content(tbird_owner.contents)
-    >>> print extract_text(main_content)
+    >>> print(extract_text(main_content))
     Download project files
     No download files exist for this project...
     Add download file to the trunk series for release: 0.8
@@ -134,7 +134,7 @@ add download files for each release in that series is presented.
 
     >>> firefox_owner.open('http://launchpad.test/firefox/+download')
     >>> for tag in find_tags_by_class(firefox_owner.contents, 'add-files'):
-    ...     print extract_text(tag)
+    ...     print(extract_text(tag))
     Add download file to the trunk series for release: 0.9.2, 0.9.1, 0.9
     Add download file to the 1.0 series for release: 1.0.0
 
@@ -149,7 +149,7 @@ series should not show up in the list.
     >>> firefox_owner.getControl('Register Series').click()
     >>> firefox_owner.open('http://launchpad.test/firefox/+download')
     >>> for tag in find_tags_by_class(firefox_owner.contents, 'add-files'):
-    ...     print extract_text(tag)
+    ...     print(extract_text(tag))
     Add download file to the trunk series for release: 0.9.2, 0.9.1, 0.9
     Add download file to the 1.0 series for release: 1.0.0
 
@@ -166,11 +166,11 @@ the list.
     >>> firefox_owner.getLink('Create release').click()
     >>> firefox_owner.getControl('Date released').value = '2000-01-01'
     >>> firefox_owner.getControl('Create release').click()
-    >>> print firefox_owner.url
+    >>> print(firefox_owner.url)
     http://launchpad.test/firefox/+milestone/3.14159
     >>> firefox_owner.open('http://launchpad.test/firefox/+download')
     >>> for tag in find_tags_by_class(firefox_owner.contents, 'add-files'):
-    ...     print extract_text(tag)
+    ...     print(extract_text(tag))
     Add download file to the trunk series for release: 0.9.2, 0.9.1, 0.9
     Add download file to the 3.0 series for release: 3.14159
     Add download file to the 1.0 series for release: 1.0.0
@@ -199,7 +199,7 @@ To add a download file the release version link is used.
 
     >>> firefox_owner.open('http://launchpad.test/firefox/+download')
     >>> firefox_owner.getLink('1.0.0').click()
-    >>> print firefox_owner.title
+    >>> print(firefox_owner.title)
     Add a download file to Mozilla Firefox...
 
 Ensure a non-owner doesn't see the 'Add download file' link after
@@ -219,7 +219,7 @@ To add a download file the +adddownloadfile page is accessed.
 The maximum size of the upload file is shown on the page.
 
     >>> content = find_main_content(firefox_owner.contents)
-    >>> print content
+    >>> print(content)
     <...
     ...You may upload files up to 1.0 GiB...
 
@@ -260,7 +260,7 @@ user to see.
     >>> anon_browser.open('http://launchpad.test/firefox/+download')
     >>> content = find_main_content(anon_browser.contents)
     >>> for tr in  content.findAll('table')[1].findAll('tr'):
-    ...     print extract_text(tr)
+    ...     print(extract_text(tr))
     File                 Description      Downloads
     bar.txt (md5)        Bar installer    -
     foo.txt (md5, sig)   Foo installer    -
@@ -274,7 +274,7 @@ an admin can also delete a release file.
     >>> checkbox.value = checkbox.options
     >>> table = find_tag_by_id(admin_browser.contents, 'downloads')
     >>> for tr in table.findAll('tr'):
-    ...     print extract_text(tr)
+    ...     print(extract_text(tr))
     File                Description    Downloads      Delete
     bar.txt (md5)       Bar installer  -
     foo.txt (md5, sig)  Foo installer  -
@@ -285,7 +285,7 @@ an admin can also delete a release file.
     1 file has been deleted.
     >>> table = find_tag_by_id(admin_browser.contents, 'downloads')
     >>> for tr in table.findAll('tr'):
-    ...     print extract_text(tr)
+    ...     print(extract_text(tr))
     File                Description    Downloads     Delete
     foo.txt (md5, sig)  Foo installer  -
 
@@ -297,7 +297,7 @@ the md5 digest of the file, and the signature that we uploaded.
     >>> non_owner.url
     'http://launchpad.test/firefox/+download'
     >>> content = find_main_content(non_owner.contents)
-    >>> print content
+    >>> print(content)
     <...
     ...foo.txt...md5...sig...Foo installer...
 
@@ -388,7 +388,7 @@ are listed within series in reverse chronological order, except
     >>> for row in rows[1:]:
     ...     a_list = row.findAll('a')
     ...     if len(a_list) > 0:
-    ...        print a_list[0].string
+    ...        print(a_list[0].string)
     firefox_0.9.2.orig.tar.gz
     foo09.txt
     foo3.txt
@@ -423,7 +423,7 @@ XXX Mon May  7 10:02:49 2007 -- bac
     ...     if key.lower() == "location":
     ...         redirect_url = value
     ...         break
-    >>> print urlopen(redirect_url).read()
+    >>> print(urlopen(redirect_url).read())
     Foo2 installer package...
 
 Delete the file foo2.txt.
@@ -449,7 +449,7 @@ Ensure the file is no longer listed.
     >>> for row in rows[1:]:
     ...     a_list = row.findAll('a')
     ...     if len(a_list) > 0:
-    ...        print a_list[0].string
+    ...        print(a_list[0].string)
     firefox_0.9.2.orig.tar.gz
     foo09.txt
     foo3.txt
@@ -465,7 +465,7 @@ Non-administrators do not have the delete option.
     >>> non_owner.open('http://launchpad.test/firefox/trunk/0.9')
     >>> table = find_tag_by_id(non_owner.contents, 'downloads')
     >>> for tr in table.findAll('tr'):
-    ...     print extract_text(tr)
+    ...     print(extract_text(tr))
     File             Description     Downloads
     foo09.txt (md5)  Foo09 installer -
 
@@ -484,7 +484,7 @@ so it is not shown.
     >>> firefox_owner.open('http://launchpad.test/firefox/trunk/0.9')
     >>> table = find_tag_by_id(firefox_owner.contents, 'downloads')
     >>> for tr in table.findAll('tr'):
-    ...     print extract_text(tr)
+    ...     print(extract_text(tr))
     File             Description     Downloads  Delete
     foo09.txt (md5)  Foo09 installer -
 
diff --git a/lib/lp/registry/stories/product/xx-product-index.txt b/lib/lp/registry/stories/product/xx-product-index.txt
index 72e1d36..28e7079 100644
--- a/lib/lp/registry/stories/product/xx-product-index.txt
+++ b/lib/lp/registry/stories/product/xx-product-index.txt
@@ -11,7 +11,7 @@ The product page has a link to help translate it.
     >>> link = user_browser.getLink(
     ...     url='http://translations.launchpad.test/evolution')
     >>> link.click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Translations : Evolution
 
 
@@ -20,7 +20,7 @@ Links and Programming languages
 
 Evolution has no external links.
 
-    >>> print find_tag_by_id(user_browser.contents, 'external-links-heading')
+    >>> print(find_tag_by_id(user_browser.contents, 'external-links-heading'))
     None
 
 Now update Tomcat to actually have this data:
@@ -51,14 +51,14 @@ Let's check it out:
     >>> content = find_main_content(browser.contents)
     >>> external_links = find_tag_by_id(content, 'external-links')
     >>> for link in external_links.findAll('a'):
-    ...     print extract_text(link), link['href']
+    ...     print(extract_text(link), link['href'])
     Home page http://home.page/
     Sourceforge project http://sourceforge.net/projects/sf-tomcat
     Wiki http://wiki.url/
     Screenshots http://screenshots.url/
     External downloads http://download.url/
 
-    >>> print extract_text(find_tag_by_id(content, 'product-languages'))
+    >>> print(extract_text(find_tag_by_id(content, 'product-languages')))
     Programming languages: C++,Xenon and Purple
 
 When the sourceforge URL is identical to the homepage, we omit the homepage:
@@ -76,7 +76,7 @@ When the sourceforge URL is identical to the homepage, we omit the homepage:
     >>> content = find_main_content(browser.contents)
     >>> external_links = find_tag_by_id(content, 'external-links')
     >>> for link in external_links.findAll('a'):
-    ...     print extract_text(link), link['href']
+    ...     print(extract_text(link), link['href'])
     Sourceforge project http://sourceforge.net/projects/sf-tomcat
     Wiki http://wiki.url/
     Screenshots http://screenshots.url/
@@ -101,8 +101,8 @@ been reviewed by a Launchpad administrator will be displayed as
 Any user can see that the project's licence has not been reviewed.
 
     >>> user_browser.open('http://launchpad.test/thunderbird')
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'license-status'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'license-status')))
     This project’s licence has not been reviewed.
 
 Changing the state to reviewed but not approved results in the project
@@ -129,11 +129,11 @@ direct the owner to purchase a subscription.
     >>> transaction.commit()
     >>> logout()
     >>> owner_browser.open('http://launchpad.test/firefox')
-    >>> print find_tag_by_id(owner_browser.contents, 'license-status')
+    >>> print(find_tag_by_id(owner_browser.contents, 'license-status'))
     <...This project’s licence is proprietary...
 
-    >>> print find_tag_by_id(owner_browser.contents,
-    ...     'portlet-requires-subscription')
+    >>> print(find_tag_by_id(owner_browser.contents,
+    ...     'portlet-requires-subscription'))
     <div...Purchasing a commercial subscription is required...</div>
 
 Any user can see that the project's licence is proprietary.
@@ -141,8 +141,8 @@ Any user can see that the project's licence is proprietary.
     >>> user_browser.open('http://launchpad.test/firefox')
     >>> user_browser.contents
     '<...This project&rsquo;s licence is proprietary...
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'licences'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'licences')))
     Licence:
     Other/Proprietary (Internal project.)
     Commercial subscription expires ...
@@ -150,8 +150,8 @@ Any user can see that the project's licence is proprietary.
 
 A non-owner does not see that a commercial subscription is due.
 
-    >>> print find_tag_by_id(user_browser.contents,
-    ...                      'portlet-requires-subscription')
+    >>> print(find_tag_by_id(user_browser.contents,
+    ...                      'portlet-requires-subscription'))
     None
 
 If the project qualifies for free hosting, tghe portlet is not displayed.
@@ -160,8 +160,8 @@ If the project qualifies for free hosting, tghe portlet is not displayed.
     >>> flush_database_updates()
     >>> transaction.commit()
     >>> owner_browser.open('http://launchpad.test/firefox')
-    >>> print find_tag_by_id(owner_browser.contents,
-    ...                      'portlet-requires-subscription')
+    >>> print(find_tag_by_id(owner_browser.contents,
+    ...                      'portlet-requires-subscription'))
     None
 
 If the project's licence is open source, the licence status is not
@@ -169,10 +169,10 @@ displayed on the index page, since most projects fall into this
 category.
 
     >>> user_browser.open('http://launchpad.test/firefox')
-    >>> print find_tag_by_id(owner_browser.contents, 'license-status')
+    >>> print(find_tag_by_id(owner_browser.contents, 'license-status'))
     None
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'licences'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'licences')))
     Licence:
     GNU GPL v2
     Commercial subscription expires ...
@@ -200,8 +200,8 @@ Enable the subscription.
 
     >>> owner_browser = setupBrowser(auth='Basic bac@xxxxxxxxxxxxx:test')
     >>> owner_browser.open('http://launchpad.test/mega-money-maker')
-    >>> print extract_text(find_tag_by_id(owner_browser.contents,
-    ...                      'commercial_subscription'))
+    >>> print(extract_text(find_tag_by_id(owner_browser.contents,
+    ...                      'commercial_subscription')))
     Commercial subscription expires ...
 
 Commercial team members will see the expiration information.
@@ -209,15 +209,15 @@ Commercial team members will see the expiration information.
     >>> comm_browser = setupBrowser(
     ...     auth='Basic commercial-member@xxxxxxxxxxxxx:test')
     >>> comm_browser.open('http://launchpad.test/mega-money-maker')
-    >>> print extract_text(find_tag_by_id(comm_browser.contents,
-    ...                      'commercial_subscription'))
+    >>> print(extract_text(find_tag_by_id(comm_browser.contents,
+    ...                      'commercial_subscription')))
     Commercial subscription expires ...
 
 Launchpad administrators will see the expiration information.
 
     >>> admin_browser.open('http://launchpad.test/mega-money-maker')
-    >>> print extract_text(find_tag_by_id(admin_browser.contents,
-    ...                      'commercial_subscription'))
+    >>> print(extract_text(find_tag_by_id(admin_browser.contents,
+    ...                      'commercial_subscription')))
     Commercial subscription expires ...
 
 
@@ -227,8 +227,8 @@ Development
 The project page shows the series that is the focus of development.
 
     >>> anon_browser.open('http://launchpad.test/firefox')
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'development-focus'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'development-focus')))
     trunk series is the current focus of development.
 
 The page has a link to view the project's milestones.
@@ -252,7 +252,7 @@ home page.
 
     >>> Product.byName("firefox").setAliases(['iceweasel', 'snowchicken'])
     >>> anon_browser.open('http://launchpad.test/firefox')
-    >>> print extract_text(find_tag_by_id(anon_browser.contents, 'aliases'))
+    >>> print(extract_text(find_tag_by_id(anon_browser.contents, 'aliases')))
     Also known as: iceweasel, snowchicken
 
 
@@ -262,8 +262,8 @@ Ubuntu packaging
 If a product is packaged in Ubuntu the links are shown.
 
     >>> user_browser.open('http://launchpad.test/firefox')
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'portlet-packages'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'portlet-packages')))
     All packages
     Packages in Distributions
     mozilla-firefox source package in Warty Version 0.9 uploaded on...
diff --git a/lib/lp/registry/stories/product/xx-product-package-pages.txt b/lib/lp/registry/stories/product/xx-product-package-pages.txt
index 93e9bbf..54261e5 100644
--- a/lib/lp/registry/stories/product/xx-product-package-pages.txt
+++ b/lib/lp/registry/stories/product/xx-product-package-pages.txt
@@ -6,16 +6,16 @@ each.
 
     >>> anon_browser.open(
     ...     'http://launchpad.test/evolution/+packages')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     Linked packages ...
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'distribution-series'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'distribution-series')))
     Distribution series  Source package        Version  Project series
     Warty (4.10)         evolution  Evolution           trunk series ...
 
     >>> anon_browser.getLink(url='/ubuntu/warty/+source/evolution').click()
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'maincontent').h1)
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'maincontent').h1))
     evolution source package in Warty
 
 It can also show the packages by product series. And if you have permission
@@ -25,10 +25,10 @@ Evolution.
 
     >>> evo_owner = setupBrowser(auth='Basic mark@xxxxxxxxxxx:test')
     >>> evo_owner.open('http://launchpad.test/evolution/+packages')
-    >>> print evo_owner.title
+    >>> print(evo_owner.title)
     Linked packages ...
-    >>> print extract_text(find_tag_by_id(
-    ...     evo_owner.contents, 'packages-trunk'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     evo_owner.contents, 'packages-trunk')))
     Distribution  Distribution series  Source package  Version
     Ubuntu        Warty (4.10)         evolution                Remove...
     Ubuntu        Hoary (5.04)         evolution       1.0      Remove...
@@ -39,7 +39,7 @@ Evolution.
 Any logged in users can still see the links to create a packaging link.
 
     >>> user_browser.open('http://launchpad.test/evolution/+packages')
-    >>> print user_browser.getLink(url='/evolution/trunk/+ubuntupkg').url
+    >>> print(user_browser.getLink(url='/evolution/trunk/+ubuntupkg').url)
     http://launchpad.test/evolution/trunk/+ubuntupkg
 
     >>> anon_browser.open('http://launchpad.test/evolution/+packages')
@@ -56,16 +56,16 @@ Packaging links can be deleted if they were created in error.
 
     >>> evo_owner.getLink(
     ...     url='/ubuntu/warty/+source/evolution/+remove-packaging').click()
-    >>> print evo_owner.title
+    >>> print(evo_owner.title)
     Unlink an upstream project...
     >>> evo_owner.getControl('Unlink').click()
-    >>> print evo_owner.title
+    >>> print(evo_owner.title)
     Linked packages...
 
     >>> print_feedback_messages(evo_owner.contents)
     Removed upstream association between Evolution trunk series and Warty.
 
-    >>> print extract_text(find_tag_by_id(
-    ...     evo_owner.contents, 'packages-trunk'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     evo_owner.contents, 'packages-trunk')))
     Distribution  Distribution series  Source package  Version
     Ubuntu        Hoary (5.04)         evolution       1.0     Remove...
diff --git a/lib/lp/registry/stories/product/xx-product-rdf.txt b/lib/lp/registry/stories/product/xx-product-rdf.txt
index 711ceeb..32219d1 100644
--- a/lib/lp/registry/stories/product/xx-product-rdf.txt
+++ b/lib/lp/registry/stories/product/xx-product-rdf.txt
@@ -3,7 +3,7 @@ We export DOAP RDF metadata for products from a link in the
 
     >>> anon_browser.open("http://launchpad.test/firefox";)
     >>> anon_browser.getLink("RDF metadata").click()
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     <?xml version="1.0" encoding="utf-8"...?>
     <rdf:RDF ...xmlns:lp="https://launchpad.net/rdf/launchpad#";...>
     <lp:Product>
@@ -26,5 +26,5 @@ And it's valid XML and RDF:
 
     >>> from xml.dom.minidom import parseString
     >>> document = parseString(str(anon_browser.contents))
-    >>> print document.documentElement.nodeName
+    >>> print(document.documentElement.nodeName)
     rdf:RDF
diff --git a/lib/lp/registry/stories/product/xx-product-reassignment-and-milestones.txt b/lib/lp/registry/stories/product/xx-product-reassignment-and-milestones.txt
index c04c1dd..abb9ff1 100644
--- a/lib/lp/registry/stories/product/xx-product-reassignment-and-milestones.txt
+++ b/lib/lp/registry/stories/product/xx-product-reassignment-and-milestones.txt
@@ -9,7 +9,7 @@ even if the user was trying to set the milestone value.
     >>> browser.getControl("Save Changes").click()
 
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     The milestone setting was ignored because you reassigned the bug
     to...Evolution...
 
@@ -37,6 +37,6 @@ milestone value, if one was set.
     >>> browser.getControl("Save Changes").click()
 
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     The Mozilla Firefox 1.0 milestone setting has been removed
     because you reassigned the bug to Evolution.
diff --git a/lib/lp/registry/stories/product/xx-productset.txt b/lib/lp/registry/stories/product/xx-productset.txt
index 8d24a47..c13ab3a 100644
--- a/lib/lp/registry/stories/product/xx-productset.txt
+++ b/lib/lp/registry/stories/product/xx-productset.txt
@@ -33,7 +33,7 @@ A member of the Launchpad registry experts team may successfully access the
     >>> registry_browser.getLink("Review projects").click()
     >>> registry_browser.url
     'http://launchpad.test/projects/+review-licenses'
-    >>> print registry_browser.title
+    >>> print(registry_browser.title)
     Review projects...
 
 
@@ -43,13 +43,13 @@ View all projects
 The unprivileged user can see "+all".
 
     >>> user_browser.open('http://launchpad.test/projects/+all')
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Projects registered in Launchpad...
 
 The commercial user can also view "+all".
 
     >>> registry_browser.open('http://launchpad.test/projects/+all')
-    >>> print registry_browser.title
+    >>> print(registry_browser.title)
     Projects registered in Launchpad...
 
 
@@ -59,17 +59,17 @@ Create a project
 The unprivileged user can see "+new".
 
     >>> user_browser.open('http://launchpad.test/projects/+new')
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Register a project in Launchpad...
 
 The commercial user can also view "+new".
 
     >>> registry_browser.open('http://launchpad.test/projects/+new')
-    >>> print registry_browser.title
+    >>> print(registry_browser.title)
     Register a project in Launchpad...
 
 The commercial user can also view "+new".
 
     >>> registry_browser.open('http://launchpad.test/projects/+new')
-    >>> print registry_browser.title
+    >>> print(registry_browser.title)
     Register a project in Launchpad...
diff --git a/lib/lp/registry/stories/productrelease/xx-productrelease-basics.txt b/lib/lp/registry/stories/productrelease/xx-productrelease-basics.txt
index 082bdb6..e33c07c 100644
--- a/lib/lp/registry/stories/productrelease/xx-productrelease-basics.txt
+++ b/lib/lp/registry/stories/productrelease/xx-productrelease-basics.txt
@@ -22,22 +22,22 @@ release in the series.
     >>> browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/firefox/+milestone/1.0')
     >>> browser.getLink('Create release').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/firefox/+milestone/1.0/+addrelease
 
-    >>> print browser.title
+    >>> print(browser.title)
     Create a new release for Mozilla Firefox...
 
 Links to previous releases from the series are listed.
 
     >>> other_releases = find_tag_by_id(browser.contents, 'other-releases')
-    >>> print extract_text(other_releases)
+    >>> print(extract_text(other_releases))
     The following releases have been made for the trunk series:
     2004-10-16 Mozilla Firefox 0.9.2 (One (secure) Tree Hill)
     2004-10-16 Mozilla Firefox 0.9.1 (One Tree Hill (v2))
     2004-10-16 Mozilla Firefox 0.9 (One Tree Hill)
 
-    >>> print browser.getLink('0.9.1').url
+    >>> print(browser.getLink('0.9.1').url)
     http://launchpad.test/firefox/trunk/0.9.1
 
 Sample Person completes the release.
@@ -54,9 +54,9 @@ After creating the release, Sample Person sees the release page.
 
 The release's information is displayed in the page.
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'release-notes'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'release-notes')))
     Released 1.0
-    >>> print extract_text(find_tag_by_id(browser.contents, 'changelog'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'changelog')))
     Fix Foo
 
 Only one release can be created for each milestone.
@@ -106,12 +106,12 @@ The release owner can edit the release via its +edit form:
 
     >>> browser.open('http://launchpad.test/firefox/trunk/1.0')
     >>> browser.getLink('Change release details', index=1).click()
-    >>> print browser.title
+    >>> print(browser.title)
     Edit Mozilla Firefox 1.0 release details...
 
     >>> browser.getControl('Changelog').value = 'This is not a joke.'
     >>> browser.getControl('Change').click()
-    >>> print browser.title
+    >>> print(browser.title)
     1.0 : Series trunk : Mozilla Firefox
 
 
@@ -123,7 +123,7 @@ release too.
     >>> from lp.registry.model.person import Person
     >>> from lp.registry.model.product import Product
     >>> tomcat = Product.selectOneBy(name='tomcat')
-    >>> print tomcat.owner.name
+    >>> print(tomcat.owner.name)
     ubuntu-team
 
 Let's add a release as Jeff:
@@ -137,7 +137,7 @@ Let's add a release as Jeff:
     >>> browser.getControl('Date released').value = '2008-12-01'
     >>> browser.getControl('Changelog').value = 'Fix Foo'
     >>> browser.getControl('Create release').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/tomcat/+milestone/0.6.6.6
 
 Celso is a member of ubuntu-team, so he can edit this release too:
@@ -145,12 +145,12 @@ Celso is a member of ubuntu-team, so he can edit this release too:
     >>> browser = setupBrowser(auth='Basic celso.providelo@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/tomcat/trunk/0.6.6.6')
     >>> browser.getLink('Change release details', index=0).click()
-    >>> print browser.title
+    >>> print(browser.title)
     Edit Tomcat 0.6.6.6 release details...
 
     >>> browser.getControl('Changelog').value = 'Fixes 3 bugs.'
     >>> browser.getControl('Change').click()
-    >>> print browser.title
+    >>> print(browser.title)
     0.6.6.6 : Series trunk : Tomcat
 
 And if no-priv drives the series...
@@ -164,10 +164,10 @@ others:
     >>> browser = setupBrowser(auth='Basic no-priv@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/tomcat/trunk/0.6.6.6')
     >>> browser.getLink('Change release details', index=0).click()
-    >>> print browser.title
+    >>> print(browser.title)
     Edit Tomcat 0.6.6.6 release details...
 
     >>> browser.getControl('Changelog').value = 'Fixes 4 bugs.'
     >>> browser.getControl('Change').click()
-    >>> print browser.title
+    >>> print(browser.title)
     0.6.6.6 : Series trunk : Tomcat
diff --git a/lib/lp/registry/stories/productrelease/xx-productrelease-delete.txt b/lib/lp/registry/stories/productrelease/xx-productrelease-delete.txt
index 08aa581..31563f9 100644
--- a/lib/lp/registry/stories/productrelease/xx-productrelease-delete.txt
+++ b/lib/lp/registry/stories/productrelease/xx-productrelease-delete.txt
@@ -4,11 +4,11 @@ The main page of a product series includes a list of releases of that series.
 The 0.9.2 milestone has a release.
 
     >>> user_browser.open('http://launchpad.test/firefox/trunk')
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Series trunk : Mozilla Firefox
 
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'series-trunk'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'series-trunk')))
     Version                    Expected   Released     Summary
     Mozilla Firefox 0.9.2 ...  None       2004-10-15   ...
 
@@ -17,7 +17,7 @@ access the delete page. A user without the necessary rights won't see the
 link and cannot access the +delete page.
 
     >>> user_browser.getLink('0.9.2').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     0.9.2 "One (secure) Tree Hill" : Mozilla Firefox
 
     >>> user_browser.getLink(url='/firefox/trunk/0.9.2/+delete')
@@ -34,14 +34,14 @@ Salgado has the necessary rights, so he sees the link and the +delete page.
     >>> salgados_browser = setupBrowser(auth='Basic salgado@xxxxxxxxxx:test')
     >>> salgados_browser.open('http://launchpad.test/firefox/trunk/0.9.2')
     >>> salgados_browser.getLink('Delete release').click()
-    >>> print salgados_browser.title
+    >>> print(salgados_browser.title)
     Delete Mozilla Firefox 0.9.2...
 
 The 0.9.2 release has some files associated with it. Salgado reads that
 they will be deleted too.
 
     >>> text = extract_text(find_main_content(salgados_browser.contents))
-    >>> print text.encode('ASCII', 'backslashreplace')
+    >>> print(text.encode('ASCII', 'backslashreplace'))
     Delete Mozilla Firefox 0.9.2 \u201cOne (secure) Tree Hill\u201d
     ...
     Are you sure you want to delete the 0.9.2 release of
@@ -52,7 +52,7 @@ they will be deleted too.
 Salgado chooses the delete button, then reads that the action is successful.
 
     >>> salgados_browser.getControl('Delete Release').click()
-    >>> print salgados_browser.title
+    >>> print(salgados_browser.title)
     Series trunk : Mozilla Firefox
 
     >>> print_feedback_messages(salgados_browser.contents)
@@ -61,7 +61,7 @@ Salgado chooses the delete button, then reads that the action is successful.
 Milestone 0.9.2 no longer has a release. The release column explains that
 the milestone is inactive.
 
-    >>> print extract_text(
-    ...     find_tag_by_id(salgados_browser.contents, 'series-trunk'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(salgados_browser.contents, 'series-trunk')))
     Version                   Expected              Released      Summary
     Mozilla Firefox 0.9.2...  Set date  Change details   This is an inactive ...
diff --git a/lib/lp/registry/stories/productrelease/xx-productrelease-rdf.txt b/lib/lp/registry/stories/productrelease/xx-productrelease-rdf.txt
index 5eb96c5..a620ac3 100644
--- a/lib/lp/registry/stories/productrelease/xx-productrelease-rdf.txt
+++ b/lib/lp/registry/stories/productrelease/xx-productrelease-rdf.txt
@@ -1,8 +1,8 @@
 Check that the productrelease RDF export works.
 
-    >>> print http(r"""
+    >>> print(http(r"""
     ... GET /firefox/trunk/0.9/+rdf HTTP/1.1
-    ... """)
+    ... """))
     HTTP/1.1 200 Ok
     Content-Disposition: attachment; filename="firefox-trunk-0.9.rdf"
     Content-Length: ...
diff --git a/lib/lp/registry/stories/productrelease/xx-productrelease-view.txt b/lib/lp/registry/stories/productrelease/xx-productrelease-view.txt
index 8c41ffd..05f00e4 100644
--- a/lib/lp/registry/stories/productrelease/xx-productrelease-view.txt
+++ b/lib/lp/registry/stories/productrelease/xx-productrelease-view.txt
@@ -1,11 +1,11 @@
 Any user can see a release for a series.
 
     >>> anon_browser.open('http://launchpad.test/firefox/trunk/0.9.2')
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     0.9.2 "One (secure) Tree Hill" : Series trunk : Mozilla Firefox
 
     >>> content = find_main_content(anon_browser.contents)
-    >>> print extract_text(find_tag_by_id(content, 'Release-details'))
+    >>> print(extract_text(find_tag_by_id(content, 'Release-details')))
     Milestone information
     Project: Mozilla Firefox
     Series: trunk
@@ -21,11 +21,11 @@ Any user can see a table describing the files that are associated with the
 release. Each file is linked.
 
     >>> table = find_tag_by_id(content, 'downloads')
-    >>> print extract_text(table)
+    >>> print(extract_text(table))
     File                            Description  Downloads
     firefox_0.9.2.orig.tar.gz (md5)              -
 
-    >>> print table.a
+    >>> print(table.a)
     <a href=".../firefox/trunk/0.9.2/+download/firefox_0.9.2.orig.tar.gz"
        title="firefox_0.9.2.orig.tar.gz (9.5 MiB)">...
 
@@ -46,7 +46,7 @@ downloaded and the date of the last download on that table as well.
     >>> lfa.updateDownloadCount(date(2006, 5, 4), None, 1)
 
     >>> anon_browser.reload()
-    >>> print extract_text(find_tag_by_id(anon_browser.contents, 'downloads'))
+    >>> print(extract_text(find_tag_by_id(anon_browser.contents, 'downloads')))
     File                            Description  Downloads
     firefox_0.9.2.orig.tar.gz (md5)              1 last downloaded ...
                               Total downloads:   1
@@ -58,7 +58,7 @@ downloaded, so we can't say it was downloaded a few minutes/hours ago.
     >>> import pytz
     >>> lfa.updateDownloadCount(datetime.now(pytz.utc).date(), None, 4356)
     >>> anon_browser.reload()
-    >>> print extract_text(find_tag_by_id(anon_browser.contents, 'downloads'))
+    >>> print(extract_text(find_tag_by_id(anon_browser.contents, 'downloads')))
     File                            Description  Downloads
     firefox_0.9.2.orig.tar.gz (md5)              4,357 last downloaded today
                               Total downloads:   4,357
diff --git a/lib/lp/registry/stories/productseries/xx-productseries-add-and-edit.txt b/lib/lp/registry/stories/productseries/xx-productseries-add-and-edit.txt
index c5c938f..64c43bc 100644
--- a/lib/lp/registry/stories/productseries/xx-productseries-add-and-edit.txt
+++ b/lib/lp/registry/stories/productseries/xx-productseries-add-and-edit.txt
@@ -23,10 +23,10 @@ But Sample Person will and be able to add a series.
     >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/firefox')
     >>> browser.getLink('Register a series').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/firefox/+addseries
 
-    >>> print find_main_content(browser.contents).find('h1').renderContents()
+    >>> print(find_main_content(browser.contents).find('h1').renderContents())
     Register a new Mozilla Firefox release series
 
 After checking that the page +addseries is there, we try to add a new series.
@@ -43,7 +43,7 @@ Now we are redirected to the Overview page of the product series we just added
     >>> browser.url
     'http://launchpad.test/firefox/stable'
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'description'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'description')))
     Product series add testing
 
     >>> browser.getLink('lp://dev/~mark/firefox/release-0.9.2')
@@ -76,7 +76,7 @@ name already in use and an invalud release URL pattern:
 We'll get a nice error message for the three problems:
 
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     print extract_text(tag)
+    ...     print(extract_text(tag))
     There are 2 errors.
     1.0 is already in use by another series.
     Invalid release URL pattern.
@@ -97,7 +97,7 @@ of firefox:
 The new values are then shown in the series' page.
 
     >>> content = find_tag_by_id(browser.contents, 'series-details')
-    >>> print extract_text(find_tag_by_id(content, 'series-name'))
+    >>> print(extract_text(find_tag_by_id(content, 'series-name')))
     Series: unstable
 
 And if we try to add another series with the same name to same product, we
@@ -110,7 +110,7 @@ should get a nice error message.
     >>> browser.getControl('Register Series').click()
 
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print message.renderContents()
+    ...     print(message.renderContents())
     There is 1 error.
     unstable is already in use by another series.
 
@@ -123,7 +123,7 @@ we can create structural bug subscriptions.
 
     >>> browser.open('http://launchpad.test/firefox/unstable')
     >>> browser.getLink('Subscribe to bug mail').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/firefox/unstable/+subscribe
-    >>> print browser.title
+    >>> print(browser.title)
     Subscribe : Series unstable : Bugs : Mozilla Firefox
diff --git a/lib/lp/registry/stories/productseries/xx-productseries-delete.txt b/lib/lp/registry/stories/productseries/xx-productseries-delete.txt
index b551c96..b1c867d 100644
--- a/lib/lp/registry/stories/productseries/xx-productseries-delete.txt
+++ b/lib/lp/registry/stories/productseries/xx-productseries-delete.txt
@@ -8,19 +8,19 @@ is really happening in the 1.0 series.
 
     >>> owner_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
     >>> owner_browser.open('http://launchpad.test/firefox/trunk')
-    >>> print owner_browser.title
+    >>> print(owner_browser.title)
     Series trunk : Mozilla Firefox
 
     >>> owner_browser.getLink('Delete series').click()
-    >>> print owner_browser.title
+    >>> print(owner_browser.title)
     Delete Mozilla Firefox trunk series...
 
 The trunk series is the focus of development. It cannot be deleted.
 The owner learns that they must make another series the focus of development
 first. There is no delete button on the page.
 
-    >>> print extract_text(
-    ...     find_tag_by_id(owner_browser.contents, 'cannot-delete'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(owner_browser.contents, 'cannot-delete')))
     You cannot delete a series that is the focus of development. Make
     another series the focus of development before deleting this one.
     You cannot delete a series that is linked to packages in distributions.
@@ -37,12 +37,12 @@ bogus.
 
     >>> owner_browser.getLink('Cancel').click()
     >>> owner_browser.open('http://launchpad.test/firefox/+edit')
-    >>> print owner_browser.title
+    >>> print(owner_browser.title)
     Change Mozilla Firefox's details...
 
     >>> owner_browser.getControl('Development focus').value = ['2']
     >>> owner_browser.getControl('Change').click()
-    >>> print owner_browser.title
+    >>> print(owner_browser.title)
     Mozilla Firefox in Launchpad
 
     >>> owner_browser.getLink('All packages').click()
@@ -57,35 +57,35 @@ deleted. The milestones and releases are linked.
 
     >>> owner_browser.getLink('trunk series').click()
     >>> owner_browser.getLink('Delete series').click()
-    >>> print owner_browser.title
+    >>> print(owner_browser.title)
     Delete Mozilla Firefox trunk series...
 
     >>> contents = find_main_content(owner_browser.contents)
-    >>> print extract_text(find_tag_by_id(contents, 'milestones-and-files'))
+    >>> print(extract_text(find_tag_by_id(contents, 'milestones-and-files')))
     The associated milestones and releases
     and their files will be also be deleted:
 
-    >>> print extract_text(find_tag_by_id(contents, 'milestones'))
+    >>> print(extract_text(find_tag_by_id(contents, 'milestones')))
     0.9.2 "One (secure) Tree Hill"
     0.9.1 "One Tree Hill (v2)"
     0.9 "One Tree Hill"
     1.0
 
-    >>> print owner_browser.getLink('0.9.2 "One (secure) Tree Hill"')
+    >>> print(owner_browser.getLink('0.9.2 "One (secure) Tree Hill"'))
     <Link text='0.9.2 "One (secure) Tree Hill"' ...>
 
-    >>> print extract_text(find_tag_by_id(contents, 'files'))
+    >>> print(extract_text(find_tag_by_id(contents, 'files')))
     firefox_0.9.2.orig.tar.gz
 
-    >>> print extract_text(
-    ... find_tag_by_id(contents, 'bugtasks-and-blueprints'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(contents, 'bugtasks-and-blueprints')))
     Support E4X in EcmaScript
 
 The owner deletes the series and the project page is displayed. They read
 that the series was deleted, and can not see a link to it anymore.
 
     >>> owner_browser.getControl('Delete this Series').click()
-    >>> print owner_browser.title
+    >>> print(owner_browser.title)
     Mozilla Firefox in Launchpad
 
     >>> print_feedback_messages(owner_browser.contents)
@@ -101,10 +101,10 @@ release manager sees the explanation when they try to delete the series.
 
     >>> owner_browser.open('http://launchpad.test/evolution/trunk')
     >>> owner_browser.getLink('Delete series').click()
-    >>> print owner_browser.title
+    >>> print(owner_browser.title)
     Delete Evolution trunk series ...
 
-    >>> print extract_text(
-    ...     find_tag_by_id(owner_browser.contents, 'cannot-delete'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(owner_browser.contents, 'cannot-delete')))
     You ...
     This series cannot be deleted because it has translations.
diff --git a/lib/lp/registry/stories/productseries/xx-productseries-driver.txt b/lib/lp/registry/stories/productseries/xx-productseries-driver.txt
index bb36017..6910665 100644
--- a/lib/lp/registry/stories/productseries/xx-productseries-driver.txt
+++ b/lib/lp/registry/stories/productseries/xx-productseries-driver.txt
@@ -8,16 +8,16 @@ by appointing someone else as the driver.
     >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/firefox/1.0')
     >>> content  = find_tag_by_id(browser.contents, 'series-details')
-    >>> print extract_text(find_tag_by_id(content, 'series-drivers'))
+    >>> print(extract_text(find_tag_by_id(content, 'series-drivers')))
     Project drivers: Sample Person
-    >>> print extract_text(find_tag_by_id(content, 'series-release-manager'))
+    >>> print(extract_text(find_tag_by_id(content, 'series-release-manager')))
     Release manager: None Appoint release manager
 
     >>> browser.getLink('Appoint release manager').click()
 
     >>> browser.url
     'http://launchpad.test/firefox/1.0/+driver'
-    >>> print browser.title
+    >>> print(browser.title)
     Appoint the release manager for...
     >>> browser.getControl('Release manager').value
     ''
@@ -38,8 +38,8 @@ message explains that the driver changed.
 Sample Person and Guilherme Salgado are listed as the drivers of Firefox 1.0.
 
     >>> content  = find_tag_by_id(browser.contents, 'series-details')
-    >>> print extract_text(find_tag_by_id(content, 'series-drivers'))
+    >>> print(extract_text(find_tag_by_id(content, 'series-drivers')))
     Project drivers: Guilherme Salgado, Sample Person
-    >>> print extract_text(find_tag_by_id(content, 'series-release-manager'))
+    >>> print(extract_text(find_tag_by_id(content, 'series-release-manager')))
     Release manager: Guilherme Salgado Appoint release manager
 
diff --git a/lib/lp/registry/stories/productseries/xx-productseries-index.txt b/lib/lp/registry/stories/productseries/xx-productseries-index.txt
index bbd88b8..7a4470c 100644
--- a/lib/lp/registry/stories/productseries/xx-productseries-index.txt
+++ b/lib/lp/registry/stories/productseries/xx-productseries-index.txt
@@ -4,14 +4,14 @@ Product Series Overview
 The product series overview page summarises the series.
 
     >>> anon_browser.open('http://launchpad.test/firefox/trunk')
-    >>> print extract_text(anon_browser.title)
+    >>> print(extract_text(anon_browser.title))
     Series trunk : Mozilla Firefox
 
     >>> content = find_main_content(anon_browser.contents)
-    >>> print extract_text(content.h1)
+    >>> print(extract_text(content.h1))
     Mozilla Firefox trunk series
 
-    >>> print extract_text(find_tag_by_id(content, 'series-details'))
+    >>> print(extract_text(find_tag_by_id(content, 'series-details')))
     Series information
     Project: Mozilla Firefox
     Series: trunk
@@ -22,7 +22,7 @@ The product series overview page summarises the series.
     Release URL pattern: None
     Download RDF metadata
 
-    >>> print extract_text(find_tag_by_id(content, 'description'))
+    >>> print(extract_text(find_tag_by_id(content, 'description')))
     The "trunk" series represents the primary line of
     development rather than a stable release branch. This is sometimes
     also called MAIN or HEAD.
@@ -52,7 +52,7 @@ The series page lists the milestones and releases for the series.
 
     >>> rows = find_tag_by_id(content, 'series-trunk').findAll('tr')
     >>> for row in rows[0:2]:
-    ...     print extract_text(row)
+    ...     print(extract_text(row))
     Version                          Expected   Released     Summary
     Mozilla Firefox 0.9.2 "One ...   None       2004-10-15   This was a ...
 
@@ -62,7 +62,7 @@ The driver can see a link to set the expected date.
     >>> driver_rows = find_tag_by_id(
     ...     driver_content, 'series-trunk').findAll('tr')
     >>> for row in driver_rows[0:2]:
-    ...     print extract_text(row)
+    ...     print(extract_text(row))
     Version                          Expected             Released    Summary
     Mozilla Firefox 0.9.2 "One ...   Set date Chang...    2004-10-16  ...
 
@@ -72,13 +72,13 @@ The driver can see a link to set the expected date.
 The milestone summary column in the table may also contain a summary of
 the status of the bugs and blueprints.
 
-    >>> print extract_text(rows[-1])
+    >>> print(extract_text(rows[-1]))
     Mozilla Firefox 1.0  2056-10-16   not yet released
     Blueprints targeted: 1 Unknown
 
 The driver can see a link to create a release for the milestone.
 
-    >>> print extract_text(driver_rows[-1])
+    >>> print(extract_text(driver_rows[-1]))
     Mozilla Firefox 1.0   2056-10-16  Release now   Blueprints targeted: ...
 
     >>> driver_browser.getLink('Release now')
@@ -96,13 +96,13 @@ The user can learn where the code of the series is located if a branch
 is set. Otherwise, there is a message explaining that the information has
 not been set.
 
-    >>> print extract_text(find_tag_by_id(content, 'branch-details'))
+    >>> print(extract_text(find_tag_by_id(content, 'branch-details')))
     No revision control details recorded for Mozilla Firefox trunk series.
 
 The driver sees that they can link a branch to this series, and there is
 an explanation where they can push the branch.
 
-    >>> print extract_text(find_tag_by_id(driver_content, 'branch-details'))
+    >>> print(extract_text(find_tag_by_id(driver_content, 'branch-details')))
     You haven't yet told Launchpad where your source code is ...
     bzr push lp:~name12/firefox/trunk ...
 
@@ -112,11 +112,11 @@ an explanation where they can push the branch.
 Distribution packaging is listed too. There is a link to the source package
 in each Ubuntu series.
 
-    >>> print extract_text(find_tag_by_id(
-    ...     content, 'distribution-packaging-explanation'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     content, 'distribution-packaging-explanation')))
     This series is packaged in the following distribution series:
 
-    >>> print extract_text(find_tag_by_id(content, 'distribution-packaging'))
+    >>> print(extract_text(find_tag_by_id(content, 'distribution-packaging')))
     Ubuntu Warty mozilla-firefox
 
     >>> anon_browser.getLink('Ubuntu Warty mozilla-firefox')
@@ -127,8 +127,8 @@ If there are no sourcepackages, any user can see there are none:
     >>> anon_2_browser = setupBrowser()
     >>> anon_2_browser.open('http://launchpad.test/thunderbird/trunk')
     >>> thunderbird_content = find_main_content(anon_2_browser.contents)
-    >>> print extract_text(find_tag_by_id(
-    ...     thunderbird_content, 'distribution-packaging-explanation'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     thunderbird_content, 'distribution-packaging-explanation')))
     This series is not packaged in any distribution series.
 
 
diff --git a/lib/lp/registry/stories/productseries/xx-productseries-rdf.txt b/lib/lp/registry/stories/productseries/xx-productseries-rdf.txt
index aa2d4d8..6d142db 100644
--- a/lib/lp/registry/stories/productseries/xx-productseries-rdf.txt
+++ b/lib/lp/registry/stories/productseries/xx-productseries-rdf.txt
@@ -1,8 +1,8 @@
 Check that the productseries RDF export works.
 
-    >>> print http(r"""
+    >>> print(http(r"""
     ... GET /firefox/trunk/+rdf HTTP/1.1
-    ... """)
+    ... """))
     HTTP/1.1 200 Ok
     Content-Disposition: attachment; filename="firefox-trunk.rdf"
     Content-Length: ...
diff --git a/lib/lp/registry/stories/productseries/xx-productseries-review.txt b/lib/lp/registry/stories/productseries/xx-productseries-review.txt
index b9449c0..fcc2db8 100644
--- a/lib/lp/registry/stories/productseries/xx-productseries-review.txt
+++ b/lib/lp/registry/stories/productseries/xx-productseries-review.txt
@@ -1,6 +1,6 @@
 Foo Bar changes the productseries named 'failedbranch' from the product a52dec
 to bazaar. Also changes the name of the productseries to 'newname'.
-  >>> print http(r"""
+  >>> print(http(r"""
   ... POST /a52dec/failedbranch/+review HTTP/1.1
   ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
   ... Referer: https://launchpad.test/
@@ -19,14 +19,14 @@ to bazaar. Also changes the name of the productseries to 'newname'.
   ...
   ... Change
   ... -----------------------------10572808480422220968425074--
-  ... """)
+  ... """))
   HTTP/1.1 303 See Other
   ...
   Location: http://localhost/bazaar/newname...
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /bazaar/newname HTTP/1.1
   ... Authorization: Basic Zm9vLmJhckBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...
diff --git a/lib/lp/registry/stories/productseries/xx-productseries-series.txt b/lib/lp/registry/stories/productseries/xx-productseries-series.txt
index fe0b3ab..6d983d5 100644
--- a/lib/lp/registry/stories/productseries/xx-productseries-series.txt
+++ b/lib/lp/registry/stories/productseries/xx-productseries-series.txt
@@ -19,7 +19,7 @@ Each project has a page that lists the status of all its series.
 
     >>> anon_browser.open('http://launchpad.test/firefox')
     >>> anon_browser.getLink('View full history').click()
-    >>> print anon_browser.title
+    >>> print(anon_browser.title)
     timeline...
 
 Each series, the status, milestones, releases, bugs and blueprints are
@@ -27,7 +27,7 @@ listed.
 
     >>> content = find_main_content(anon_browser.contents)
     >>> series_trunk = find_tag_by_id(content, 'series-trunk')
-    >>> print extract_text(series_trunk)
+    >>> print(extract_text(series_trunk))
     trunk series Focus of Development
     Latest milestones: 1.0    Latest releases: 0.9.2, 0.9.1, 0.9
     Bugs targeted: None
@@ -37,31 +37,31 @@ listed.
 Any user can see that the trunk series is the focus of development and that
 it is highlighted.
 
-    >>> print ' '.join(series_trunk['class'])
+    >>> print(' '.join(series_trunk['class']))
     highlight series
 
 The 1.0 series is not the focus of development, it is active, so it is not
 highlighted.
 
     >>> series_1_0 = find_tag_by_id(content, 'series-1-0')
-    >>> print extract_text(series_1_0)
+    >>> print(extract_text(series_1_0))
     1.0 series Active Development
     Latest releases: 1.0.0
     Bugs targeted: 1 New
     Blueprints targeted: None
     The 1.0 branch of the Mozilla web browser. Currently, this is the ...
 
-    >>> print ' '.join(series_1_0['class'])
+    >>> print(' '.join(series_1_0['class']))
     series
 
 Any user can see that obsolete series are lowlight. Obsolete series do not
 show bug status counts because it is expensive to retrieve the information.
 
     >>> series_xxx = find_tag_by_id(content, 'series-xxx')
-    >>> print extract_text(series_xxx)
+    >>> print(extract_text(series_xxx))
     xxx series Obsolete
     Blueprints targeted: None
     Use true GTK UI.
 
-    >>> print ' '.join(series_xxx['class'])
+    >>> print(' '.join(series_xxx['class']))
     lowlight series
diff --git a/lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt b/lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt
index 77965ab..c311585 100644
--- a/lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt
+++ b/lib/lp/registry/stories/productseries/xx-productseries-set-branch.txt
@@ -40,7 +40,7 @@ A user can choose to link to an existing branch on Launchpad.
     >>> browser.getControl('Update').click()
     >>> print_feedback_messages(browser.contents)
     Series code location updated.
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/firefox/trunk
 
 
@@ -62,7 +62,7 @@ the branch name to use in Launchpad, and the branch owner.
     >>> browser.getControl('Update').click()
     >>> print_feedback_messages(browser.contents)
     Code import created and branch linked to the series.
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/firefox/trunk
 
 The process is the same for a Git external branch, though the novel
@@ -77,7 +77,7 @@ The process is the same for a Git external branch, though the novel
     >>> browser.getControl('Update').click()
     >>> print_feedback_messages(browser.contents)
     Code import created and branch linked to the series.
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/firefox/trunk
 
 Likewise Subversion can use the "svn://" scheme.
@@ -91,7 +91,7 @@ Likewise Subversion can use the "svn://" scheme.
     >>> browser.getControl('Update').click()
     >>> print_feedback_messages(browser.contents)
     Code import created and branch linked to the series.
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/firefox/trunk
 
 The branch owner can be the logged in user or one of their teams.
@@ -106,12 +106,12 @@ The branch owner can be the logged in user or one of their teams.
     >>> browser.getControl('Update').click()
     >>> print_feedback_messages(browser.contents)
     Code import created and branch linked to the series.
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/firefox/trunk
     >>> login('test@xxxxxxxxxxxxx')
     >>> firefox_trunk = firefox.getSeries('trunk')
-    >>> print firefox_trunk.branch.unique_name
+    >>> print(firefox_trunk.branch.unique_name)
     ~hwdb-team/firefox/git-firefox-branch
-    >>> print firefox_trunk.branch.owner.name
+    >>> print(firefox_trunk.branch.owner.name)
     hwdb-team
     >>> logout()
diff --git a/lib/lp/registry/stories/project/xx-project-add-product.txt b/lib/lp/registry/stories/project/xx-project-add-product.txt
index c3b2614..b01d6c5 100644
--- a/lib/lp/registry/stories/project/xx-project-add-product.txt
+++ b/lib/lp/registry/stories/project/xx-project-add-product.txt
@@ -8,7 +8,7 @@ bottom of the project page to add a new product:
     >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/gnome')
     >>> browser.getLink('Register a project in GNOME').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/gnome/+newproduct
 
 Now we'll fill in the product details and add it.  This is a two-step
@@ -19,13 +19,13 @@ us on the same page.
     >>> browser.getControl(name='field.name', index=0).value = 'eog'
     >>> browser.getControl(name='field.summary').value = 'An image viewer for GNOME'
     >>> browser.getControl('Continue').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/gnome/+newproduct
 
     >>> browser.getControl(name='field.description').value = 'Blah blah blah'
     >>> browser.getControl(name='field.licenses').value = ['GNU_GPL_V2']
     >>> browser.getControl('Complete Registration').click()
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/eog
 
 Now let's get the page for that product to make sure it's there.  It
diff --git a/lib/lp/registry/stories/project/xx-project-add.txt b/lib/lp/registry/stories/project/xx-project-add.txt
index e0bcb64..481c57b 100644
--- a/lib/lp/registry/stories/project/xx-project-add.txt
+++ b/lib/lp/registry/stories/project/xx-project-add.txt
@@ -56,5 +56,5 @@ Now we add a new project.
   'http://launchpad.test/kde'
 
   >>> anon_browser.open(admin_browser.url)
-  >>> print anon_browser.title
+  >>> print(anon_browser.title)
   K Desktop Environment in Launchpad
diff --git a/lib/lp/registry/stories/project/xx-project-driver.txt b/lib/lp/registry/stories/project/xx-project-driver.txt
index 3bdc89d..b6fc7b5 100644
--- a/lib/lp/registry/stories/project/xx-project-driver.txt
+++ b/lib/lp/registry/stories/project/xx-project-driver.txt
@@ -5,7 +5,7 @@ Appoint Sample Person as the driver of the GNOME Project.
 
     >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/gnome/+driver')
-    >>> print browser.title
+    >>> print(browser.title)
     Appoint the driver for...
 
     >>> browser.getControl('Driver').value = 'name12'
@@ -23,7 +23,7 @@ message informs them of the driver change.
 
 Sample Person is listed as the driver of the project.
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'driver'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'driver')))
     Driver:
     Sample Person
     Edit
diff --git a/lib/lp/registry/stories/project/xx-project-edit.txt b/lib/lp/registry/stories/project/xx-project-edit.txt
index 2a7b710..7b5b696 100644
--- a/lib/lp/registry/stories/project/xx-project-edit.txt
+++ b/lib/lp/registry/stories/project/xx-project-edit.txt
@@ -6,7 +6,7 @@ The maintainer of a project can edit its details.
     >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/gnome')
     >>> browser.getLink('Change details').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Change project group details...
 
     >>> soup = find_main_content(browser.contents)
@@ -17,7 +17,7 @@ The maintainer of a project can edit its details.
     >>> browser.getControl(name='field.bugtracker').value = 'mozilla.org'
     >>> browser.getControl('Change Details').click()
 
-    >>> print browser.url
+    >>> print(browser.url)
     http://launchpad.test/gnome
 
 Regular users can't access the +review page.
@@ -32,10 +32,10 @@ But administrators can access the page:
     >>> admin_browser.open('http://launchpad.test/gnome')
     >>> admin_browser.getLink('Administer').click()
 
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://launchpad.test/gnome/+review
 
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     Change project group details : New Name
 
 Mark the project as reviewed and change the name.
@@ -44,19 +44,19 @@ Mark the project as reviewed and change the name.
     >>> admin_browser.getControl(name='field.name').value = 'new-name'
     >>> admin_browser.getControl('Change').click()
 
-    >>> print admin_browser.url
+    >>> print(admin_browser.url)
     http://launchpad.test/new-name
 
 The project summary shows the status as reviewed for admins only.
 
-    >>> print extract_text(find_tag_by_id(admin_browser.contents, 'status'))
+    >>> print(extract_text(find_tag_by_id(admin_browser.contents, 'status')))
     Status: Active Reviewed
 
 Other users cannot see the Project group status in the details portlet.
 
     >>> anon_browser.open('http://launchpad.test/new-name')
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'portlet-details'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'portlet-details')))
     Project group information
     Maintainer:
     Sample Person
@@ -80,21 +80,21 @@ of each other, as well as adding aliases to the project group.
     >>> admin_browser.getControl('Change').click()
 
     >>> admin_browser.getLink('Administer').click()
-    >>> print admin_browser.getControl('Aliases').value
+    >>> print(admin_browser.getControl('Aliases').value)
     old-name
 
     >>> admin_browser.goBack()
 
 The project maintainer and registrant are now updated.
 
-    >>> print extract_text(
-    ...     find_tag_by_id(admin_browser.contents, 'maintainer'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'maintainer')))
     Maintainer:
     Celso Providelo
     Edit
 
-    >>> print extract_text(
-    ...     find_tag_by_id(admin_browser.contents, 'registration'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'registration')))
     Registered ... by David Allouche
 
 The registrant really should only be a person, not a team, but that
@@ -106,8 +106,8 @@ teams as registrants.
     >>> admin_browser.getControl('Registrant').value = 'registry'
     >>> admin_browser.getControl('Change').click()
 
-    >>> print extract_text(
-    ...     find_tag_by_id(admin_browser.contents, 'registration'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'registration')))
     Registered ... by Registry Administrators
 
 Registry experts
@@ -138,7 +138,7 @@ there are fewer fields available.
 
     >>> expert_browser.open('http://launchpad.test/new-name')
     >>> expert_browser.getLink('Administer').click()
-    >>> print expert_browser.url
+    >>> print(expert_browser.url)
     http://launchpad.test/new-name/+review
 
     >>> expert_browser.getControl('Maintainer')
@@ -160,5 +160,5 @@ there are fewer fields available.
 
     >>> expert_browser.open('http://launchpad.test/newer-name')
     >>> expert_browser.getLink('Administer').click()
-    >>> print expert_browser.getControl('Aliases').value
+    >>> print(expert_browser.getControl('Aliases').value)
     sleepy
diff --git a/lib/lp/registry/stories/project/xx-project-index.txt b/lib/lp/registry/stories/project/xx-project-index.txt
index 25fa18d..9439d73 100644
--- a/lib/lp/registry/stories/project/xx-project-index.txt
+++ b/lib/lp/registry/stories/project/xx-project-index.txt
@@ -27,7 +27,7 @@ The overview page for project groups is accessible to all users.
 The page lists the member projects, together with the releases/milestones of
 its development focus.
 
-    >>> print extract_text(find_tag_by_id(anon_browser.contents, 'products'))
+    >>> print(extract_text(find_tag_by_id(anon_browser.contents, 'products')))
     Projects
     Evolution
     GNOME Terminal
@@ -37,7 +37,7 @@ its development focus.
 
 The projects are linked.
 
-    >>> print anon_browser.getLink('gnomebaker').url
+    >>> print(anon_browser.getLink('gnomebaker').url)
     http://launchpad.test/gnomebaker
 
 The project overview page contains a link to register new products with the
@@ -130,7 +130,7 @@ the new product form for the project group.
 
     >>> admin_browser.getLink(
     ...     'register another project that is part of Test Group').click()
-    >>> print admin_browser.title
+    >>> print(admin_browser.title)
     Register a project in Launchpad...
 
 
@@ -143,7 +143,7 @@ that project.
     >>> browser.open('http://launchpad.test/mozilla')
     >>> products = find_tags_by_class(browser.contents, 'sprite product')
     >>> for product in products:
-    ...     print product
+    ...     print(product)
     <a...Mozilla Firefox</a>
     <a...Mozilla Thunderbird</a>
 
@@ -165,7 +165,7 @@ Inactive products are not included in that list, though.
     >>> browser.open('http://launchpad.test/mozilla')
     >>> products = find_tags_by_class(browser.contents, 'sprite product')
     >>> for product in products:
-    ...     print product
+    ...     print(product)
     <a...Mozilla Thunderbird</a>
 
     >>> firefox.active = True
@@ -180,7 +180,7 @@ can create structural bug subscriptions.
 
     >>> user_browser.open('http://launchpad.test/mozilla')
     >>> user_browser.getLink('Subscribe to bug mail').click()
-    >>> print user_browser.url
+    >>> print(user_browser.url)
     http://launchpad.test/mozilla/+subscribe
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Subscribe : Bugs : The Mozilla Project
diff --git a/lib/lp/registry/stories/project/xx-project-rdf.txt b/lib/lp/registry/stories/project/xx-project-rdf.txt
index 52c78dc..790e8be 100644
--- a/lib/lp/registry/stories/project/xx-project-rdf.txt
+++ b/lib/lp/registry/stories/project/xx-project-rdf.txt
@@ -3,7 +3,7 @@ We export DOAP RDF metadata for projects from a link in the
 
     >>> anon_browser.open("http://launchpad.test/mozilla";)
     >>> anon_browser.getLink("Download RDF metadata").click()
-    >>> print anon_browser.contents
+    >>> print(anon_browser.contents)
     <?xml version="1.0" encoding="utf-8"...
     <rdf:RDF ...xmlns:lp="https://launchpad.net/rdf/launchpad#";...
     <lp:Project>
@@ -25,5 +25,5 @@ It's valid XML and RDF:
 
     >>> from xml.dom.minidom import parseString
     >>> document = parseString(str(anon_browser.contents))
-    >>> print document.documentElement.nodeName
+    >>> print(document.documentElement.nodeName)
     rdf:RDF
diff --git a/lib/lp/registry/stories/project/xx-reassign-project.txt b/lib/lp/registry/stories/project/xx-reassign-project.txt
index 001da8a..c643d82 100644
--- a/lib/lp/registry/stories/project/xx-reassign-project.txt
+++ b/lib/lp/registry/stories/project/xx-reassign-project.txt
@@ -4,10 +4,10 @@
   Logged in as no-priv@xxxxxxxxxxxxx we can't do that, because they're not the
   owner of the project nor a member of admins.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /mozilla/+reassign HTTP/1.1
   ... Authorization: Basic no-priv@xxxxxxxxxxxxx:test
-  ... """)
+  ... """))
   HTTP/1.1 403 Forbidden
   ...
 
@@ -15,10 +15,10 @@
   Now we're logged in as mark@xxxxxxxxxxx and he's the owner of the admins team,
   so he can do everything.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /mozilla/+reassign HTTP/1.1
   ... Authorization: Basic mark@xxxxxxxxxxx:test
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...
   ...Current:...
@@ -29,13 +29,13 @@
 
   Here he changes the owner to himself.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... POST /mozilla/+reassign HTTP/1.1
   ... Authorization: Basic mark@xxxxxxxxxxx:test
   ... Referer: https://launchpad.test/
   ...
   ... field.owner=mark&field.existing=existing"""
-  ... r"""&field.actions.change=Change""")
+  ... r"""&field.actions.change=Change"""))
   HTTP/1.1 303 See Other
   ...
   Location: http://localhost/mozilla
@@ -45,10 +45,10 @@
 
   Here we see the new owner: Mark Shuttleworth
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /mozilla/ HTTP/1.1
   ... Authorization: Basic mark@xxxxxxxxxxx:test
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   Content-Length: ...
   Content-Type: text/html;charset=utf-8
diff --git a/lib/lp/registry/stories/team-polls/create-poll-options.txt b/lib/lp/registry/stories/team-polls/create-poll-options.txt
index 2af52e0..e15aad6 100644
--- a/lib/lp/registry/stories/team-polls/create-poll-options.txt
+++ b/lib/lp/registry/stories/team-polls/create-poll-options.txt
@@ -37,8 +37,8 @@ After adding an options we're taken back to the poll's home page.
 And here we see the option listed as one of the active options for this
 poll.
 
-    >>> print extract_text(
-    ...     find_tag_by_id(team_admin_browser.contents, 'options'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(team_admin_browser.contents, 'options')))
     Name        Title           Active
     bill...     Bill Graham     Yes
 
diff --git a/lib/lp/registry/stories/team-polls/edit-options.txt b/lib/lp/registry/stories/team-polls/edit-options.txt
index 38d2ec4..547f384 100644
--- a/lib/lp/registry/stories/team-polls/edit-options.txt
+++ b/lib/lp/registry/stories/team-polls/edit-options.txt
@@ -7,7 +7,7 @@ team's administrators:
     >>> user_browser.getLink('A public poll that never closes').click()
     >>> user_browser.url
     'http://launchpad.test/~ubuntu-team/+poll/never-closes4'
-    >>> print extract_text(find_tag_by_id(user_browser.contents, 'options'))
+    >>> print(extract_text(find_tag_by_id(user_browser.contents, 'options')))
     Name        Title       Active
     OptionA     OptionA     Yes
     ...
@@ -50,7 +50,8 @@ been opened yet, he should be able to edit its options.
 
     >>> browser.url
     'http://launchpad.test/~ubuntu-team/+poll/not-yet-opened'
-    >>> print find_portlet(browser.contents, 'Voting options').renderContents()
+    >>> print(
+    ...     find_portlet(browser.contents, 'Voting options').renderContents())
     <BLANKLINE>
     <h2>Voting options</h2>
     ...
diff --git a/lib/lp/registry/stories/team-polls/edit-poll.txt b/lib/lp/registry/stories/team-polls/edit-poll.txt
index fc4a9de..ca63501 100644
--- a/lib/lp/registry/stories/team-polls/edit-poll.txt
+++ b/lib/lp/registry/stories/team-polls/edit-poll.txt
@@ -73,8 +73,8 @@ Trying to edit a poll that's already open isn't possible.
 
     >>> team_admin_browser.open(
     ...     'http://launchpad.test/~ubuntu-team/+poll/never-closes/+edit')
-    >>> print extract_text(
-    ...     find_tag_by_id(team_admin_browser.contents, 'not-editable'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(team_admin_browser.contents, 'not-editable')))
     This poll can't be edited...
 
 It's also not possible to edit a poll that's already closed.
@@ -92,6 +92,6 @@ It's also not possible to edit a poll that's already closed.
     True
 
     >>> team_admin_browser.getLink('Change details').click()
-    >>> print extract_text(
-    ...     find_tag_by_id(team_admin_browser.contents, 'not-editable'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(team_admin_browser.contents, 'not-editable')))
     This poll can't be edited...
diff --git a/lib/lp/registry/stories/team-polls/vote-poll.txt b/lib/lp/registry/stories/team-polls/vote-poll.txt
index e6185a7..aa9de0b 100644
--- a/lib/lp/registry/stories/team-polls/vote-poll.txt
+++ b/lib/lp/registry/stories/team-polls/vote-poll.txt
@@ -10,7 +10,7 @@ that they must use to see/change their vote afterwards.
     >>> browser.url
     'http://launchpad.test/~ubuntu-team/+poll/never-closes/+vote'
 
-    >>> print find_tag_by_id(browser.contents, 'your-vote').renderContents()
+    >>> print(find_tag_by_id(browser.contents, 'your-vote').renderContents())
     <BLANKLINE>
     ...
     <h2>Your current vote</h2>
@@ -26,11 +26,11 @@ that they must use to see/change their vote afterwards.
 
     >>> tags = find_tags_by_class(browser.contents, "informational message")
     >>> for tag in tags:
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Your vote has been recorded. If you want to view or change it later you
     must write down this key: ...
 
-    >>> print find_tag_by_id(browser.contents, 'your-vote').renderContents()
+    >>> print(find_tag_by_id(browser.contents, 'your-vote').renderContents())
     <BLANKLINE>
     ...
     <h2>Your current vote</h2>
@@ -44,7 +44,7 @@ Foo Bar will now vote on a poll with public votes.
     >>> browser.url
     'http://launchpad.test/~ubuntu-team/+poll/never-closes4/+vote'
 
-    >>> print find_tag_by_id(browser.contents, 'your-vote').renderContents()
+    >>> print(find_tag_by_id(browser.contents, 'your-vote').renderContents())
     <BLANKLINE>
     ...
     <h2>Your current vote</h2>
@@ -60,11 +60,11 @@ Foo Bar will now vote on a poll with public votes.
 
     >>> tags = find_tags_by_class(browser.contents, "informational message")
     >>> for tag in tags:
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Your vote was stored successfully. You can come back to this page at any
     time before this poll closes to view or change your vote, if you want.
 
-    >>> print find_tag_by_id(browser.contents, 'your-vote').renderContents()
+    >>> print(find_tag_by_id(browser.contents, 'your-vote').renderContents())
     <BLANKLINE>
     ...
     <h2>Your current vote</h2>
@@ -88,10 +88,10 @@ yet.
 
     >>> contents = team_admin_browser.contents
     >>> for tag in find_tags_by_class(contents, "informational message"):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     You chose not to vote yet.
 
-    >>> print find_tag_by_id(contents, 'your-vote').renderContents()
+    >>> print(find_tag_by_id(contents, 'your-vote').renderContents())
     <BLANKLINE>
     ...
     <h2>Your current vote</h2>
@@ -110,7 +110,7 @@ even if they guess the URL for the voting page.
     ...     'http://launchpad.test/~ubuntu-team/+poll/never-closes/+vote')
     >>> for tag in find_tags_by_class(
     ...     non_member_browser.contents, "informational message"):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     You can’t vote in this poll because you’re not a member of Ubuntu Team.
 
 
@@ -120,8 +120,8 @@ It's not possible to vote on closed polls, even if we manually craft the URL.
 
     >>> team_admin_browser.open(
     ...     'http://launchpad.test/~ubuntu-team/+poll/leader-2004')
-    >>> print find_tag_by_id(
-    ...     team_admin_browser.contents, 'maincontent').renderContents()
+    >>> print(find_tag_by_id(
+    ...     team_admin_browser.contents, 'maincontent').renderContents())
     <BLANKLINE>
     ...
     <h2>Voting has closed</h2>
@@ -129,8 +129,8 @@ It's not possible to vote on closed polls, even if we manually craft the URL.
 
     >>> team_admin_browser.open(
     ...     'http://launchpad.test/~ubuntu-team/+poll/leader-2004/+vote')
-    >>> print find_tag_by_id(
-    ...     team_admin_browser.contents, 'maincontent').renderContents()
+    >>> print(find_tag_by_id(
+    ...     team_admin_browser.contents, 'maincontent').renderContents())
     <BLANKLINE>
     ...
     <p class="informational message">
@@ -148,8 +148,8 @@ The same is true for condorcet polls too.
 
     >>> team_admin_browser.open(
     ...     'http://launchpad.test/~ubuntu-team/+poll/director-2004')
-    >>> print find_tag_by_id(
-    ...     team_admin_browser.contents, 'maincontent').renderContents()
+    >>> print(find_tag_by_id(
+    ...     team_admin_browser.contents, 'maincontent').renderContents())
     <BLANKLINE>
     ...
     <h2>Voting has closed</h2>
diff --git a/lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt b/lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt
index 5c3b515..1bf2413 100644
--- a/lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt
+++ b/lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt
@@ -4,20 +4,20 @@
   Go to a condorcet-style poll (which is still open) and check that apart
   from seeing our vote we can also change it.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /~ubuntu-team/+poll/never-closes2 HTTP/1.1
   ... Accept-Language: en-us,en;q=0.5
   ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
-  ... """)
+  ... """))
   HTTP/1.1 303 See Other
   ...
   Location: http://localhost/~ubuntu-team/+poll/never-closes2/+vote
   ...
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
   ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...
   ...You must enter your vote key...
@@ -31,10 +31,10 @@
   If a non-member (Sample Person) guesses the voting URL and tries to vote,
   they won't be allowed.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
   ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...You can&#8217;t vote in this poll because you&#8217;re not...
   ...a member of Ubuntu Team...
@@ -42,13 +42,13 @@
 
   By providing the token we will be able to see our current vote.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... POST /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
   ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
   ... Content-Type: application/x-www-form-urlencoded
   ... Referer: https://launchpad.test/
   ...
-  ... token=xn9FDCTp4m&showvote=Show+My+Vote&option_12=&option_13=&option_14=&option_15=""")
+  ... token=xn9FDCTp4m&showvote=Show+My+Vote&option_12=&option_13=&option_14=&option_15="""))
   HTTP/1.1 200 Ok
   ...
                   <p>Your current vote is as follows:</p>
@@ -78,13 +78,13 @@
 
   It's also possible to change the vote, if wanted.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... POST /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
   ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
   ... Content-Type: application/x-www-form-urlencoded
   ... Referer: https://launchpad.test/
   ...
-  ... token=xn9FDCTp4m&option_12=2&option_13=3&option_14=4&option_15=1&changevote=Change+Vote""")
+  ... token=xn9FDCTp4m&option_12=2&option_13=3&option_14=4&option_15=1&changevote=Change+Vote"""))
   HTTP/1.1 200 Ok
   ...
   ...Your vote was changed successfully.</p>
@@ -120,19 +120,19 @@
   Now we go to another poll in which name16 voted. But this time it's a public
   one, so there's no need to provide the token to see the current vote.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /~ubuntu-team/+poll/never-closes3 HTTP/1.1
   ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
-  ... """)
+  ... """))
   HTTP/1.1 303 See Other
   ...
   Location: http://localhost/~ubuntu-team/+poll/never-closes3/+vote
   ...
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
   ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...
                   <p>Your current vote is as follows:</p>
@@ -166,13 +166,13 @@
   Now we change the vote and we see the new vote displayed as our current
   vote.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... POST /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
   ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
   ... Content-Type: application/x-www-form-urlencoded
   ... Referer: https://launchpad.test/
   ...
-  ... option_16=4&option_17=2&option_18=1&option_19=3&changevote=Change+Vote""")
+  ... option_16=4&option_17=2&option_18=1&option_19=3&changevote=Change+Vote"""))
   HTTP/1.1 200 Ok
   ...
                   <p>Your current vote is as follows:</p>
@@ -207,10 +207,10 @@
   condorcet-style poll that's still open and get redirected to a page where
   it's possible to vote (and see the current vote).
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /~ubuntu-team/+poll/never-closes3 HTTP/1.1
   ... Authorization: Basic mark@xxxxxxxxxxx:test
-  ... """)
+  ... """))
   HTTP/1.1 303 See Other
   ...
   Location: http://localhost/~ubuntu-team/+poll/never-closes3/+vote
@@ -220,10 +220,10 @@
   And here we'll see the form which says you haven't voted yet and allows you
   to vote.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
   ... Authorization: Basic mark@xxxxxxxxxxx:test
-  ... """)
+  ... """))
   HTTP/1.1 200 Ok
   ...
   ...Your current vote...
diff --git a/lib/lp/registry/stories/team-polls/xx-poll-confirm-vote.txt b/lib/lp/registry/stories/team-polls/xx-poll-confirm-vote.txt
index 8d33202..27b6cb3 100644
--- a/lib/lp/registry/stories/team-polls/xx-poll-confirm-vote.txt
+++ b/lib/lp/registry/stories/team-polls/xx-poll-confirm-vote.txt
@@ -3,10 +3,10 @@
 
   >>> import base64
   >>> jdub_auth = base64.b64encode('jeff.waugh@xxxxxxxxxxxxxxx:test')
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /~ubuntu-team/+poll/director-2004 HTTP/1.1
   ... Authorization: Basic %s
-  ... """ % jdub_auth)
+  ... """ % jdub_auth))
   HTTP/1.1 200 Ok
   ...
   ...2004 Director's Elections...
@@ -22,13 +22,13 @@
   Now let's see if jdub's vote was stored correctly, by entering the token he
   got when voting.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... POST /~ubuntu-team/+poll/director-2004 HTTP/1.1
   ... Authorization: Basic %s
   ... Referer: https://launchpad.test/
   ... Content-Type: application/x-www-form-urlencoded
   ... 
-  ... token=9WjxQq2V9p&showvote=Show+My+Vote""" % jdub_auth)
+  ... token=9WjxQq2V9p&showvote=Show+My+Vote""" % jdub_auth))
   HTTP/1.1 200 Ok
   ...
                 <p>Your vote was as follows:</p>
@@ -62,10 +62,10 @@
   Now we'll see the results of the leader-2004 poll, in which jdub also
   voted.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... GET /~ubuntu-team/+poll/leader-2004 HTTP/1.1
   ... Authorization: Basic %s
-  ... """ % jdub_auth)
+  ... """ % jdub_auth))
   HTTP/1.1 200 Ok
   ...
   ...2004 Leader's Elections...
@@ -78,13 +78,13 @@
 
   And now we confirm his vote on this poll too.
 
-  >>> print http(r"""
+  >>> print(http(r"""
   ... POST /~ubuntu-team/+poll/leader-2004 HTTP/1.1
   ... Authorization: Basic %s
   ... Referer: https://launchpad.test/
   ... Content-Type: application/x-www-form-urlencoded
   ... 
-  ... token=W7gR5mjNrX&showvote=Show+My+Vote""" % jdub_auth)
+  ... token=W7gR5mjNrX&showvote=Show+My+Vote""" % jdub_auth))
   HTTP/1.1 200 Ok
   ...
               <p>Your vote was for
diff --git a/lib/lp/registry/stories/team-polls/xx-poll-results.txt b/lib/lp/registry/stories/team-polls/xx-poll-results.txt
index a2a96a6..286eeaa 100644
--- a/lib/lp/registry/stories/team-polls/xx-poll-results.txt
+++ b/lib/lp/registry/stories/team-polls/xx-poll-results.txt
@@ -2,7 +2,7 @@ First we check all polls of 'ubuntu-team'.
 
   >>> anon_browser.open("http://launchpad.test/~ubuntu-team";)
   >>> anon_browser.getLink('Show polls').click()
-  >>> print find_main_content(anon_browser.contents)
+  >>> print(find_main_content(anon_browser.contents))
   <...
   ...Current polls...
   ...A random poll that never closes...
@@ -16,7 +16,7 @@ First we check all polls of 'ubuntu-team'.
   Check the results of a closed simple-style poll.
 
   >>> anon_browser.open("http://launchpad.test/~ubuntu-team/+poll/leader-2004";)
-  >>> print find_main_content(anon_browser.contents)
+  >>> print(find_main_content(anon_browser.contents))
   <...
   ...Who's going to be the next leader?...
   ...Results...
@@ -44,7 +44,7 @@ First we check all polls of 'ubuntu-team'.
   Check the results of a closed condorcet-style poll.
 
   >>> anon_browser.open("http://launchpad.test/~ubuntu-team/+poll/director-2004";)
-  >>> print find_main_content(anon_browser.contents)
+  >>> print(find_main_content(anon_browser.contents))
   <...
   ...Who's going to be the next director?...
   ...Results...
diff --git a/lib/lp/registry/stories/team/xx-team-add-my-teams.txt b/lib/lp/registry/stories/team/xx-team-add-my-teams.txt
index 1a99bd5..e61a187 100644
--- a/lib/lp/registry/stories/team/xx-team-add-my-teams.txt
+++ b/lib/lp/registry/stories/team/xx-team-add-my-teams.txt
@@ -16,7 +16,7 @@ home page.
 For moderated teams, it's only possible to propose another team as a member.
 The proposal will have to be reviewed by a team administrator.
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'candidates'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'candidates')))
     This is a moderated team, so one of its administrators will have
     to review any memberships you propose.
     GuadaMen
@@ -35,8 +35,8 @@ We'll now propose Ubuntu Team as a member of the Hoary Gnome Team (name21).
     'Hoary Gnome Team in Launchpad'
     >>> print_feedback_messages(browser.contents)
     Ubuntu Team has been proposed to this team.
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'recently-proposed'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'recently-proposed')))
     Pending approval
     Ubuntu Team...
 
@@ -52,19 +52,19 @@ of trying to propose the membership, which is an invalid status change.
     >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
     >>> super_team = factory.makeTeam(name="super-team", owner=inviting_owner)
     >>> sub_team = factory.makeTeam(name="sub-team", owner=no_priv)
-    >>> print super_team.addMember(sub_team, inviting_owner)
+    >>> print(super_team.addMember(sub_team, inviting_owner))
     (True, <DBItem TeamMembershipStatus.INVITED...)
     >>> Store.of(sub_team).flush()
     >>> logout()
     >>> user_browser.open('http://launchpad.test/~super-team/+add-my-teams')
     >>> user_browser.getControl(name='field.teams').value = ['sub-team']
     >>> user_browser.getControl('Continue').click()
-    >>> print user_browser.title
+    >>> print(user_browser.title)
     Super Team in Launchpad
     >>> print_feedback_messages(user_browser.contents)
     Sub Team has been added to this team because of an existing invite.
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'recently-approved'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'recently-approved')))
     Latest members
     Sub Team
 
@@ -85,8 +85,8 @@ members.
     'Hoary Gnome Team in Launchpad'
     >>> print_feedback_messages(browser.contents)
     Ubuntu Team has been added to this team.
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'recently-approved'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'recently-approved')))
     Latest members
     Ubuntu Team...
 
@@ -107,8 +107,8 @@ your teams as members.
     LinkNotFoundError
 
     >>> browser.open('http://launchpad.test/~ubuntu-team/+add-my-teams')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'candidates'), formatter='html')
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'candidates'), formatter='html'))
     This is a restricted team
     New members can not be proposed&mdash;they can only be added by one
     of the team's administrators.
@@ -152,5 +152,5 @@ anymore of the Hoary Gnome Team:
 And when no teams can be added, a message is displayed:
 
     >>> browser.open('http://launchpad.test/~name21/+add-my-teams')
-    >>> print extract_text(find_tag_by_id(browser.contents, 'no-candidates'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'no-candidates')))
     None of the teams you administer can be added to this team.
diff --git a/lib/lp/registry/stories/team/xx-team-claim.txt b/lib/lp/registry/stories/team/xx-team-claim.txt
index e18bb82..21a9015 100644
--- a/lib/lp/registry/stories/team/xx-team-claim.txt
+++ b/lib/lp/registry/stories/team/xx-team-claim.txt
@@ -82,8 +82,8 @@ claim process the team's owner.
     LookupError:...
     >>> from lp.services.beautifulsoup import BeautifulSoup
     >>> soup = BeautifulSoup(user_browser.contents)
-    >>> print extract_text(
-    ...     soup.find(attrs={'for': 'field.teamowner'}).findPrevious('tr'))
+    >>> print(extract_text(
+    ...     soup.find(attrs={'for': 'field.teamowner'}).findPrevious('tr')))
     Team Owner: No Privileges Person...
 
     >>> user_browser.getControl('Display Name').value = 'Ubuntu Doc Team'
@@ -98,7 +98,7 @@ page.
     >>> print_feedback_messages(user_browser.contents)
     Team claimed successfully
 
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'team-owner'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'team-owner')))
     Owner:
     No Privileges Person
diff --git a/lib/lp/registry/stories/team/xx-team-contactemail-xss.txt b/lib/lp/registry/stories/team/xx-team-contactemail-xss.txt
index 1c93d0b..37e7328 100644
--- a/lib/lp/registry/stories/team/xx-team-contactemail-xss.txt
+++ b/lib/lp/registry/stories/team/xx-team-contactemail-xss.txt
@@ -18,8 +18,8 @@ is parse-able:
 
 The markup is valid and correctly escaped:
 
-    >>> print find_tag_by_id(
-    ...     admin_browser.contents, 'field.contact_address').prettify()
+    >>> print(find_tag_by_id(
+    ...     admin_browser.contents, 'field.contact_address').prettify())
     <input class="textType" id="field.contact_address"
            name="field.contact_address" size="20" type="text"
            value='&lt;script&gt;alert("cheezburger");&lt;/script&gt;'/>
@@ -27,7 +27,7 @@ The markup is valid and correctly escaped:
 The error message is also valid and correctly escaped:
 
     >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print tag.prettify()
+    ...     print(tag.prettify())
     <p class="error message">
     ...
     <div class="message">
diff --git a/lib/lp/registry/stories/team/xx-team-home.txt b/lib/lp/registry/stories/team/xx-team-home.txt
index 4be0a91..9720e3e 100644
--- a/lib/lp/registry/stories/team/xx-team-home.txt
+++ b/lib/lp/registry/stories/team/xx-team-home.txt
@@ -4,90 +4,90 @@ A team's home page
 The home page of a public team is visible to everyone.
 
     >>> browser.open('http://launchpad.test/~ubuntu-team')
-    >>> print browser.title
+    >>> print(browser.title)
     Ubuntu Team in Launchpad
     >>> privacy_info = find_tag_by_id(browser.contents, 'privacy')
-    >>> print extract_text(privacy_info)
+    >>> print(extract_text(privacy_info))
     Public team
 
 The page contains a few interesting details about team membership.
 
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'recently-approved'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'recently-approved')))
     Latest members
     Warty Gnome Team
     Daniel Silverstone
     Celso Providelo
     Steve Alexander...
 
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'recently-proposed'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'recently-proposed')))
     Pending approval
     Sample Person
     Andrew Bennetts
 
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'recently-invited'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'recently-invited')))
     Latest invited
     Warty Security Team
 
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'team-owner'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'team-owner')))
     Owner:
     Mark Shuttleworth
 
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'team-datecreated'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'team-datecreated')))
     Created on:
     2005-06-06
 
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'subscription-policy'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'subscription-policy')))
     Membership policy:
     Moderated Team
 
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'membership-summary'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'membership-summary')))
     10 active members, 1 invited member, 2 proposed members...
 
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'contact-email'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'contact-email')))
     Email:
     Log in for email information.
 
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'contact-languages'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'contact-languages')))
     Languages:
     English
 
 The polls portlet is only shown if current polls exist.
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'polls'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'polls')))
     Polls
     A random poll that never closes...
     Show polls
 
     >>> browser.open('http://launchpad.test/~launchpad')
-    >>> print find_tag_by_id(browser.contents, 'polls')
+    >>> print(find_tag_by_id(browser.contents, 'polls'))
     None
 
 The subteam-of portlet is not shown if the team is not a subteam.
 
     >>> browser.open('http://launchpad.test/~ubuntu-team')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'subteam-of'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'subteam-of')))
     Subteam of “Ubuntu Team” is a member of these teams: GuadaMen...
 
     >>> browser.open('http://launchpad.test/~launchpad')
-    >>> print find_tag_by_id(browser.contents, 'subteam-of')
+    >>> print(find_tag_by_id(browser.contents, 'subteam-of'))
     None
 
 Unless the user is the owner of the team and there are invitations to
 join a team.
 
     >>> admin_browser.open('http://launchpad.test/~launchpad')
-    >>> print extract_text(
-    ...     find_tag_by_id(admin_browser.contents, 'subteam-of'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(admin_browser.contents, 'subteam-of')))
     Subteam of...
     itself is not a member of any other team.
     Show received invitations
@@ -97,34 +97,34 @@ members, the empty lists are hidden using the "hidden" css class:
 
     >>> browser.open('http://launchpad.test/~launchpad')
     >>> tag = find_tag_by_id(browser.contents, 'recently-approved')
-    >>> print ' '.join(tag['class'])
+    >>> print(' '.join(tag['class']))
     hidden
 
     >>> tag = find_tag_by_id(browser.contents, 'recently-proposed')
-    >>> print ' '.join(tag['class'])
+    >>> print(' '.join(tag['class']))
     hidden
 
     >>> tag = find_tag_by_id(browser.contents, 'recently-invited')
-    >>> print ' '.join(tag['class'])
+    >>> print(' '.join(tag['class']))
     hidden
 
 In the above case there's no user logged in, so it doesn't actually show
 what's the user's involvement with the team. If the user logs in, they'll
 see that, though.
 
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'your-involvement'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'your-involvement')))
     You must log in to join or leave this team.
 
     >>> browser = setupBrowser(auth='Basic foo.bar@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/~guadamen')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'your-involvement'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'your-involvement')))
     You own this team...
 
     >>> browser.open('http://launchpad.test/~rosetta-admins')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'your-involvement'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'your-involvement')))
     You indirectly own this team.
     You are not currently a member...
 
@@ -132,13 +132,13 @@ see that, though.
     >>> browser.getControl('New member').value = 'admins'
     >>> browser.getControl('Add Member').click()
     >>> browser.open('http://launchpad.test/~rosetta-admins')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'your-involvement'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'your-involvement')))
     You indirectly own this team.
 
     >>> browser.open('http://launchpad.test/~ubuntu-team')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'your-involvement'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'your-involvement')))
     You are a member of this team...
 
 Member can contact their team even if the team does not have a contact
@@ -146,13 +146,13 @@ address:
 
     >>> sample_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
     >>> sample_browser.open('http://launchpad.test/~landscape-developers')
-    >>> print extract_text(
-    ...     find_tag_by_id(sample_browser.contents, 'contact-email'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(sample_browser.contents, 'contact-email')))
     Email:
     None, members emailed directly
     Set contact address
-    >>> print extract_text(
-    ...     find_tag_by_id(sample_browser.contents, 'contact-user'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(sample_browser.contents, 'contact-user')))
     Contact this team's members
 
 As teams do not have OpenID Logins, there is no link in the Contact
@@ -167,8 +167,8 @@ If the logged in user is an indirect member of the team, we'll say that and
 will even show the path from the user to the team.
 
     >>> sample_browser.open('http://launchpad.test/~name18')
-    >>> print extract_text(
-    ...     find_tag_by_id(sample_browser.contents, 'your-involvement'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(sample_browser.contents, 'your-involvement')))
     You are an indirect member of this team:
     Sample Person → Warty Security Team → Ubuntu Gnome Team...
 
@@ -178,7 +178,7 @@ team.  Notice that the output of mugshots is batched.
     >>> anon_browser.open('http://launchpad.test/~ubuntu-team/')
     >>> anon_browser.getLink('Show member photos').click()
     >>> main_content = find_main_content(anon_browser.contents)
-    >>> print main_content
+    >>> print(main_content)
     <...
     <h1>Member photos</h1>
     ...
@@ -193,8 +193,8 @@ Team owners and admins can see a link to approve and decline applicants.
 
     >>> owner_browser = setupBrowser(auth="Basic foo.bar@xxxxxxxxxxxxx:test")
     >>> owner_browser.open('http://launchpad.test/~ubuntu-team')
-    >>> print extract_text(
-    ...     find_tag_by_id(owner_browser.contents, 'recently-proposed'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(owner_browser.contents, 'recently-proposed')))
     Pending approval
     Sample Person
     Andrew Bennetts
@@ -210,20 +210,20 @@ Non members
 No Privileges Person is not a member of the Ubuntu team.
 
     >>> user_browser.open('http://launchpad.test/~ubuntu-team')
-    >>> print extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'your-involvement'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'your-involvement')))
     Join...
     You are not a member of this team...
 
 They can see the contact address, and the link explains the email
 will actually go to the team's administrators.
 
-    >>> print extract_text(find_tag_by_id(
-    ...     user_browser.contents, 'contact-email'))
+    >>> print(extract_text(find_tag_by_id(
+    ...     user_browser.contents, 'contact-email')))
     Email:
     support@xxxxxxxxxx
     >>> content = find_tag_by_id(user_browser.contents, 'contact-user')
-    >>> print extract_text(content)
+    >>> print(extract_text(content))
     Contact this team's admins
 
     >>> content.a
diff --git a/lib/lp/registry/stories/team/xx-team-membership.txt b/lib/lp/registry/stories/team/xx-team-membership.txt
index c035799..2cebe0c 100644
--- a/lib/lp/registry/stories/team/xx-team-membership.txt
+++ b/lib/lp/registry/stories/team/xx-team-membership.txt
@@ -12,7 +12,7 @@ administrator and his subscription never expires.
     >>> url = '/~ubuntu-team/+member/kamion'
     >>> browser.getLink(url=url).click()
 
-    >>> print browser.title
+    >>> print(browser.title)
     Colin Watson's membership : ...Ubuntu Team... team
     >>> "Active member" in six.ensure_text(browser.contents)
     True
@@ -48,7 +48,7 @@ TestBrowser to manually re-enable the input. That's what the
 We get a nice error message
 
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Invalid expiration: Invalid date value
 
 Give up on change, nothing should have changed with Colin:
@@ -66,7 +66,7 @@ Give up on change, nothing should have changed with Colin:
     ...     kamion, ubuntu_team)
     >>> kamion_membership.status.title
     'Administrator'
-    >>> print kamion_membership.dateexpires
+    >>> print(kamion_membership.dateexpires)
     None
     >>> logout()
 
@@ -93,11 +93,11 @@ We're redirected to the +members page
     >>> login(ANONYMOUS)
     >>> kamion_membership = teammembershipset.getByPersonAndTeam(
     ...     kamion, ubuntu_team)
-    >>> print kamion_membership.status.title
+    >>> print(kamion_membership.status.title)
     Approved
     >>> kamion_membership.dateexpires.date() == expire_date.date()
     True
-    >>> print kamion_membership.last_change_comment
+    >>> print(kamion_membership.last_change_comment)
     Arfie
     >>> logout()
 
@@ -121,7 +121,7 @@ the administrator control on the membership page.
     >>> url = '/~ubuntu-team/+member/jdub'
     >>> jdub_browser.getLink(url=url).click()
 
-    >>> print jdub_browser.title
+    >>> print(jdub_browser.title)
     Jeff Waugh's membership : ...Ubuntu Team... team
     >>> "Active member" in six.ensure_text(jdub_browser.contents)
     True
@@ -189,14 +189,14 @@ But in the second browser with the stale data we get an error message:
 An admin can see the former members of the team.
 
     >>> browser.open('http://launchpad.test/~name18/+members')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'inactivemembers'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'inactivemembers')))
     Name    Joined in   Status...
 
 Other users cannot see the former members of the team.
 
     >>> user_browser.open('http://launchpad.test/~name18/+members')
-    >>> print find_tag_by_id(user_browser.contents, 'inactivemembers')
+    >>> print(find_tag_by_id(user_browser.contents, 'inactivemembers'))
     None
 
 
@@ -209,21 +209,21 @@ member, as well as the teams in which they are an indirect member.
 Kiko has not joined any teams:
 
     >>> anon_browser.open('http://launchpad.test/~kiko/+participation')
-    >>> print extract_text(
-    ...     find_tag_by_id(anon_browser.contents, 'no-participation'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(anon_browser.contents, 'no-participation')))
     Christian Reis has not yet joined any teams.
-    >>> print find_tag_by_id(anon_browser.contents, 'participation')
+    >>> print(find_tag_by_id(anon_browser.contents, 'participation'))
     None
 
 Sample Person has both direct and indirect memberships:
 
     >>> anon_browser.open('http://launchpad.test/~name12/+participation')
     >>> content = find_main_content(anon_browser.contents)
-    >>> print find_tag_by_id(content, 'no-participation')
+    >>> print(find_tag_by_id(content, 'no-participation'))
     None
 
-    >>> print extract_text(
-    ...     find_tag_by_id(content, 'participation'), formatter='html')
+    >>> print(extract_text(
+    ...     find_tag_by_id(content, 'participation'), formatter='html'))
     Team                  Joined      Role    Via                 Mailing List
     HWDB Team             2009-07-09  Member  &mdash;              &mdash;
     Landscape Developers  2006-07-11  Owner   &mdash;              &mdash;
@@ -234,13 +234,13 @@ Sample Person has both direct and indirect memberships:
 User can see links to register teams and change their mailing list
 subscriptions on their own participation page.
 
-    >>> print find_tag_by_id(content, 'participation-actions')
+    >>> print(find_tag_by_id(content, 'participation-actions'))
     None
 
     >>> user_browser.open('http://launchpad.test/~no-priv/+participation')
     >>> actions = find_tag_by_id(
     ...     user_browser.contents, 'participation-actions')
-    >>> print extract_text(actions)
+    >>> print(extract_text(actions))
     Register a team
     Change mailing list subscriptions
 
@@ -253,8 +253,8 @@ Teams also have a participation page, but it does not include a mailing
 list column.
 
     >>> admin_browser.open('http://launchpad.test/~admins/+participation')
-    >>> print extract_text(
+    >>> print(extract_text(
     ...     find_tag_by_id(admin_browser.contents, 'participation'),
-    ...     formatter='html')
+    ...     formatter='html'))
     Team                  Joined      Role    Via
     Mailing List Experts  2007-10-04  Owner   &mdash;
diff --git a/lib/lp/registry/stories/teammembership/private-team.txt b/lib/lp/registry/stories/teammembership/private-team.txt
index 48f6a4b..f725951 100644
--- a/lib/lp/registry/stories/teammembership/private-team.txt
+++ b/lib/lp/registry/stories/teammembership/private-team.txt
@@ -33,7 +33,7 @@ admins and members of that team can see the team.
 The page indicates that the team is private.
 
     >>> privacy_info = find_tag_by_id(cprov_browser.contents, 'privacy')
-    >>> print extract_text(privacy_info)
+    >>> print(extract_text(privacy_info))
     Private team
 
 A normal user cannot see the team.
diff --git a/lib/lp/registry/stories/teammembership/xx-add-member.txt b/lib/lp/registry/stories/teammembership/xx-add-member.txt
index 94bf73e..686af11 100644
--- a/lib/lp/registry/stories/teammembership/xx-add-member.txt
+++ b/lib/lp/registry/stories/teammembership/xx-add-member.txt
@@ -13,7 +13,7 @@ Any administrator of a team can add new members to that team.
 
     >>> for tag in find_tags_by_class(browser.contents,
     ...                               'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Celso Providelo (cprov) has been added as a member of this team.
 
 Let's make sure that 'cprov' is now an Approved member of
@@ -44,7 +44,7 @@ become a member.
 
     >>> for tag in find_tags_by_class(browser.contents,
     ...                               'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Launchpad Developers (launchpad) has been invited to join this team.
 
 As we can see, the launchpad team will not be one of the team's active
@@ -65,7 +65,7 @@ team, not even if they manually craft the URL.
     >>> browser.open(
     ...     'http://launchpad.test/~landscape-developers/+member/launchpad')
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'not-responded'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'not-responded')))
     Launchpad Developers (launchpad) has been invited to join this team, but
     hasn't responded to the invitation yet.
 
@@ -98,7 +98,7 @@ rights to edit the membership in question) can do it.
 First, let's accept the invitation sent on behalf of Landscape Developers to
 the Launchpad Developers.
 
-    >>> print extract_text(find_tag_by_id(browser.contents, 'invitations'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'invitations')))
     Sent by         On behalf of
     Andrew Bennetts Landscape Developers
 
@@ -113,8 +113,8 @@ the Launchpad Developers.
 
     >>> browser.url
     'http://launchpad.test/~launchpad'
-    >>> print extract_text(
-    ...     find_tags_by_class(browser.contents, 'informational')[0])
+    >>> print(extract_text(
+    ...     find_tags_by_class(browser.contents, 'informational')[0]))
     This team is now a member of Landscape Developers.
 
 Now we'll decline the invitation sent on behalf of Ubuntu Team to
@@ -124,8 +124,8 @@ Warty Security Team:
     >>> browser.getControl('Decline').click()
     >>> browser.url
     'http://launchpad.test/~name20'
-    >>> print extract_text(
-    ...     find_tags_by_class(browser.contents, 'informational')[0])
+    >>> print(extract_text(
+    ...     find_tags_by_class(browser.contents, 'informational')[0]))
     Declined the invitation to join Ubuntu Team
 
 
@@ -147,7 +147,7 @@ First invite name20 to be a member of ubuntu-team.
 
     >>> for tag in find_tags_by_class(browser.contents,
     ...                               'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Warty Security Team (name20) has been invited to join this team.
 
 Open the invitations page with one admin browser.
@@ -169,7 +169,7 @@ Accept the invitation in the first browser.
 
     >>> for tag in find_tags_by_class(browser.contents,
     ...                               'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     This team is now a member of Ubuntu Team.
 
 Accepting the invitation in the second browser, redirects to the team page
@@ -181,5 +181,5 @@ and a message is displayed.
 
     >>> for tag in find_tags_by_class(second_browser.contents,
     ...                               'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     This invitation has already been processed.
diff --git a/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.txt b/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.txt
index 732ed37..eea7fb6 100644
--- a/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.txt
+++ b/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.txt
@@ -23,7 +23,7 @@ some changes.
     >>> membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
     ...     personset.getByName('karl'),
     ...     personset.getByName('ubuntu-mirror-admins'))
-    >>> print membership.dateexpires
+    >>> print(membership.dateexpires)
     None
     >>> membership.team.renewal_policy != TeamMembershipRenewalPolicy.ONDEMAND
     True
@@ -42,7 +42,7 @@ membership can't be renewed and explain why.
     ...
     LookupError: label ...'Renew'
     ...
-    >>> print extract_text(find_tag_by_id(browser.contents, 'maincontent'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'maincontent')))
     Renew membership of Karl Tilbury in Mirror Administrators
     This membership cannot be renewed because Mirror Administrators
     (ubuntu-mirror-admins) is not a team that allows its members to renew
@@ -72,7 +72,7 @@ the user to renew that membership because it's not about to expire.
     ...
     LookupError: label ...'Renew'
     ...
-    >>> print extract_text(find_tag_by_id(browser.contents, 'maincontent'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'maincontent')))
     Renew membership of Karl Tilbury in Mirror Administrators
     This membership cannot be renewed because it is not set to expire in
     7 days or less. You or one of the team administrators has already
@@ -104,7 +104,7 @@ of the expiry._control.attrs TestBrowser voodoo.
 
     >>> browser.open('http://launchpad.test/~karl/+expiringmembership/'
     ...              'ubuntu-mirror-admins')
-    >>> print extract_text(find_tag_by_id(browser.contents, 'maincontent'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'maincontent')))
     Renew membership of Karl Tilbury in Mirror Administrators
     This membership is going to expire ... from now. If you want to
     remain a member of Mirror Administrators, you must renew it.
@@ -117,7 +117,7 @@ Karl then renews his membership.
     'http://launchpad.test/~karl'
     >>> for tag in find_tags_by_class(
     ...         browser.contents, 'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Membership renewed until ...
 
 Karl can't renew it again, since it's now not set to expire soon.
@@ -129,7 +129,7 @@ Karl can't renew it again, since it's now not set to expire soon.
     ...
     LookupError: label ...'Renew'
     ...
-    >>> print extract_text(find_tag_by_id(browser.contents, 'maincontent'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'maincontent')))
     Renew membership of Karl Tilbury in Mirror Administrators
     This membership cannot be renewed because it is not set to expire in
     7 days or less. You or one of the team administrators has already
@@ -158,7 +158,7 @@ now renew the membership.
     >>> browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/~landscape-developers'
     ...              '/+expiringmembership/ubuntu-mirror-admins')
-    >>> print extract_text(find_tag_by_id(browser.contents, 'maincontent'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'maincontent')))
     Renew membership of Landscape Developers in Mirror Administrators
     This membership is going to expire ... from now. If you want this team
     to remain a member of Mirror Administrators, you must renew it.
@@ -169,7 +169,7 @@ now renew the membership.
     'http://launchpad.test/~landscape-developers'
     >>> for tag in find_tags_by_class(
     ...         browser.contents, 'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Membership renewed until ...
 
 If the user double clicks or goes back to a cached version of the page
@@ -184,7 +184,7 @@ provided no information as to whether the membership was renewed.
     'http://launchpad.test/~landscape-developers'
     >>> for tag in find_tags_by_class(
     ...         browser.contents, 'informational message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Membership renewed until ...
 
 When the page is loaded again, there is no form since the membership
@@ -192,7 +192,7 @@ will no longer be expiring soon.
 
     >>> browser.open('http://launchpad.test/~landscape-developers'
     ...              '/+expiringmembership/ubuntu-mirror-admins')
-    >>> print extract_text(find_tag_by_id(browser.contents, 'maincontent'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'maincontent')))
     Renew membership of Landscape Developers in Mirror Administrators
     This membership cannot be renewed because it is not set to expire in
     7 days or less. Somebody else has already renewed it.
diff --git a/lib/lp/registry/stories/teammembership/xx-renew-subscription.txt b/lib/lp/registry/stories/teammembership/xx-renew-subscription.txt
index d624aec..2aee569 100644
--- a/lib/lp/registry/stories/teammembership/xx-renew-subscription.txt
+++ b/lib/lp/registry/stories/teammembership/xx-renew-subscription.txt
@@ -6,10 +6,10 @@ the 'Renew' button.
 
     >>> browser = setupBrowser(auth='Basic mark@xxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/~name18/+member/mark')
-    >>> print browser.title
+    >>> print(browser.title)
     Mark Shuttleworth's membership : ...Ubuntu Gnome Team... team
     >>> content = find_main_content(browser.contents)
-    >>> print extract_text(content.p)
+    >>> print(extract_text(content.p))
     Mark Shuttleworth (mark) is an Expired Member of Ubuntu Gnome Team.
 
     >>> browser.getControl(name='expires').value = ['never']
@@ -19,10 +19,10 @@ He is redirected to the team page. He can see that his subscription
 is approved because he is in the active members table.
 
     >>> from lp.services.helpers import backslashreplace
-    >>> print backslashreplace(browser.title)
+    >>> print(backslashreplace(browser.title))
     Members : \u201cUbuntu Gnome Team\u201d team
     >>> content = find_tag_by_id(browser.contents, 'activemembers')
-    >>> print extract_text(content, formatter='html')
+    >>> print(extract_text(content, formatter='html'))
     Name               Member since  Expires  Status
     ...
     Mark Shuttleworth  2005-03-03    &ndash;  Approved
diff --git a/lib/lp/registry/stories/teammembership/xx-team-leave.txt b/lib/lp/registry/stories/teammembership/xx-team-leave.txt
index 699a9f3..8fc7d3d 100644
--- a/lib/lp/registry/stories/teammembership/xx-team-leave.txt
+++ b/lib/lp/registry/stories/teammembership/xx-team-leave.txt
@@ -12,7 +12,7 @@ by choosing the 'Leave' button.
     >>> admin_browser.title
     'Leave Ubuntu Team...
     >>> content = find_main_content(admin_browser.contents)
-    >>> print content.p
+    >>> print(content.p)
     <p>Are you sure you want to leave this team?</p>
     >>> admin_browser.getControl('Leave').click()
 
@@ -45,8 +45,8 @@ User is redirect to their homepage page after leaving.
     >>> browser.url
     'http://launchpad.test/~member'
 
-    >>> print extract_text(
-    ...     first_tag_by_class(browser.contents, 'informational message'))
+    >>> print(extract_text(
+    ...     first_tag_by_class(browser.contents, 'informational message')))
     You are no longer a member of private team...
 
 == Team overview page quick-links ==
@@ -56,14 +56,14 @@ team's overview page.
 
     >>> browser = setupBrowser(auth='Basic carlos@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/~admins')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'your-involvement'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'your-involvement')))
     You are a member of this team...
     >>> browser.getLink('Leave the Team').click()
     >>> browser.title
     'Leave Launchpad Administrators...
     >>> browser.getControl('Leave').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Launchpad Administrators in Launchpad
 
     # The 'Leave' link should be gone.
@@ -79,8 +79,8 @@ team's overview page.
 Team owners do not have the option to leave.
 
     >>> browser.open('http://launchpad.test/~testing-spanish-team')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'your-involvement'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'your-involvement')))
     You own this team...
     >>> browser.getLink('Leave the Team')
     Traceback (most recent call last):
diff --git a/lib/lp/registry/stories/teammembership/xx-teammembership.txt b/lib/lp/registry/stories/teammembership/xx-teammembership.txt
index 5652ceb..6080f1b 100644
--- a/lib/lp/registry/stories/teammembership/xx-teammembership.txt
+++ b/lib/lp/registry/stories/teammembership/xx-teammembership.txt
@@ -39,8 +39,8 @@ Karl will join the newly created team.
 
     >>> browser = setupBrowser(auth='Basic karl@xxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/~myemail')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'subscription-policy'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'subscription-policy')))
     Membership policy:
     Open Team
 
@@ -55,7 +55,7 @@ Karl will join the newly created team.
 Since this is an open team, he's automatically approved.
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     You have successfully joined your own team.
 
 Now, the link to join the team is not present anymore and the +join page will
@@ -68,7 +68,7 @@ team.
     LinkNotFoundError
     >>> browser.open('http://launchpad.test/~myemail/+join')
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     You are an active member of this team already.
 
 We have a 'Back' button, though, which just takes the user back to
@@ -90,8 +90,8 @@ approved, though.
     >>> browser = setupBrowser(
     ...     auth='Basic james.blackwell@xxxxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/~myemail')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'subscription-policy'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'subscription-policy')))
     Membership policy:
     Moderated Team
 
@@ -99,7 +99,7 @@ approved, though.
     >>> browser.url
     'http://launchpad.test/~myemail/+join'
 
-    >>> print find_tag_by_id(browser.contents, 'maincontent').renderContents()
+    >>> print(find_tag_by_id(browser.contents, 'maincontent').renderContents())
     <BLANKLINE>
     ...
     One of this team's administrators will have to approve your membership
@@ -121,7 +121,7 @@ hit the 'Cancel' button, going back to the team's page...
     'http://launchpad.test/~myemail'
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Your request to join your own team is awaiting approval.
 
 Delegated teams also require approval of direct membership.
@@ -133,8 +133,8 @@ Delegated teams also require approval of direct membership.
 
     >>> browser = setupBrowser(auth='Basic colin.watson@xxxxxxxxxxxxxxx:test')
     >>> browser.open('http://launchpad.test/~myemail')
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'subscription-policy'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'subscription-policy')))
     Membership policy:
     Delegated Team
 
@@ -142,7 +142,7 @@ Delegated teams also require approval of direct membership.
     >>> browser.url
     'http://launchpad.test/~myemail/+join'
 
-    >>> print find_tag_by_id(browser.contents, 'maincontent').renderContents()
+    >>> print(find_tag_by_id(browser.contents, 'maincontent').renderContents())
     <BLANKLINE>
     ...
     One of this team's administrators will have to approve your membership
@@ -154,7 +154,7 @@ Delegated teams also require approval of direct membership.
     'http://launchpad.test/~myemail'
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     Your request to join your own team is awaiting approval.
 
 If it was a restricted team, users wouldn't even see a link to join the team.
@@ -167,8 +167,8 @@ If it was a restricted team, users wouldn't even see a link to join the team.
     >>> browser.open('http://launchpad.test/~myemail')
     >>> browser.url
     'http://launchpad.test/~myemail'
-    >>> print extract_text(
-    ...     find_tag_by_id(browser.contents, 'subscription-policy'))
+    >>> print(extract_text(
+    ...     find_tag_by_id(browser.contents, 'subscription-policy')))
     Membership policy:
     Restricted Team
 
@@ -185,7 +185,7 @@ message explaining that this is a restricted team.
     'http://launchpad.test/~myemail/+join'
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     your own team is a restricted team.
     Only a team administrator can add new members.
 
@@ -207,12 +207,12 @@ be there at all.
 
     >>> contents = anon_browser.contents
     >>> for link in find_tag_by_id(contents, 'activemembers').findAll('a'):
-    ...     print link.renderContents()
+    ...     print(link.renderContents())
     Karl Tilbury
     Sample Person
 
     >>> for link in find_tag_by_id(contents, 'proposedmembers').findAll('a'):
-    ...     print link.renderContents()
+    ...     print(link.renderContents())
     Colin Watson
     James Blackwell
 
@@ -229,7 +229,7 @@ have been invited.
     ...     for link in table.findAll('a'):
     ...         link_contents = six.ensure_text(link.renderContents())
     ...         if link_contents != 'Edit' and not link.find('img'):
-    ...             print link_contents.encode('ascii', 'replace')
+    ...             print(link_contents.encode('ascii', 'replace'))
 
     >>> browser.open('http://launchpad.test/~landscape-developers')
     >>> browser.getLink('All members').click()
@@ -248,7 +248,7 @@ have been invited.
 
 Former members are only viewable by admins of the team.
 
-    >>> print find_tag_by_id(browser.contents, 'inactivemembers')
+    >>> print(find_tag_by_id(browser.contents, 'inactivemembers'))
     None
 
     >>> name12_browser = setupBrowser(
@@ -398,7 +398,7 @@ possible.
     >>> browser.url
     'http://launchpad.test/~myemail/+members'
 
-    >>> print find_tag_by_id(browser.contents, 'inactivemembers')
+    >>> print(find_tag_by_id(browser.contents, 'inactivemembers'))
     None
     >>> print_members(browser.contents, 'activemembers')
     James Blackwell
@@ -415,5 +415,5 @@ error message:
     >>> browser2.url
     'http://launchpad.test/~myemail/+member/karl/+index'
     >>> for tag in find_tags_by_class(browser2.contents, 'error message'):
-    ...     print tag.renderContents()
+    ...     print(tag.renderContents())
     The membership request for Karl Tilbury has already been processed.
diff --git a/lib/lp/registry/stories/webservice/xx-derivedistroseries.txt b/lib/lp/registry/stories/webservice/xx-derivedistroseries.txt
index 8677001..af88a08 100644
--- a/lib/lp/registry/stories/webservice/xx-derivedistroseries.txt
+++ b/lib/lp/registry/stories/webservice/xx-derivedistroseries.txt
@@ -63,10 +63,10 @@ has a parent series.
     >>> ws_child_series_with_parent = ws_object(
     ...     soyuz_team_webservice, child_series_with_parent)
 
-    >>> print soyuz_team_webservice.named_post(
+    >>> print(soyuz_team_webservice.named_post(
     ...     ws_child_series_with_parent['self_link'],
     ...     'initDerivedDistroSeries', parents=[str(other_series.id)],
-    ...     rebuild=False)
+    ...     rebuild=False))
     HTTP/1.1 400 Bad Request
     ...
     DistroSeries ... already has parent series.
@@ -75,10 +75,10 @@ If we call it correctly, it works.
 
     >>> ws_child_series = ws_object(soyuz_team_webservice, child_series)
 
-    >>> print soyuz_team_webservice.named_post(
+    >>> print(soyuz_team_webservice.named_post(
     ...     ws_child_series['self_link'], 'initDerivedDistroSeries',
     ...     parents=[str(parent_series.id)],
-    ...     rebuild=False)
+    ...     rebuild=False))
     HTTP/1.1 200 Ok
     ...
 
@@ -92,5 +92,5 @@ And we can verify that the job has been created.
     ...     getUtility(IInitializeDistroSeriesJobSource).iterReady(),
     ...     key=lambda x: x.distroseries.name)
     >>> for job in jobs:
-    ...     print job.distroseries.name
+    ...     print(job.distroseries.name)
     child1
diff --git a/lib/lp/registry/stories/webservice/xx-distribution-mirror.txt b/lib/lp/registry/stories/webservice/xx-distribution-mirror.txt
index 2737490..52227fc 100644
--- a/lib/lp/registry/stories/webservice/xx-distribution-mirror.txt
+++ b/lib/lp/registry/stories/webservice/xx-distribution-mirror.txt
@@ -93,13 +93,13 @@ Ensure that anonymous API sessions can view mirror listings; archive/releases.
     >>> archive_response = anon_webservice.get(
     ...     ubuntu['archive_mirrors_collection_link'])
     >>> anon_archive_mirrors = archive_response.jsonBody()
-    >>> print anon_archive_mirrors['total_size']
+    >>> print(anon_archive_mirrors['total_size'])
     5
 
     >>> cd_response = anon_webservice.get(
     ...     ubuntu['cdimage_mirrors_collection_link'])
     >>> anon_cd_mirrors = cd_response.jsonBody()
-    >>> print anon_cd_mirrors['total_size']
+    >>> print(anon_cd_mirrors['total_size'])
     4
 
 One must have special permissions to access certain attributes:
@@ -179,7 +179,7 @@ But if we use Karl, the mirror listing admin's, webservice, we can update the ow
     209
 
     >>> patched_canonical_archive = response.jsonBody()
-    >>> print patched_canonical_archive['owner_link']
+    >>> print(patched_canonical_archive['owner_link'])
     http://.../~karl
 
 Some attributes are read-only via the API:
@@ -194,7 +194,7 @@ Some attributes are read-only via the API:
     ... }
     >>> response = karl_webservice.patch(
     ...     canonical_releases['self_link'], 'application/json', dumps(patch))
-    >>> print response
+    >>> print(response)
     HTTP/1.1 400 Bad Request
     ...
     distribution_link: You tried to modify a read-only attribute.
@@ -252,7 +252,7 @@ or not.
 
     >>> is_official_mirror = webservice.named_get(canonical_releases['self_link'],
     ...     'isOfficial').jsonBody()
-    >>> print is_official_mirror
+    >>> print(is_official_mirror)
     False
 
 "getOverallFreshness" returns the freshness of the mirror determined by the
@@ -263,5 +263,5 @@ mirror prober from the mirror's last probe.
     ...     name='releases-mirror2').jsonBody()
     >>> freshness = webservice.named_get(releases_mirror2['self_link'],
     ...     'getOverallFreshness').jsonBody()
-    >>> print freshness
+    >>> print(freshness)
     Up to date
diff --git a/lib/lp/registry/stories/webservice/xx-distribution-source-package.txt b/lib/lp/registry/stories/webservice/xx-distribution-source-package.txt
index b6e9b01..418bed1 100644
--- a/lib/lp/registry/stories/webservice/xx-distribution-source-package.txt
+++ b/lib/lp/registry/stories/webservice/xx-distribution-source-package.txt
@@ -9,7 +9,7 @@ Source packages can be obtained from the context of a distribution.
     ...     debian['self_link'], 'getSourcePackage',
     ...     name='mozilla-firefox').jsonBody()
 
-    >>> print pretty(mozilla_firefox)
+    >>> print(pretty(mozilla_firefox))
     {u'bug_reported_acknowledgement': None,
      u'bug_reporting_guidelines': None,
      u'display_name': u'mozilla-firefox in Debian',
@@ -30,11 +30,11 @@ It's also possible to search for tasks with the "searchTasks" method:
     ...     'searchTasks', status='New').jsonBody()
 
     >>> for bug_task in bug_task_collection['entries']:
-    ...     print bug_task['title']
-    ...     print '%s, %s, <%s>' % (
+    ...     print(bug_task['title'])
+    ...     print('%s, %s, <%s>' % (
     ...         bug_task['status'], bug_task['importance'],
-    ...         bug_task['bug_link'])
-    ...     print '<%s>' % bug_task['self_link']
+    ...         bug_task['bug_link']))
+    ...     print('<%s>' % bug_task['self_link'])
     Bug #3 in mozilla-firefox (Debian): "Bug Title Test"
     New, Unknown, <http://api.launchpad.test/beta/bugs/3>
     <http://api.launchpad.test/beta/debian/+source/mozilla-firefox/+bug/3>
@@ -51,7 +51,7 @@ package.
     >>> upstream_product = webservice.get(
     ...     ubuntu_firefox['upstream_product_link']).jsonBody()
 
-    >>> print pretty(upstream_product)
+    >>> print(pretty(upstream_product))
     {...
      u'display_name': u'Mozilla Firefox'...
      u'self_link': u'http://.../firefox'...}
@@ -59,5 +59,5 @@ package.
 If the package isn't linked to an upstream product its
 upstream_product_link will be None.
 
-    >>> print mozilla_firefox['upstream_product_link']
+    >>> print(mozilla_firefox['upstream_product_link'])
     None
diff --git a/lib/lp/registry/stories/webservice/xx-distribution.txt b/lib/lp/registry/stories/webservice/xx-distribution.txt
index 3463c08..a578671 100644
--- a/lib/lp/registry/stories/webservice/xx-distribution.txt
+++ b/lib/lp/registry/stories/webservice/xx-distribution.txt
@@ -6,7 +6,7 @@ Ubuntu and its flavours being the first on the list.
 
     >>> distros = webservice.get("/distros").jsonBody()
     >>> for entry in distros['entries']:
-    ...     print entry['self_link']
+    ...     print(entry['self_link'])
     http://.../ubuntu
     http://.../kubuntu
     http://.../ubuntutest
@@ -75,14 +75,14 @@ Distribution has some custom operations.
     >>> series = webservice.named_get(
     ...     ubuntu['self_link'], 'getSeries',
     ...     name_or_version='hoary').jsonBody()
-    >>> print series['self_link']
+    >>> print(series['self_link'])
     http://.../ubuntu/hoary
 
 Requesting a series that does not exist is results in a not found error.
 
-    >>> print webservice.named_get(
+    >>> print(webservice.named_get(
     ...     ubuntu['self_link'], 'getSeries',
-    ...     name_or_version='fnord')
+    ...     name_or_version='fnord'))
     HTTP/1.1 404 Not Found
     ...
     No such distribution series: 'fnord'.
@@ -93,7 +93,7 @@ distribution that are marked as in development.
     >>> dev_series = webservice.named_get(
     ...     ubuntu['self_link'], 'getDevelopmentSeries').jsonBody()
     >>> for entry in sorted(dev_series['entries']):
-    ...     print entry['self_link']
+    ...     print(entry['self_link'])
     http://.../ubuntu/hoary
 
 "getMilestone" returns a milestone for the given name, or None if there
@@ -104,11 +104,11 @@ is no milestone for the given name.
 
     >>> milestone_3_1 = webservice.named_get(
     ...     debian['self_link'], "getMilestone", name="3.1").jsonBody()
-    >>> print milestone_3_1['self_link']
+    >>> print(milestone_3_1['self_link'])
     http://.../debian/+milestone/3.1
 
-    >>> print webservice.named_get(
-    ...     debian['self_link'], "getMilestone", name="fnord").jsonBody()
+    >>> print(webservice.named_get(
+    ...     debian['self_link'], "getMilestone", name="fnord").jsonBody())
     None
 
 "getSourcePackage" returns a distribution source package for the given
@@ -117,7 +117,7 @@ name.
     >>> alsa_utils = webservice.named_get(
     ...     ubuntu['self_link'], 'getSourcePackage',
     ...     name='alsa-utils').jsonBody()
-    >>> print alsa_utils['self_link']
+    >>> print(alsa_utils['self_link'])
     http://.../ubuntu/+source/alsa-utils
 
 "searchSourcePackages" returns a collection of distribution source
@@ -128,7 +128,7 @@ packages matching (substring) the given text.
     ...     source_match='a').jsonBody()
 
     >>> for entry in alsa_results['entries']:
-    ...     print entry['self_link']
+    ...     print(entry['self_link'])
     http://.../ubuntu/+source/alsa-utils
     http://.../ubuntu/+source/commercialpackage
     http://.../ubuntu/+source/foobar
@@ -140,7 +140,7 @@ packages matching (substring) the given text.
     >>> partner = webservice.named_get(
     ...     ubuntu['self_link'], 'getArchive',
     ...     name='partner').jsonBody()
-    >>> print partner['self_link']
+    >>> print(partner['self_link'])
     http://.../ubuntu/+archive/partner
 
 "getMirrorByName" returns a mirror by its unique name.
@@ -230,7 +230,7 @@ Mark new mirror as official and a country mirror.
     ...     ubuntu['self_link'], 'getCountryMirror',
     ...     country=uk['self_link'],
     ...     mirror_type="Archive")
-    >>> print uk_country_mirror_archive.jsonBody()
+    >>> print(uk_country_mirror_archive.jsonBody())
     None
 
 For "getCountryMirror", the mirror_type parameter must be "Archive" or
@@ -240,7 +240,7 @@ For "getCountryMirror", the mirror_type parameter must be "Archive" or
     ...     ubuntu['self_link'], 'getCountryMirror',
     ...     country=uk['self_link'],
     ...     mirror_type="Bogus")
-    >>> print uk_country_mirror_archive.jsonBody()
+    >>> print(uk_country_mirror_archive.jsonBody())
     Traceback (most recent call last):
     ...
     ValueError: mirror_type: Invalid value "Bogus". Acceptable values are:
diff --git a/lib/lp/registry/stories/webservice/xx-distroseries.txt b/lib/lp/registry/stories/webservice/xx-distroseries.txt
index 0cff63d..e6ad345 100644
--- a/lib/lp/registry/stories/webservice/xx-distroseries.txt
+++ b/lib/lp/registry/stories/webservice/xx-distroseries.txt
@@ -5,7 +5,7 @@ We can get a distroseries object via a distribution object in several ways:
 
     >>> distros = webservice.get("/distros").jsonBody()
     >>> ubuntu = distros['entries'][0]
-    >>> print ubuntu['self_link']
+    >>> print(ubuntu['self_link'])
     http://.../ubuntu
 
 Via all the available series:
@@ -13,7 +13,7 @@ Via all the available series:
     >>> all_series = webservice.get(
     ...     ubuntu['series_collection_link']).jsonBody()
     >>> for entry in all_series['entries']:
-    ...     print entry['self_link']
+    ...     print(entry['self_link'])
     http://.../ubuntu/breezy-autotest
     http://.../ubuntu/grumpy
     http://.../ubuntu/hoary
@@ -24,7 +24,7 @@ The series are available to the anonymous API user too:
     >>> all_series = anon_webservice.get(
     ...     ubuntu['series_collection_link']).jsonBody()
     >>> for entry in all_series['entries']:
-    ...     print entry['self_link']
+    ...     print(entry['self_link'])
     http://.../ubuntu/breezy-autotest
     http://.../ubuntu/grumpy
     http://.../ubuntu/hoary
@@ -34,7 +34,7 @@ Via the current series:
 
     >>> current_series = webservice.get(
     ...     ubuntu['current_series_link']).jsonBody()
-    >>> print current_series['self_link']
+    >>> print(current_series['self_link'])
     http://.../ubuntu/hoary
 
 Via the collection of development series:
@@ -42,7 +42,7 @@ Via the collection of development series:
     >>> dev_series = webservice.named_get(
     ...     ubuntu['self_link'], 'getDevelopmentSeries').jsonBody()
     >>> for entry in sorted(dev_series['entries']):
-    ...     print entry['self_link']
+    ...     print(entry['self_link'])
     http://.../ubuntu/hoary
 
 And via a direct query of a named series:
@@ -50,7 +50,7 @@ And via a direct query of a named series:
     >>> series = webservice.named_get(
     ...     ubuntu['self_link'], 'getSeries',
     ...     name_or_version='hoary').jsonBody()
-    >>> print series['self_link']
+    >>> print(series['self_link'])
     http://.../ubuntu/hoary
 
 For distroseries we publish a subset of its attributes.
@@ -133,7 +133,7 @@ Creating a milestone on the distroseries
     ...     current_series['self_link'], 'newMilestone', {},
     ...     name='alpha1', code_name='wombat', date_targeted=u'2009-09-06',
     ...     summary='summary.')
-    >>> print response
+    >>> print(response)
     HTTP/1.1 201 Created
     ...
     Location: http://.../ubuntu/+milestone/alpha1
diff --git a/lib/lp/registry/stories/webservice/xx-person.txt b/lib/lp/registry/stories/webservice/xx-person.txt
index 58a700e..13e9bce 100644
--- a/lib/lp/registry/stories/webservice/xx-person.txt
+++ b/lib/lp/registry/stories/webservice/xx-person.txt
@@ -148,10 +148,10 @@ Apart from the link to the preferred email, there is a link to the
 collection of other confirmed email addresses of that person/team.
 
     >>> sample_person = webservice.get("/~name12").jsonBody()
-    >>> print sample_person['preferred_email_address_link']
+    >>> print(sample_person['preferred_email_address_link'])
     http://.../~name12/+email/test@xxxxxxxxxxxxx
     >>> emails = sample_person['confirmed_email_addresses_collection_link']
-    >>> print emails
+    >>> print(emails)
     http://.../~name12/confirmed_email_addresses
     >>> print_self_link_of_entries(webservice.get(emails).jsonBody())
     http://.../~name12/+email/testing@xxxxxxxxxxxxx
@@ -170,7 +170,7 @@ representations too.
 One can only traverse to the email addresses of the person already
 traversed to, obviously.
 
-    >>> print webservice.get("/~salgado/+email/test@xxxxxxxxxxxxx")
+    >>> print(webservice.get("/~salgado/+email/test@xxxxxxxxxxxxx"))
     HTTP/1.1 404 Not Found
     ...
 
@@ -187,7 +187,7 @@ The sample person "ssh-user" doesn't have any keys to begin with:
     >>> logout()
     >>> sample_person = webservice.get("/~ssh-user").jsonBody()
     >>> sshkeys = sample_person['sshkeys_collection_link']
-    >>> print sshkeys
+    >>> print(sshkeys)
     http://.../~ssh-user/sshkeys
     >>> print_self_link_of_entries(anon_webservice.get(sshkeys).jsonBody())
 
@@ -234,7 +234,7 @@ The sample person "name12" doesn't have any keys to begin with:
 
     >>> sample_person = webservice.get("/~name12").jsonBody()
     >>> gpgkeys = sample_person['gpg_keys_collection_link']
-    >>> print gpgkeys
+    >>> print(gpgkeys)
     http://.../~name12/gpg_keys
     >>> print_self_link_of_entries(webservice.get(gpgkeys).jsonBody())
 
@@ -337,58 +337,58 @@ You can also change a TeamMembership through its custom operations.
 
 To change its expiration date, use setExpirationDate(date).
 
-    >>> print salgado_landscape['date_expires']
+    >>> print(salgado_landscape['date_expires'])
     None
 
     >>> import pytz
     >>> from datetime import datetime
     >>> someday = datetime(2058, 8, 1, tzinfo=pytz.UTC)
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     salgado_landscape['self_link'], 'setExpirationDate', {},
-    ...     date=str(someday))
+    ...     date=str(someday)))
     HTTP/1.1 200 Ok
     ...
 
-    >>> print webservice.get(
-    ...     salgado_landscape['self_link']).jsonBody()['date_expires']
+    >>> print(webservice.get(
+    ...     salgado_landscape['self_link']).jsonBody()['date_expires'])
     2058-08-01...
 
 To change its status, use setStatus(status).
 
-    >>> print salgado_landscape['status']
+    >>> print(salgado_landscape['status'])
     Approved
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     salgado_landscape['self_link'], 'setStatus', {},
-    ...     status='Deactivated')
+    ...     status='Deactivated'))
     HTTP/1.1 200 Ok
     ...
 
-    >>> print webservice.get(
-    ...     salgado_landscape['self_link']).jsonBody()['status']
+    >>> print(webservice.get(
+    ...     salgado_landscape['self_link']).jsonBody()['status'])
     Deactivated
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     salgado_landscape['self_link'], 'setStatus', {},
-    ...     status='Approved', silent=True)
+    ...     status='Approved', silent=True))
     HTTP/1.1 200 Ok
     ...
 
-    >>> print webservice.get(
-    ...     salgado_landscape['self_link']).jsonBody()['status']
+    >>> print(webservice.get(
+    ...     salgado_landscape['self_link']).jsonBody()['status'])
     Approved
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     salgado_landscape['self_link'], 'setStatus', {},
-    ...     status='Deactivated', silent=True)
+    ...     status='Deactivated', silent=True))
     HTTP/1.1 200 Ok
     ...
 
     # Now revert the change to salgado's membership to not break other tests
     # further down.
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     salgado_landscape['self_link'], 'setStatus', {},
-    ...     status='Approved')
+    ...     status='Approved'))
     HTTP/1.1 200 Ok
     ...
 
@@ -464,7 +464,7 @@ All wiki names associated to a person/team are also linked to that
 person/team.
 
     >>> wikis_link = salgado['wiki_names_collection_link']
-    >>> print wikis_link
+    >>> print(wikis_link)
     http://.../~salgado/wiki_names
     >>> print_self_link_of_entries(webservice.get(wikis_link).jsonBody())
     http://.../~salgado/+wikiname/2
@@ -490,7 +490,7 @@ representations too.
 One can only traverse to the WikiNames of the person already traversed
 to, obviously.
 
-    >>> print webservice.get("/~name12/+wikiname/2")
+    >>> print(webservice.get("/~name12/+wikiname/2"))
     HTTP/1.1 404 Not Found
     ...
 
@@ -504,7 +504,7 @@ Wiki names can be modified.
     ...     wiki_name['self_link'], 'application/json', dumps(patch))
     >>> wiki_name = sorted(
     ...     webservice.get(wikis_link).jsonBody()['entries'])[0]
-    >>> print wiki_name['url']
+    >>> print(wiki_name['url'])
     http://www.example.com/MrExample
 
 But only if we supply valid data. Due to bug #1088358 the error is
@@ -515,7 +515,7 @@ escaped as if it was HTML.
     ...    u'wikiname': 'MrExample'}
     >>> response = webservice.patch(
     ...     wiki_name['self_link'], 'application/json', dumps(patch))
-    >>> print response
+    >>> print(response)
     HTTP/1.1 400 Bad Request
     ...
     wiki: The URI scheme &quot;javascript&quot; is not allowed.
@@ -529,7 +529,7 @@ Jabber IDs of a person are also linked.
 
     >>> mark = webservice.get("/~mark").jsonBody()
     >>> jabber_ids_link = mark['jabber_ids_collection_link']
-    >>> print jabber_ids_link
+    >>> print(jabber_ids_link)
     http://.../~mark/jabber_ids
     >>> print_self_link_of_entries(webservice.get(jabber_ids_link).jsonBody())
     http://.../~mark/+jabberid/markshuttleworth@xxxxxxxxxx
@@ -548,8 +548,8 @@ representations too.
 One can only traverse to the Jabber IDs of the person already traversed
 to, obviously.
 
-    >>> print webservice.get(
-    ...     "/~salgado/+jabberid/markshuttleworth@xxxxxxxxxx")
+    >>> print(webservice.get(
+    ...     "/~salgado/+jabberid/markshuttleworth@xxxxxxxxxx"))
     HTTP/1.1 404 Not Found
     ...
 
@@ -560,7 +560,7 @@ IRC nicknames
 The same for IRC nicknames
 
     >>> irc_ids_link = mark['irc_nicknames_collection_link']
-    >>> print irc_ids_link
+    >>> print(irc_ids_link)
     http://.../~mark/irc_nicknames
     >>> print_self_link_of_entries(webservice.get(irc_ids_link).jsonBody())
     http://.../~mark/+ircnick/1
@@ -585,7 +585,7 @@ too.
 One can only traverse to the IRC IDs of the person already traversed
 to, obviously.
 
-    >>> print webservice.get("/~salgado/+ircnick/1")
+    >>> print(webservice.get("/~salgado/+ircnick/1"))
     HTTP/1.1 404 Not Found
     ...
 
@@ -596,11 +596,11 @@ PPAs
 We can get to the person's default PPA via the 'archive' property:
 
     >>> mark_archive_link = mark['archive_link']
-    >>> print mark_archive_link
+    >>> print(mark_archive_link)
     http://.../~mark/+archive/ubuntu/ppa
 
     >>> mark_archive = webservice.get(mark_archive_link).jsonBody()
-    >>> print mark_archive['description']
+    >>> print(mark_archive['description'])
     packages to help the humanity (you know, ubuntu)
 
 The 'ppas' property returns a collection of PPAs owned by that
@@ -613,24 +613,24 @@ person.
 A specific PPA can be looked up by name via 'getPPAByName'
 named-operation on IPerson.
 
-    >>> print webservice.named_get(
+    >>> print(webservice.named_get(
     ...     mark['self_link'], 'getPPAByName',
-    ...     distribution='/ubuntu', name='ppa').jsonBody()['self_link']
+    ...     distribution='/ubuntu', name='ppa').jsonBody()['self_link'])
     http://.../~mark/+archive/ubuntu/ppa
 
 If no distribution is specified, it defaults to Ubuntu.
 
-    >>> print webservice.named_get(
+    >>> print(webservice.named_get(
     ...     mark['self_link'], 'getPPAByName',
-    ...     name='ppa').jsonBody()['self_link']
+    ...     name='ppa').jsonBody()['self_link'])
     http://.../~mark/+archive/ubuntu/ppa
 
 In cases where a PPA with a given name cannot be found, a Not Found error is
 returned.
 
-    >>> print webservice.named_get(
+    >>> print(webservice.named_get(
     ...     mark['self_link'], 'getPPAByName', distribution='/debian',
-    ...     name='ppa')
+    ...     name='ppa'))
     HTTP/1.1 404 Not Found
     ...
     No such ppa: 'ppa'.
@@ -639,9 +639,9 @@ The method doesn't even bother to execute the lookup if the given
 'name' doesn't match the constraints for PPA names. An error message
 indicating what was wrong is returned.
 
-    >>> print webservice.named_get(
+    >>> print(webservice.named_get(
     ...     mark['self_link'], 'getPPAByName', distribution='/ubuntu',
-    ...     name='XpTo@#$%')
+    ...     name='XpTo@#$%'))
     HTTP/1.1 400 Bad Request
     ...
     name:
@@ -662,7 +662,7 @@ all the URLs to the private archives that the person can access.
 
     >>> launchpad = launchpadlib_for(
     ...     'person test', 'mark', 'WRITE_PUBLIC')
-    >>> print launchpad.me.getArchiveSubscriptionURLs()
+    >>> print(launchpad.me.getArchiveSubscriptionURLs())
     [u'http://mark:testtoken@xxxxxxxxxxxxxxxxxxxxxxxxxx/mark/p3a/ubuntu']
 
 
@@ -708,18 +708,18 @@ Team membership operations
 
 Joining and leaving teams:
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     salgado['self_link'], 'join', {},
-    ...     team=ubuntu_team['self_link'])
+    ...     team=ubuntu_team['self_link']))
     HTTP/1.1 200 Ok
     ...
     >>> webservice.get(
     ...     "/~ubuntu-team/+member/salgado").jsonBody()['status']
     u'Proposed'
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     salgado['self_link'], 'leave', {},
-    ...     team=landscape_developers['self_link'])
+    ...     team=landscape_developers['self_link']))
     HTTP/1.1 200 Ok
     ...
     >>> webservice.get(
@@ -740,10 +740,10 @@ mentoring process (Bug 498181).
 
     # The sample user (name12) is used to verify that it works when
     # the new member's email address is hidden.
-    >>> print owner_webservice.named_post(
+    >>> print(owner_webservice.named_post(
     ...     webservice.getAbsoluteUrl('~otherteam'), 'addMember', {},
     ...     person=webservice.getAbsoluteUrl('/~name12'),
-    ...     status='Proposed', comment='Just a test')
+    ...     status='Proposed', comment='Just a test'))
     HTTP/1.1 200 Ok
     ...
     >>> owner_webservice.get("/~otherteam/+member/name12"
@@ -753,10 +753,10 @@ mentoring process (Bug 498181).
 Adding a team as a new member will result in the membership being
 set to the Invited status.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     ubuntu_team['self_link'], 'addMember', {},
     ...     person=landscape_developers['self_link'],
-    ...     comment='Just a test')
+    ...     comment='Just a test'))
     HTTP/1.1 200 Ok
     ...
     >>> webservice.get("/~ubuntu-team/+member/landscape-developers"
@@ -765,18 +765,18 @@ set to the Invited status.
 
 Accepting or declining a membership invitation:
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     landscape_developers['self_link'], 'acceptInvitationToBeMemberOf',
-    ...     {}, team=ubuntu_team['self_link'], comment='Just a test')
+    ...     {}, team=ubuntu_team['self_link'], comment='Just a test'))
     HTTP/1.1 200 Ok
     ...
     >>> webservice.get("/~ubuntu-team/+member/landscape-developers"
     ...     ).jsonBody()['status']
     u'Approved'
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     '/~name20', 'declineInvitationToBeMemberOf', {},
-    ...     team=ubuntu_team['self_link'], comment='Just a test')
+    ...     team=ubuntu_team['self_link'], comment='Just a test'))
     HTTP/1.1 200 Ok
     ...
     >>> webservice.get("/~ubuntu-team/+member/name20").jsonBody()['status']
@@ -785,9 +785,9 @@ Accepting or declining a membership invitation:
 The retractTeamMembership method allows a team admin to remove their team
 from another team.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     landscape_developers['self_link'], 'retractTeamMembership',
-    ...     {}, team=ubuntu_team['self_link'], comment='bye bye')
+    ...     {}, team=ubuntu_team['self_link'], comment='bye bye'))
     HTTP/1.1 200 Ok
     ...
     >>> webservice.get("/~ubuntu-team/+member/landscape-developers"
@@ -802,8 +802,8 @@ A team can't be its own owner.
 
     >>> import simplejson
     >>> doc = {'team_owner_link' : webservice.getAbsoluteUrl("/~admins")}
-    >>> print webservice.patch(
-    ...    "/~admins", 'application/json', simplejson.dumps(doc))
+    >>> print(webservice.patch(
+    ...    "/~admins", 'application/json', simplejson.dumps(doc)))
     HTTP/1.1 400 Bad Request
     ...
     team_owner_link: Constraint not satisfied.
diff --git a/lib/lp/registry/stories/webservice/xx-personlocation.txt b/lib/lp/registry/stories/webservice/xx-personlocation.txt
index 1a6f739..2030b0b 100644
--- a/lib/lp/registry/stories/webservice/xx-personlocation.txt
+++ b/lib/lp/registry/stories/webservice/xx-personlocation.txt
@@ -7,27 +7,27 @@ We start with the case where there is no information about the user's
 location, at all.
 
     >>> jdub = webservice.get("/~jdub").jsonBody()
-    >>> print jdub['time_zone']
+    >>> print(jdub['time_zone'])
     UTC
-    >>> print jdub['latitude']
+    >>> print(jdub['latitude'])
     None
-    >>> print jdub['longitude']
+    >>> print(jdub['longitude'])
     None
 
 It is also possible to set the location, but as you can see the
 latitude/longitude read via the Web API will still be None.
 
-    >>> print webservice.get("/~jdub").jsonBody()['time_zone']
+    >>> print(webservice.get("/~jdub").jsonBody()['time_zone'])
     UTC
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     '/~jdub', 'setLocation', {},
     ...     latitude='-34.6', longitude='157.0',
-    ...     time_zone=u'Australia/Sydney')
+    ...     time_zone=u'Australia/Sydney'))
     HTTP/1.1 200 Ok
     ...
     >>> webservice.get("/~jdub").jsonBody()['time_zone']
     u'Australia/Sydney'
-    >>> print jdub['latitude']
+    >>> print(jdub['latitude'])
     None
-    >>> print jdub['longitude']
+    >>> print(jdub['longitude'])
     None
diff --git a/lib/lp/registry/stories/webservice/xx-private-team.txt b/lib/lp/registry/stories/webservice/xx-private-team.txt
index 39a2503..182e367 100644
--- a/lib/lp/registry/stories/webservice/xx-private-team.txt
+++ b/lib/lp/registry/stories/webservice/xx-private-team.txt
@@ -26,7 +26,7 @@ even exists.
     >>> member = user_webservice.get("/~private-team-owner").jsonBody()
     >>> response = user_webservice.get(
     ...     member['memberships_details_collection_link'])
-    >>> print sorted(response.jsonBody().items())
+    >>> print(sorted(response.jsonBody().items()))
     [(u'entries', []),
      (u'resource_type_link',
       u'http://.../#team_membership-page-resource'),
@@ -38,7 +38,7 @@ even exists.
     >>> member = webservice.get("/~private-team-owner").jsonBody()
     >>> response = webservice.get(
     ...     member['memberships_details_collection_link'])
-    >>> print sorted(response.jsonBody().items())
+    >>> print(sorted(response.jsonBody().items()))
     [(u'entries',
       [{u'status': u'Administrator',...
         u'team_link': u'http://.../~private-team'...
@@ -61,7 +61,7 @@ cannot see the private team in the public team's super_team's attribute.
     >>> team = user_webservice.get('/~guadamen').jsonBody()
     >>> super_teams = user_webservice.get(
     ...     team['super_teams_collection_link']).jsonBody()
-    >>> print super_teams['entries']
+    >>> print(super_teams['entries'])
     []
 
 But a user with the proper permissions can see the private super team.
@@ -69,9 +69,9 @@ But a user with the proper permissions can see the private super team.
     >>> team = webservice.get('/~guadamen').jsonBody()
     >>> super_teams = webservice.get(
     ...     team['super_teams_collection_link']).jsonBody()
-    >>> print len(super_teams['entries'])
+    >>> print(len(super_teams['entries']))
     1
-    >>> print super_teams['entries'][0]['self_link']
+    >>> print(super_teams['entries'][0]['self_link'])
     http://api.launchpad.test/beta/~myteam
 
 
@@ -93,17 +93,17 @@ changed by admins and commercial admins but not by regular users.
     >>> comm_webservice = webservice_for_person(
     ...     commercial_admin, permission=OAuthPermission.WRITE_PRIVATE)
 
-    >>> print comm_webservice.named_post(
+    >>> print(comm_webservice.named_post(
     ...    "/people", "newTeam", {},
-    ...    name='my-new-team', display_name="My New Team")
+    ...    name='my-new-team', display_name="My New Team"))
     HTTP/1.1 201 Created
     ...
     Location: http://.../~my-new-team
     ...
     >>> team = webservice.get('/~my-new-team').jsonBody()
-    >>> print team['self_link']
+    >>> print(team['self_link'])
     http://api.launchpad.test/.../~my-new-team
-    >>> print team['visibility']
+    >>> print(team['visibility'])
     Public
 
 A commercial admin may change the visibility.  There is no helper
@@ -117,9 +117,9 @@ method to do it, but it can be changed via a patch.
     ...                    simplejson.dumps(representation),
     ...                    headers)
 
-    >>> print modify_team(
+    >>> print(modify_team(
     ...     '/~my-new-team', {'visibility' : 'Private'},
-    ...     'PATCH', comm_webservice)
+    ...     'PATCH', comm_webservice))
     HTTP/1.1 209 Content Returned
     ...
     Content-Type: application/json
@@ -128,27 +128,27 @@ method to do it, but it can be changed via a patch.
     {...}
 
     >>> team = webservice.get('/~my-new-team').jsonBody()
-    >>> print team['visibility']
+    >>> print(team['visibility'])
     Private
 
 As an admin, Salgado can also change a team's visibility.
 
-    >>> print user_webservice.named_post(
+    >>> print(user_webservice.named_post(
     ...    "/people", "newTeam", {},
-    ...    name='my-new-team-2', display_name="My New Team 2")
+    ...    name='my-new-team-2', display_name="My New Team 2"))
     HTTP/1.1 201 Created
     ...
     Location: http://.../~my-new-team-2
     ...
     >>> team = user_webservice.get('/~my-new-team-2').jsonBody()
-    >>> print team['self_link']
+    >>> print(team['self_link'])
     http://api.launchpad.test/.../~my-new-team-2
-    >>> print team['visibility']
+    >>> print(team['visibility'])
     Public
 
-    >>> print modify_team(
+    >>> print(modify_team(
     ...     '/~my-new-team-2', {'visibility' : 'Private'},
-    ...     'PATCH', webservice)
+    ...     'PATCH', webservice))
     HTTP/1.1 209 Content Returned
     ...
     Content-Type: application/json
@@ -157,25 +157,25 @@ As an admin, Salgado can also change a team's visibility.
     {...}
 
     >>> team = webservice.get('/~my-new-team-2').jsonBody()
-    >>> print team['visibility']
+    >>> print(team['visibility'])
     Private
 
 An unprivileged user is not able to change the visibility.
 
-    >>> print user_webservice.named_post(
+    >>> print(user_webservice.named_post(
     ...    "/people", "newTeam", {},
-    ...    name='my-new-team-3', display_name="My New Team 3")
+    ...    name='my-new-team-3', display_name="My New Team 3"))
     HTTP/1.1 201 Created
     ...
     Location: http://.../~my-new-team-3
     ...
     >>> team = user_webservice.get('/~my-new-team-3').jsonBody()
-    >>> print team['self_link']
+    >>> print(team['self_link'])
     http://api.launchpad.test/.../~my-new-team-3
-    >>> print team['visibility']
+    >>> print(team['visibility'])
     Public
 
-    >>> print modify_team('/~my-new-team-3', {'visibility' : 'Private'},
-    ...     'PATCH', user_webservice)
+    >>> print(modify_team('/~my-new-team-3', {'visibility' : 'Private'},
+    ...     'PATCH', user_webservice))
     HTTP/1.1 403 Forbidden
     ...
diff --git a/lib/lp/registry/stories/webservice/xx-project-registry.txt b/lib/lp/registry/stories/webservice/xx-project-registry.txt
index d82ce34..06c53b7 100644
--- a/lib/lp/registry/stories/webservice/xx-project-registry.txt
+++ b/lib/lp/registry/stories/webservice/xx-project-registry.txt
@@ -22,7 +22,7 @@ It is possible to get a batched list of all the project groups.
     u'http://.../apache'
 
     >>> for project_group in project_group_entries:
-    ...   print project_group['display_name']
+    ...   print(project_group['display_name'])
     Apache
     ...
     GNOME
@@ -34,7 +34,7 @@ It's possible to search the list and get a subset of the project groups.
     >>> group_collection = webservice.named_get(
     ...     "/projectgroups", "search", text="Apache").jsonBody()
     >>> for project_group in group_collection['entries']:
-    ...   print project_group['display_name']
+    ...   print(project_group['display_name'])
     Apache
 
 Searching without providing a search string is the same as getting all
@@ -45,7 +45,7 @@ the project groups.
     >>> project_group_entries = sorted(
     ...     group_collection['entries'], key=itemgetter('name'))
     >>> for project_group in project_group_entries:
-    ...   print project_group['display_name']
+    ...   print(project_group['display_name'])
     Apache
     ...
     GNOME
@@ -135,11 +135,11 @@ is no milestone for the given name.
 
     >>> milestone_1_0 = webservice.named_get(
     ...     mozilla['self_link'], "getMilestone", name="1.0").jsonBody()
-    >>> print milestone_1_0['self_link']
+    >>> print(milestone_1_0['self_link'])
     http://.../mozilla/+milestone/1.0
 
-    >>> print webservice.named_get(
-    ...     mozilla['self_link'], "getMilestone", name="fnord").jsonBody()
+    >>> print(webservice.named_get(
+    ...     mozilla['self_link'], "getMilestone", name="fnord").jsonBody())
     None
 
 
@@ -210,7 +210,7 @@ In Launchpad project names may not have uppercase letters in their
 name.  As a convenience, requests for projects using the wrong case
 are redirected to the correct location.
 
-    >>> print webservice.get('/FireFox')
+    >>> print(webservice.get('/FireFox'))
     HTTP/1.1 301 Moved Permanently
     ...
     Location: http://api.launchpad.test/beta/firefox
@@ -256,11 +256,11 @@ is no milestone for the given name.
 
     >>> milestone_1_0 = webservice.named_get(
     ...     firefox['self_link'], "getMilestone", name="1.0").jsonBody()
-    >>> print milestone_1_0['self_link']
+    >>> print(milestone_1_0['self_link'])
     http://.../firefox/+milestone/1.0
 
-    >>> print webservice.named_get(
-    ...     firefox['self_link'], "getMilestone", name="fnord").jsonBody()
+    >>> print(webservice.named_get(
+    ...     firefox['self_link'], "getMilestone", name="fnord").jsonBody())
     None
 
 The project group can be accessed through the project_group_link.
@@ -272,7 +272,7 @@ A list of series can be accessed through the series_collection_link.
 
     >>> response = webservice.get(firefox['series_collection_link'])
     >>> series = response.jsonBody()
-    >>> print series['total_size']
+    >>> print(series['total_size'])
     2
 
     >>> print_self_link_of_entries(series)
@@ -283,14 +283,14 @@ A list of series can be accessed through the series_collection_link.
 
     >>> series_1_0 = webservice.named_get(
     ...     firefox['self_link'], "getSeries", name="1.0").jsonBody()
-    >>> print series_1_0['self_link']
+    >>> print(series_1_0['self_link'])
     http://.../firefox/1.0
 
 Series can also be accessed anonymously.
 
     >>> response = anon_webservice.get(firefox['series_collection_link'])
     >>> series = response.jsonBody()
-    >>> print series['total_size']
+    >>> print(series['total_size'])
     2
 
 "newSeries" permits the creation of new series.
@@ -298,7 +298,7 @@ Series can also be accessed anonymously.
     >>> experimental_new_series = webservice.named_post(
     ...     firefox['self_link'], "newSeries", name="experimental",
     ...     summary="An experimental new series.")
-    >>> print experimental_new_series
+    >>> print(experimental_new_series)
     HTTP/1.1 201 Created
     ...
     Location: http://.../firefox/experimental
@@ -308,7 +308,7 @@ A list of releases can be accessed through the releases_collection_link.
 
     >>> response = webservice.get(firefox['releases_collection_link'])
     >>> releases = response.jsonBody()
-    >>> print releases['total_size']
+    >>> print(releases['total_size'])
     4
 
     >>> print_self_link_of_entries(releases)
@@ -321,14 +321,14 @@ A list of releases can be accessed through the releases_collection_link.
 
     >>> release_0_9_1 = webservice.named_get(
     ...     firefox['self_link'], "getRelease", version="0.9.1").jsonBody()
-    >>> print release_0_9_1['self_link']
+    >>> print(release_0_9_1['self_link'])
     http://.../firefox/trunk/0.9.1
 
 Releases can also be accessed anonymously.
 
     >>> response = anon_webservice.get(firefox['releases_collection_link'])
     >>> releases = response.jsonBody()
-    >>> print releases['total_size']
+    >>> print(releases['total_size'])
     4
 
 The development focus series can be accessed through the
@@ -348,8 +348,8 @@ Attributes can be edited via the webservice.patch() method.
     ...     u'bug_tracker_link':
     ...         webservice.getAbsoluteUrl('/bugs/bugtrackers/mozilla.org'),
     ...     }
-    >>> print webservice.patch(
-    ...     '/firefox', 'application/json', dumps(patch))
+    >>> print(webservice.patch(
+    ...     '/firefox', 'application/json', dumps(patch)))
     HTTP/1.1 209 Content Returned
     ...
 
@@ -393,8 +393,8 @@ changed as well.
     >>> patch = {
     ...     u'owner_link': webservice.getAbsoluteUrl('/~mark'),
     ...     }
-    >>> print webservice.patch(
-    ...     '/test-project', 'application/json', dumps(patch))
+    >>> print(webservice.patch(
+    ...     '/test-project', 'application/json', dumps(patch)))
     HTTP/1.1 209 Content Returned
     ...
 
@@ -408,8 +408,8 @@ webservice.patch() method.
     >>> patch = {
     ...     u'registrant_link': webservice.getAbsoluteUrl('/~mark'),
     ...     }
-    >>> print webservice.patch(
-    ...     '/firefox', 'application/json', dumps(patch))
+    >>> print(webservice.patch(
+    ...     '/firefox', 'application/json', dumps(patch)))
     HTTP/1.1 400 Bad Request
     ...
     registrant_link: You tried to modify a read-only attribute.
@@ -424,8 +424,8 @@ Similarly the date_created attribute cannot be modified.
     >>> patch = {
     ...     u'date_created': u'2000-01-01T01:01:01+00:00Z'
     ...     }
-    >>> print webservice.patch(
-    ...     '/firefox', 'application/json', dumps(patch))
+    >>> print(webservice.patch(
+    ...     '/firefox', 'application/json', dumps(patch)))
     HTTP/1.1 400 Bad Request
     ...
     date_created: You tried to modify a read-only attribute.
@@ -438,14 +438,14 @@ Similarly the date_created attribute cannot be modified.
 hierarchy of series, milestones, and releases.
 
     >>> patch = {'status': 'Obsolete'}
-    >>> print webservice.patch(
-    ...     '/firefox/trunk', 'application/json', dumps(patch))
+    >>> print(webservice.patch(
+    ...     '/firefox/trunk', 'application/json', dumps(patch)))
     HTTP/1.1 209 Content Returned...
     >>> timeline = webservice.named_get(
     ...     firefox['self_link'],
     ...     "get_timeline",
     ...     include_inactive=True).jsonBody()
-    >>> print pretty(timeline)
+    >>> print(pretty(timeline))
     {u'entries': [{u'http_etag': ...
                    u'is_development_focus': True,
                    u'landmarks': [{u'code_name': None,
@@ -525,7 +525,7 @@ It's possible to search the list and get a subset of the project groups.
     ...     project['display_name']
     ...     for project in project_collection['entries']]
     >>> for project_name in sorted(projects):
-    ...     print project_name
+    ...     print(project_name)
     Derby
     Tomcat
 
@@ -552,7 +552,7 @@ The latest projects registered can be retrieved.
     >>> entries = sorted(
     ...    latest['entries'], key=itemgetter('display_name'))
     >>> for project in entries:
-    ...     print project['display_name']
+    ...     print(project['display_name'])
     Derby
     Mega Money Maker
     Obsolete Junk
@@ -569,7 +569,7 @@ licensing.  We can find all projects with unreviewed licenses.
     >>> entries = sorted(
     ...    unreviewed['entries'], key=itemgetter('display_name'))
     >>> for project in entries:
-    ...     print project['display_name']
+    ...     print(project['display_name'])
     Arch mirrors ...
 
 The project collection has a method for creating a new project.
@@ -597,47 +597,47 @@ The project collection has a method for creating a new project.
 
 Verify a project does not exist and then create it.
 
-    >>> print webservice.get('/my-new-project')
+    >>> print(webservice.get('/my-new-project'))
     HTTP/1.1 404 Not Found
     ...
 
-    >>> print create_project('my-new-project', 'My New Project',
+    >>> print(create_project('my-new-project', 'My New Project',
     ...     'My New Project', 'My Shiny New Project',
     ...     licenses=["Zope Public Licence", "GNU GPL v2"],
-    ...     wiki_url="http://example.com/shiny";)
+    ...     wiki_url="http://example.com/shiny";))
     HTTP/1.1 201 Created
     ...
     Location: http://.../my-new-project
     ...
 
-    >>> print webservice.get('/my-new-project')
+    >>> print(webservice.get('/my-new-project'))
     HTTP/1.1 200 Ok
     ...
 
     >>> new_project = webservice.get('/my-new-project').jsonBody()
-    >>> print new_project['name']
+    >>> print(new_project['name'])
     my-new-project
 
-    >>> print new_project['display_name']
+    >>> print(new_project['display_name'])
     My New Project
 
-    >>> print new_project['summary']
+    >>> print(new_project['summary'])
     My Shiny New Project
 
-    >>> print sorted(new_project['licenses'])
+    >>> print(sorted(new_project['licenses']))
     [u'GNU GPL v2', u'Zope Public Licence']
 
-    >>> print new_project['project_reviewed']
+    >>> print(new_project['project_reviewed'])
     False
 
-    >>> print new_project['homepage_url']
+    >>> print(new_project['homepage_url'])
     None
 
 Attempting to create a project with a name that has already been used is
 an error.
 
-    >>> print create_project('my-new-project', 'My New Project',
-    ...     'My New Project', 'My Shiny New Project')
+    >>> print(create_project('my-new-project', 'My New Project',
+    ...     'My New Project', 'My Shiny New Project'))
     HTTP/1.1 400 Bad Request
     ...
     name: my-new-project is already used by another project
@@ -646,9 +646,9 @@ If the fields do not validate a Bad Request error is received.  Here the
 URL is not properly formed. Due to bug #1088358 the error is escaped as
 if it was HTML.
 
-    >>> print create_project('my-new-project', 'My New Project',
+    >>> print(create_project('my-new-project', 'My New Project',
     ...     'My New Project', 'My Shiny New Project',
-    ...     wiki_url="htp://badurl.example.com")
+    ...     wiki_url="htp://badurl.example.com"))
     HTTP/1.1 400 Bad Request
     ...
     wiki_url: The URI scheme &quot;htp&quot; is not allowed.  Only URIs
@@ -681,7 +681,7 @@ comparing the self_link, which every resource has.
     >>> featured_entries = sorted(
     ...     featured_pillars['entries'], key=itemgetter('self_link'))
     >>> for pillar in featured_entries:
-    ...     print pillar['self_link']
+    ...     print(pillar['self_link'])
     http://.../applets
     http://.../bazaar
     ...
@@ -692,7 +692,7 @@ comparing the self_link, which every resource has.
     >>> found_entries = sorted(search_result['entries'],
     ...     key=itemgetter('self_link'))
     >>> for pillar in found_entries:
-    ...     print pillar['self_link']
+    ...     print(pillar['self_link'])
     http://.../bazaar
     http://.../bzr
     http://.../launchpad
@@ -700,7 +700,7 @@ comparing the self_link, which every resource has.
     >>> search_result = webservice.named_get(
     ...     "/pillars", "search", text="bazaar", limit="1").jsonBody()
     >>> for pillar in search_result['entries']:
-    ...     print pillar['self_link']
+    ...     print(pillar['self_link'])
     http://.../bazaar
 
 
@@ -755,7 +755,7 @@ milestones and releases.
 
     >>> timeline = webservice.named_get(
     ...     babadoo_foobadoo['self_link'], "get_timeline").jsonBody()
-    >>> print pretty(timeline)
+    >>> print(pretty(timeline))
     {u'http_etag': ...
      u'is_development_focus': False,
      u'landmarks': [],
@@ -780,41 +780,41 @@ method simplifies this for us.
     ...     firefox_1_0['self_link'], 'newMilestone', {},
     ...     name='alpha1', code_name='Elmer', date_targeted=u'2005-06-06',
     ...     summary='Feature complete but buggy.')
-    >>> print response
+    >>> print(response)
     HTTP/1.1 201 Created
     ...
     Location: http://.../firefox/+milestone/alpha1
     ...
 
     >>> milestone = webservice.get(response.getHeader('Location')).jsonBody()
-    >>> print milestone['name']
+    >>> print(milestone['name'])
     alpha1
 
-    >>> print milestone['code_name']
+    >>> print(milestone['code_name'])
     Elmer
 
-    >>> print milestone['date_targeted']
+    >>> print(milestone['date_targeted'])
     2005-06-06
 
-    >>> print milestone['summary']
+    >>> print(milestone['summary'])
     Feature complete but buggy.
 
 The milestone name must be unique on the product series.
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     firefox_1_0['self_link'], 'newMilestone', {},
     ...     name='alpha1', dateexpected='157.0',
-    ...     summary='Feature complete but buggy.')
+    ...     summary='Feature complete but buggy.'))
     HTTP/1.1 400 Bad Request
     ...
     name: The name alpha1 is already used by a milestone in Mozilla Firefox.
 
 The milestone name can only contain letters, numbers, "-", "+", and ".".
 
-    >>> print webservice.named_post(
+    >>> print(webservice.named_post(
     ...     firefox_1_0['self_link'], 'newMilestone', {},
     ...     name='!@#$%^&*()', dateexpected='157.0',
-    ...     summary='Feature complete but buggy.')
+    ...     summary='Feature complete but buggy.'))
     HTTP/1.1 400 Bad Request
     ...
     Invalid name...
@@ -826,7 +826,7 @@ Invalid data will return a Bad Request error.
     ...     name='buggy', date_targeted=u'2005-10-36',
     ...     code_name='Samurai Monkey',
     ...     summary='Very buggy.')
-    >>> print response
+    >>> print(response)
     HTTP/1.1 400 Bad Request
     ...
     date_targeted: Value doesn't look like a date.
@@ -863,20 +863,20 @@ parameters.  The webservice.named_post() method simplifies this for us.
     ...     milestone['self_link'], 'createProductRelease', {},
     ...     date_released='2000-01-01T01:01:01+00:00Z',
     ...     release_notes='New stuff', changelog='Added 5,000 features.')
-    >>> print response
+    >>> print(response)
     HTTP/1.1 201 Created
     ...
     Location: http://.../firefox/1.0/alpha1
     ...
 
     >>> release = webservice.get(response.getHeader('Location')).jsonBody()
-    >>> print release['version']
+    >>> print(release['version'])
     alpha1
 
-    >>> print release['release_notes']
+    >>> print(release['release_notes'])
     New stuff
 
-    >>> print release['changelog']
+    >>> print(release['changelog'])
     Added 5,000 features.
 
 Only one product release can be created per milestone.
@@ -885,7 +885,7 @@ Only one product release can be created per milestone.
     ...     milestone['self_link'], 'createProductRelease', {},
     ...     date_released='2000-01-01T01:01:01+00:00Z',
     ...     changelog='Added 5,000 features.')
-    >>> print response
+    >>> print(response)
     HTTP/1.1 400 Bad Request
     ...
     A milestone can only have one ProductRelease.
@@ -936,7 +936,7 @@ virtual host.
 The milestone entry has a link to its release if it has one.
 
     >>> milestone = webservice.get('/firefox/+milestone/1.0.0').jsonBody()
-    >>> print milestone['release_link']
+    >>> print(milestone['release_link'])
     http://.../firefox/1.0/1.0.0
 
 
@@ -952,7 +952,7 @@ Project release entries
 They can be deleted with the 'delete' operation.
 
     >>> results = webservice.named_post('/firefox/1.0/alpha1', 'delete')
-    >>> print results
+    >>> print(results)
     HTTP/1.1 200 Ok
     ...
 
@@ -982,7 +982,7 @@ The actual file redirects to the librarian when accessed.
 
     >>> url = '/firefox/trunk/0.9.2/+file/firefox_0.9.2.orig.tar.gz/file'
     >>> result = webservice.get(url)
-    >>> print result
+    >>> print(result)
     HTTP/1.1 303 See Other
     ...
     Location: http://.../firefox_0.9.2.orig.tar.gz
@@ -993,7 +993,7 @@ no signature so we get a 404.
 
     >>> url = '/firefox/trunk/0.9.2/+file/firefox_0.9.2.orig.tar.gz/signature'
     >>> result = webservice.get(url)
-    >>> print result
+    >>> print(result)
     HTTP/1.1 404 Not Found
     ...
 
@@ -1002,14 +1002,14 @@ to put new content will result in a ForbiddenAttribute error.
 
     >>> url = '/firefox/trunk/0.9.2/+file/firefox_0.9.2.orig.tar.gz/file'
     >>> response = webservice.put(url, 'application/x-tar-gz', 'fakefiledata')
-    >>> print response
+    >>> print(response)
     HTTP/1.1 405 Method Not Allowed...
     Allow: GET
     ...
 
     >>> url = '/firefox/trunk/0.9.2/+file/firefox_0.9.2.orig.tar.gz/signature'
     >>> response = webservice.put(url, 'pgpapplication/data', 'signaturedata')
-    >>> print response
+    >>> print(response)
     HTTP/1.1 405 Method Not Allowed...
     Allow: GET
     ...
@@ -1037,7 +1037,7 @@ Project release files can be added to a project release using the API
     ...     signature_content=sig_file_content,
     ...     file_type='README File',
     ...     description="test file")
-    >>> print response
+    >>> print(response)
     HTTP/1.1 201 Created
     ...
     Location: http://.../firefox/1.0/1.0.0/+file/filename.txt
@@ -1072,7 +1072,7 @@ available then it must be explicitly set to None.
     ...     filename='filename2.txt',
     ...     file_content=file_content,
     ...     content_type='plain/txt')
-    >>> print response
+    >>> print(response)
     HTTP/1.1 201 Created
     ...
     Location: http://.../firefox/1.0/1.0.0/+file/filename2.txt
@@ -1091,7 +1091,7 @@ The file redirects to the librarian when accessed.
     >>> url = webservice.getAbsoluteUrl(
     ...     '/firefox/1.0/1.0.0/+file/filename.txt/file')
     >>> result = webservice.get(url)
-    >>> print result
+    >>> print(result)
     HTTP/1.1 303 See Other
     ...
     Location: http://.../filename.txt
@@ -1104,7 +1104,7 @@ can delete files.
     >>> url = webservice.getAbsoluteUrl(
     ...     '/firefox/1.0/1.0.0/+file/filename.txt')
     >>> results = webservice.named_post(url, 'delete')
-    >>> print results
+    >>> print(results)
     HTTP/1.1 200 Ok
     ...
 
@@ -1129,17 +1129,17 @@ through the API.
 
     >>> login('bac@xxxxxxxxxxxxx')
     >>> mmm = getUtility(IProductSet)['mega-money-maker']
-    >>> print mmm.commercial_subscription
+    >>> print(mmm.commercial_subscription)
     None
 
     >>> _ = factory.makeCommercialSubscription(mmm)
-    >>> print mmm.commercial_subscription.product.name
+    >>> print(mmm.commercial_subscription.product.name)
     mega-money-maker
 
     >>> logout()
     >>> mmm = webservice.get("/mega-money-maker").jsonBody()
-    >>> print mmm['display_name']
+    >>> print(mmm['display_name'])
     Mega Money Maker
 
-    >>> print mmm['commercial_subscription_link']
+    >>> print(mmm['commercial_subscription_link'])
     http://.../mega-money-maker/+commercialsubscription/...
diff --git a/lib/lp/registry/stories/webservice/xx-source-package.txt b/lib/lp/registry/stories/webservice/xx-source-package.txt
index eb577dc..79b2402 100644
--- a/lib/lp/registry/stories/webservice/xx-source-package.txt
+++ b/lib/lp/registry/stories/webservice/xx-source-package.txt
@@ -49,7 +49,7 @@ distribution series. By default, there are none bound to evolution.
 
     >>> branch = webservice.named_get(
     ...     evolution['self_link'], 'getBranch', pocket='Release').jsonBody()
-    >>> print branch
+    >>> print(branch)
     None
 
 
@@ -63,7 +63,7 @@ though.
     >>> owner = factory.makePerson(name='devo')
     >>> branch = factory.makePackageBranch(
     ...     sourcepackage=evolution_package, owner=owner, name='branch')
-    >>> print branch.unique_name
+    >>> print(branch.unique_name)
     ~devo/my-distro/my-series/evolution/branch
     >>> branch_url = '/' + branch.unique_name
     >>> logout()
@@ -79,7 +79,7 @@ Then we set the branch on the evolution package:
     >>> response = webservice.named_post(
     ...     evolution['self_link'], 'setBranch', pocket='Release',
     ...     branch=branch['self_link'])
-    >>> print response.jsonBody()
+    >>> print(response.jsonBody())
     None
 
 I guess this means that if we get the branch for the RELEASE pocket again,
@@ -87,15 +87,15 @@ we'll get the new branch.
 
     >>> branch = webservice.named_get(evolution['self_link'], 'getBranch',
     ...     pocket='Release').jsonBody()
-    >>> print branch['unique_name']
+    >>> print(branch['unique_name'])
     ~devo/my-distro/my-series/evolution/branch
 
     >>> linked_branches = webservice.named_get(
     ...     evolution['self_link'], 'linkedBranches').jsonBody()
-    >>> print linked_branches.keys()
+    >>> print(linked_branches.keys())
     [u'RELEASE']
     >>> branch = linked_branches[u'RELEASE']
-    >>> print branch['unique_name']
+    >>> print(branch['unique_name'])
     ~devo/my-distro/.../branch
 
 Of course, we're also allowed to change our minds. If we set the branch for
@@ -105,9 +105,9 @@ no longer an official branch for that pocket.
     >>> response = webservice.named_post(
     ...     evolution['self_link'], 'setBranch', pocket='Release',
     ...     branch='null')
-    >>> print response.jsonBody()
+    >>> print(response.jsonBody())
     None
     >>> branch = webservice.named_get(evolution['self_link'], 'getBranch',
     ...     pocket='Release').jsonBody()
-    >>> print branch
+    >>> print(branch)
     None
diff --git a/lib/lp/registry/stories/webservice/xx-structuralsubscription.txt b/lib/lp/registry/stories/webservice/xx-structuralsubscription.txt
index c072745..b410555 100644
--- a/lib/lp/registry/stories/webservice/xx-structuralsubscription.txt
+++ b/lib/lp/registry/stories/webservice/xx-structuralsubscription.txt
@@ -33,8 +33,8 @@ Now Eric subscribes to Fooix's bug notifications.
     >>> eric_webservice = webservice_for_person(
     ...     eric_db, permission=OAuthPermission.WRITE_PRIVATE)
 
-    >>> print eric_webservice.named_post(
-    ...     '/fooix', 'addBugSubscription')
+    >>> print(eric_webservice.named_post(
+    ...     '/fooix', 'addBugSubscription'))
     HTTP/1.1 201 Created
     ...
     Location: http://.../fooix/+subscription/eric
@@ -72,28 +72,28 @@ He can examine his subscription directly.
 
 If the subscription doesn't exist, None will be returned.
 
-    >>> print webservice.named_get(
+    >>> print(webservice.named_get(
     ...     '/fooix', 'getSubscription',
-    ...     person=webservice.getAbsoluteUrl('/~michael')).jsonBody()
+    ...     person=webservice.getAbsoluteUrl('/~michael')).jsonBody())
     None
 
 Eric can remove his subscription through the webservice.
 
-    >>> print eric_webservice.named_post(
-    ...     '/fooix', 'removeBugSubscription')
+    >>> print(eric_webservice.named_post(
+    ...     '/fooix', 'removeBugSubscription'))
     HTTP/1.1 200 Ok...
 
-    >>> print webservice.named_get(
+    >>> print(webservice.named_get(
     ...     '/fooix', 'getSubscription',
-    ...     person=webservice.getAbsoluteUrl('/~eric')).jsonBody()
+    ...     person=webservice.getAbsoluteUrl('/~eric')).jsonBody())
     None
 
 Teams can be subscribed by passing in the team as an argument. Eric
 tries this.
 
-    >>> print eric_webservice.named_post(
+    >>> print(eric_webservice.named_post(
     ...     '/fooix', 'addBugSubscription',
-    ...     subscriber=webservice.getAbsoluteUrl('/~pythons'))
+    ...     subscriber=webservice.getAbsoluteUrl('/~pythons')))
     HTTP/1.1 401 Unauthorized
     ...
     eric does not have permission to subscribe pythons.
@@ -104,9 +104,9 @@ admin by virtue of his ownership.
     >>> michael_webservice = webservice_for_person(
     ...     michael_db, permission=OAuthPermission.WRITE_PRIVATE)
 
-    >>> print michael_webservice.named_post(
+    >>> print(michael_webservice.named_post(
     ...     '/fooix', 'addBugSubscription',
-    ...     subscriber=webservice.getAbsoluteUrl('/~pythons'))
+    ...     subscriber=webservice.getAbsoluteUrl('/~pythons')))
     HTTP/1.1 201 Created
     ...
     Location: http://.../fooix/+subscription/pythons
@@ -130,18 +130,18 @@ admin by virtue of his ownership.
 
 Eric can't unsubscribe the team either.
 
-    >>> print eric_webservice.named_post(
+    >>> print(eric_webservice.named_post(
     ...     '/fooix', 'removeBugSubscription',
-    ...     subscriber=webservice.getAbsoluteUrl('/~pythons'))
+    ...     subscriber=webservice.getAbsoluteUrl('/~pythons')))
     HTTP/1.1 401 Unauthorized
     ...
     eric does not have permission to unsubscribe pythons.
 
 Michael can, though.
 
-    >>> print michael_webservice.named_post(
+    >>> print(michael_webservice.named_post(
     ...     '/fooix', 'removeBugSubscription',
-    ...     subscriber=webservice.getAbsoluteUrl('/~pythons'))
+    ...     subscriber=webservice.getAbsoluteUrl('/~pythons')))
     HTTP/1.1 200 Ok...
 
     >>> subscriptions = webservice.named_get(
diff --git a/lib/lp/registry/tests/test_doc.py b/lib/lp/registry/tests/test_doc.py
index 0ee75ec..6279433 100644
--- a/lib/lp/registry/tests/test_doc.py
+++ b/lib/lp/registry/tests/test_doc.py
@@ -18,6 +18,7 @@ from lp.testing.layers import (
     LaunchpadFunctionalLayer,
     LaunchpadZopelessLayer,
     )
+from lp.testing.pages import setUpGlobs
 from lp.testing.systemdocs import (
     LayeredDocFileSuite,
     setUp,
@@ -91,7 +92,9 @@ special = {
 
 
 def test_suite():
-    suite = build_test_suite(here, special, layer=DatabaseFunctionalLayer)
+    suite = build_test_suite(
+        here, special, layer=DatabaseFunctionalLayer,
+        pageTestsSetUp=lambda test: setUpGlobs(test, future=True))
     launchpadlib_path = os.path.join(os.path.pardir, 'doc', 'launchpadlib')
     lplib_suite = build_doctest_suite(here, launchpadlib_path,
                                       layer=DatabaseFunctionalLayer)