← Back to team overview

launchpad-reviewers team mailing list archive

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

 

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

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

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/397616
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:py3-services-doctest-unicode-strings into launchpad:master.
diff --git a/lib/lp/services/config/doc/canonical-config.txt b/lib/lp/services/config/doc/canonical-config.txt
index 3b6af93..796fd0b 100644
--- a/lib/lp/services/config/doc/canonical-config.txt
+++ b/lib/lp/services/config/doc/canonical-config.txt
@@ -58,8 +58,8 @@ using persistent helpers - see lp.testing.layers).
     >>> config.filename
     '.../launchpad-lazr.conf'
 
-    >>> config.extends.filename
-    u'.../launchpad-lazr.conf'
+    >>> print(config.extends.filename)
+    /.../launchpad-lazr.conf
 
 LaunchpadConfig provides __contains__ and __getitem__ to check and
 access lazr.config sections and keys.
@@ -161,8 +161,8 @@ conf file name that is loaded.
     >>> test_config.filename
     '.../configs/testrunner/test-process-lazr.conf'
 
-    >>> test_config.extends.filename
-    u'.../configs/testrunner/launchpad-lazr.conf'
+    >>> print(test_config.extends.filename)
+    /.../configs/testrunner/launchpad-lazr.conf
 
     >>> test_config.answertracker.days_before_expiration
     300
@@ -181,8 +181,8 @@ process's name.
     >>> test_config.filename
     '.../configs/testrunner/launchpad-lazr.conf'
 
-    >>> test_config.extends.filename
-    u'.../configs/development/launchpad-lazr.conf'
+    >>> print(test_config.extends.filename)
+    /.../configs/development/launchpad-lazr.conf
 
     >>> test_config.answertracker.days_before_expiration
     15
@@ -222,8 +222,8 @@ argument to the constructor.
 #    >>> from lp.services.config import config
 #    >>> config.filename
 #    '.../configs/mailman-itests/launchpad-lazr.conf'
-#    >>> config.extends.filename
-#    u'.../configs/development/launchpad-lazr.conf'
+#    >>> print(config.extends.filename)
+#    /.../configs/development/launchpad-lazr.conf
 #    >>> config.database.dbname
 #    'launchpad_dev'
 
diff --git a/lib/lp/services/database/doc/decoratedresultset.txt b/lib/lp/services/database/doc/decoratedresultset.txt
index 0066d84..c9857ed 100644
--- a/lib/lp/services/database/doc/decoratedresultset.txt
+++ b/lib/lp/services/database/doc/decoratedresultset.txt
@@ -68,13 +68,13 @@ decorated results:
     >>> decorated_result_set.config(return_both=True)
     <lp.services.database.decoratedresultset.DecoratedResultSet object at ...>
     >>> for dist in decorated_result_set:
-    ...     print(dist)
-    (<Distribution 'Debian' (debian)>, u'Dist name is: debian')
-    (<Distribution 'Gentoo' (gentoo)>, u'Dist name is: gentoo')
+    ...     print(pretty(dist))
+    (<Distribution 'Debian' (debian)>, 'Dist name is: debian')
+    (<Distribution 'Gentoo' (gentoo)>, 'Dist name is: gentoo')
     ...
-    (<Distribution 'ubuntutest' (ubuntutest)>, u'Dist name is: ubuntutest')
-    >>> decorated_result_set.first()
-    (<Distribution 'Debian' (debian)>, u'Dist name is: debian')
+    (<Distribution 'ubuntutest' (ubuntutest)>, 'Dist name is: ubuntutest')
+    >>> print(pretty(decorated_result_set.first()))
+    (<Distribution 'Debian' (debian)>, 'Dist name is: debian')
 
 This works even if there are multiple levels:
 
diff --git a/lib/lp/services/database/doc/storm.txt b/lib/lp/services/database/doc/storm.txt
index 5d83c4e..abe5dc3 100644
--- a/lib/lp/services/database/doc/storm.txt
+++ b/lib/lp/services/database/doc/storm.txt
@@ -91,8 +91,8 @@ similarly wrapped.
     >>> person = getUtility(IPersonSet).getByEmail('no-priv@xxxxxxxxxxxxx')
     >>> removeSecurityProxy(person) is person
     False
-    >>> person.displayname
-    u'No Privileges Person'
+    >>> print(person.displayname)
+    No Privileges Person
     >>> person.name = 'foo'
     ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
@@ -102,8 +102,8 @@ similarly wrapped.
     >>> person = IMasterObject(person)
     >>> removeSecurityProxy(person) is person
     False
-    >>> person.displayname
-    u'No Privileges Person'
+    >>> print(person.displayname)
+    No Privileges Person
     >>> person.name = 'foo'
     ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
@@ -113,8 +113,8 @@ similarly wrapped.
     >>> person = IMasterObject(removeSecurityProxy(person))
     >>> removeSecurityProxy(person) is person
     True
-    >>> person.displayname
-    u'No Privileges Person'
+    >>> print(person.displayname)
+    No Privileges Person
     >>> person.name = 'foo'
 
 Our objects may compare equal even if they have come from different
diff --git a/lib/lp/services/database/doc/textsearching.txt b/lib/lp/services/database/doc/textsearching.txt
index 8951f1b..77c022f 100644
--- a/lib/lp/services/database/doc/textsearching.txt
+++ b/lib/lp/services/database/doc/textsearching.txt
@@ -651,8 +651,11 @@ by tsearch2. All words are also stemmed.
 
     >>> from lp.services.database.nl_search import nl_term_candidates
 
-    >>> nl_term_candidates('When I start firefox, it crashes')
-    [u'start', u'firefox', u'crash']
+    >>> for term in nl_term_candidates('When I start firefox, it crashes'):
+    ...     print(term)
+    start
+    firefox
+    crash
 
 It returns an empty list when there is only stop-words in the query:
 
@@ -661,10 +664,15 @@ It returns an empty list when there is only stop-words in the query:
 
 Except for the hyphenation character, all non-word caracters are ignored:
 
-    >>> nl_term_candidates(
-    ...     "Will the \'\'|\'\' character (inside a ''quoted'' string) "
-    ...     "work???")
-    [u'charact', u'insid', u'quot', u'string', u'work']
+    >>> for term in nl_term_candidates(
+    ...         "Will the \'\'|\'\' character (inside a ''quoted'' string) "
+    ...         "work???"):
+    ...     print(term)
+    charact
+    insid
+    quot
+    string
+    work
 
 
 nl_phrase_search()
@@ -700,9 +708,9 @@ More than 50% of the questions matches firefox:
 
 So firefox will be removed from the final query:
 
-    >>> nl_phrase_search('system is slow when running firefox', Question,
-    ...     fast_enabled=False)
-    u'system|slow|run'
+    >>> print(nl_phrase_search('system is slow when running firefox', Question,
+    ...     fast_enabled=False))
+    system|slow|run
 
     >>> nl_term_candidates('how do I do this?')
     []
@@ -712,14 +720,14 @@ So firefox will be removed from the final query:
 The fast code path does not remove any terms. Rather it uses an & query over
 all the terms combined with an & query for each ordinal-1 subset of the terms:
 
-    >>> nl_phrase_search('system is slow when running firefox on ubuntu',
-    ...     Question)
-    u'(firefox&run&slow&system&ubuntu)|(run&slow&system&ubuntu)|(firefox&slow&system&ubuntu)|(firefox&run&system&ubuntu)|(firefox&run&slow&ubuntu)|(firefox&run&slow&system)'
+    >>> print(nl_phrase_search('system is slow when running firefox on ubuntu',
+    ...     Question))
+    (firefox&run&slow&system&ubuntu)|(run&slow&system&ubuntu)|(firefox&slow&system&ubuntu)|(firefox&run&system&ubuntu)|(firefox&run&slow&ubuntu)|(firefox&run&slow&system)
 
 Short queries are expanded more simply:
 
-    >>> nl_phrase_search('system is slow', Question)
-    u'slow|system'
+    >>> print(nl_phrase_search('system is slow', Question))
+    slow|system
 
 
 Using other constraints
@@ -743,11 +751,11 @@ considered a stop word by tsearch2).
     >>> float(get_questions) / firefox_count > 0.50
     True
 
-    >>> nl_phrase_search(
+    >>> print(nl_phrase_search(
     ...     'firefox gets very slow on flickr', Question,
     ...     [Question.product == firefox_product, Product.active],
-    ...     fast_enabled=False)
-    u'slow|flickr'
+    ...     fast_enabled=False))
+    slow|flickr
 
 When the query only has stop words in it, the returned query will be the empty
 string:
@@ -760,9 +768,9 @@ is done.
 
     >>> IStore(Question).find(Question, Question.product_id == -1).count()
     0
-    >>> nl_phrase_search('firefox is very slow on flickr', Question,
-    ...                  [Question.product == -1])
-    u'(firefox&flickr&slow)|(flickr&slow)|(firefox&slow)|(firefox&flickr)'
+    >>> print(nl_phrase_search('firefox is very slow on flickr', Question,
+    ...                  [Question.product == -1]))
+    (firefox&flickr&slow)|(flickr&slow)|(firefox&slow)|(firefox&flickr)
 
 
 No keywords filtering with few rows
@@ -799,8 +807,8 @@ And more than half of these contain the keyword "firefox" in them:
 But the keyword is still keep because there are only less than 5
 questions:
 
-    >>> nl_phrase_search(
+    >>> print(nl_phrase_search(
     ...     'firefox is slow', Question,
     ...     [Question.distribution == ubuntu,
-    ...      Question.sourcepackagename == firefox_package.sourcepackagename])
-    u'firefox|slow'
+    ...      Question.sourcepackagename == firefox_package.sourcepackagename]))
+    firefox|slow
diff --git a/lib/lp/services/database/sqlbase.py b/lib/lp/services/database/sqlbase.py
index 3942ba7..57ba46d 100644
--- a/lib/lp/services/database/sqlbase.py
+++ b/lib/lp/services/database/sqlbase.py
@@ -71,6 +71,7 @@ from lp.services.database.interfaces import (
     IStoreSelector,
     MAIN_STORE,
     )
+from lp.services.helpers import backslashreplace
 from lp.services.propertycache import clear_property_cache
 
 # Default we want for scripts, and the PostgreSQL default. Note psycopg1 will
@@ -320,8 +321,11 @@ def quote(x):
     query will be a Unicode string (the entire query will be encoded
     before sending across the wire to the database).
 
-    >>> quote(u"\N{TRADE MARK SIGN}")
-    u"E'\u2122'"
+    >>> quoted = quote(u"\N{TRADE MARK SIGN}")
+    >>> isinstance(quoted, six.text_type)
+    True
+    >>> print(backslashreplace(quoted))
+    E'\u2122'
 
     Timezone handling is not implemented, since all timestamps should
     be UTC anyway.
diff --git a/lib/lp/services/database/tests/decoratedresultset.txt b/lib/lp/services/database/tests/decoratedresultset.txt
index 30262c4..98706e8 100644
--- a/lib/lp/services/database/tests/decoratedresultset.txt
+++ b/lib/lp/services/database/tests/decoratedresultset.txt
@@ -51,11 +51,11 @@ Make sure that the new result set is actually different:
 But it still contains the expected results:
 
     >>> for distro in result_copy:
-    ...     distro
+    ...     print(distro)
     7 elements in result set
-    u'Dist name is: debian'
+    Dist name is: debian
     ...
-    u'Dist name is: ubuntutest'
+    Dist name is: ubuntutest
 
 == config() ==
 
@@ -80,8 +80,8 @@ but returns a copy of itself:
 The decorated __getitem__ gets the item from the original result set
 and decorates the item before returning it:
 
-    >>> decorated_result_set[0]
-    u'Dist name is: debian'
+    >>> print(decorated_result_set[0])
+    Dist name is: debian
 
 == any() ==
 
@@ -98,9 +98,9 @@ and decorates the result:
 The decorated first() method calls the original result set's first()
 method and decorates the result:
 
-    >>> decorated_result_set.first()
+    >>> print(decorated_result_set.first())
     1 elements in result set
-    u'Dist name is: debian'
+    Dist name is: debian
 
 pre_iter_hook is not called from methods like first() or one() which return
 at most one row:
@@ -115,9 +115,9 @@ at most one row:
 The decorated last() method calls the original result set's last()
 method and decorates the result:
 
-    >>> decorated_result_set.last()
+    >>> print(decorated_result_set.last())
     1 elements in result set
-    u'Dist name is: ubuntutest'
+    Dist name is: ubuntutest
 
 == order_by() ==
 
@@ -128,23 +128,23 @@ method and decorates the result:
     >>> ordered_results = decorated_result_set.order_by(
     ...     Desc(Distribution.name))
     >>> for dist in ordered_results:
-    ...     dist
+    ...     print(dist)
     7 elements in result set
-    u'Dist name is: ubuntutest'
+    Dist name is: ubuntutest
     ...
-    u'Dist name is: debian'
+    Dist name is: debian
 
 == one() ==
 
 The decorated one() method calls the original result set's one()
 method and decorates the result:
 
-    >>> decorated_result_set.config(offset=2, limit=1).one()
+    >>> print(decorated_result_set.config(offset=2, limit=1).one())
     1 elements in result set
-    u'Dist name is: redhat'
+    Dist name is: redhat
 
-    >>> result_decorator(decorated_result_set.result_set.one())
-    u'Dist name is: redhat'
+    >>> print(result_decorator(decorated_result_set.result_set.one()))
+    Dist name is: redhat
 
 == splicing ==
 
@@ -165,9 +165,9 @@ a refined query.
     >>> ubuntu_distros = removeSecurityProxy(decorated_result_set).find(
     ...     "Distribution.name like 'ubuntu%'")
     >>> for dist in ubuntu_distros:
-    ...     dist
-    u'Dist name is: ubuntu'
-    u'Dist name is: ubuntutest'
+    ...     print(dist)
+    Dist name is: ubuntu
+    Dist name is: ubuntutest
 
 
 == get_plain_result_set() ==
diff --git a/lib/lp/services/feeds/doc/feeds.txt b/lib/lp/services/feeds/doc/feeds.txt
index b855388..939c302 100644
--- a/lib/lp/services/feeds/doc/feeds.txt
+++ b/lib/lp/services/feeds/doc/feeds.txt
@@ -141,14 +141,14 @@ be escaped using xml entities such as "&lt;".
 
     >>> from lp.services.feeds.feed import FeedTypedData
     >>> text = FeedTypedData("<b> and &nbsp; and &amp;")
-    >>> text.content
-    u'&lt;b&gt; and &amp;nbsp; and &amp;amp;'
+    >>> print(text.content)
+    &lt;b&gt; and &amp;nbsp; and &amp;amp;
     >>> text2 = FeedTypedData("<b> and &nbsp; and &amp;", content_type="text")
-    >>> text2.content
-    u'&lt;b&gt; and &amp;nbsp; and &amp;amp;'
+    >>> print(text2.content)
+    &lt;b&gt; and &amp;nbsp; and &amp;amp;
     >>> html = FeedTypedData("<b> and &nbsp; and &amp;", content_type="html")
-    >>> html.content
-    u'&lt;b&gt; and &amp;nbsp; and &amp;amp;'
+    >>> print(html.content)
+    &lt;b&gt; and &amp;nbsp; and &amp;amp;
 
 Since xhtml is valid xml, the "<" and ">" characters do not need to be
 escaped. However, xhtml supports many more entities than xml, and Internet
@@ -160,5 +160,5 @@ we are testing xhtml encoding here in case we need it in the future.
 
     >>> xhtml = FeedTypedData("<b> and &nbsp; and &amp;</b><hr/>",
     ...               content_type="xhtml")
-    >>> xhtml.content
-    u'<b> and \xa0 and &amp;</b><hr/>'
+    >>> print(backslashreplace(xhtml.content))
+    <b> and \xa0 and &amp;</b><hr/>
diff --git a/lib/lp/services/fields/doc/uri-field.txt b/lib/lp/services/fields/doc/uri-field.txt
index ef9bb3e..f4c9c00 100644
--- a/lib/lp/services/fields/doc/uri-field.txt
+++ b/lib/lp/services/fields/doc/uri-field.txt
@@ -129,39 +129,40 @@ in a normalised form.
 The default behaviour is to allow both cases:
 
   >>> with_slash = IURIFieldTest['with_slash']
-  >>> with_slash.normalize(u'http://launchpad.net/ubuntu/')
-  u'http://launchpad.net/ubuntu/'
-  >>> with_slash.normalize(u'http://launchpad.net/ubuntu/?query#fragment')
-  u'http://launchpad.net/ubuntu/?query#fragment'
-  >>> with_slash.normalize(u'http://launchpad.net/ubuntu')
-  u'http://launchpad.net/ubuntu/'
-  >>> with_slash.normalize(u'http://launchpad.net')
-  u'http://launchpad.net/'
+  >>> print(with_slash.normalize(u'http://launchpad.net/ubuntu/'))
+  http://launchpad.net/ubuntu/
+  >>> print(with_slash.normalize(
+  ...     u'http://launchpad.net/ubuntu/?query#fragment'))
+  http://launchpad.net/ubuntu/?query#fragment
+  >>> print(with_slash.normalize(u'http://launchpad.net/ubuntu'))
+  http://launchpad.net/ubuntu/
+  >>> print(with_slash.normalize(u'http://launchpad.net'))
+  http://launchpad.net/
 
 Similarly, we can require that the URI path does not end in a slash:
 
   >>> without_slash = IURIFieldTest['without_slash']
-  >>> without_slash.normalize(u'http://launchpad.net/ubuntu')
-  u'http://launchpad.net/ubuntu'
-  >>> without_slash.normalize(u'http://launchpad.net/ubuntu/#fragment')
-  u'http://launchpad.net/ubuntu#fragment'
-  >>> without_slash.normalize(u'http://launchpad.net/ubuntu#fragment/')
-  u'http://launchpad.net/ubuntu#fragment/'
-  >>> without_slash.normalize(u'http://launchpad.net/ubuntu/')
-  u'http://launchpad.net/ubuntu'
+  >>> print(without_slash.normalize(u'http://launchpad.net/ubuntu'))
+  http://launchpad.net/ubuntu
+  >>> print(without_slash.normalize(u'http://launchpad.net/ubuntu/#fragment'))
+  http://launchpad.net/ubuntu#fragment
+  >>> print(without_slash.normalize(u'http://launchpad.net/ubuntu#fragment/'))
+  http://launchpad.net/ubuntu#fragment/
+  >>> print(without_slash.normalize(u'http://launchpad.net/ubuntu/'))
+  http://launchpad.net/ubuntu
 
 URIs with an authority but a blank path get canonicalised to a path of
 "/", which is not affected by the without_slash setting.
 
-  >>> with_slash.normalize(u'http://launchpad.net/')
-  u'http://launchpad.net/'
-  >>> with_slash.normalize(u'http://launchpad.net')
-  u'http://launchpad.net/'
+  >>> print(with_slash.normalize(u'http://launchpad.net/'))
+  http://launchpad.net/
+  >>> print(with_slash.normalize(u'http://launchpad.net'))
+  http://launchpad.net/
 
-  >>> without_slash.normalize(u'http://launchpad.net/')
-  u'http://launchpad.net/'
-  >>> without_slash.normalize(u'http://launchpad.net')
-  u'http://launchpad.net/'
+  >>> print(without_slash.normalize(u'http://launchpad.net/'))
+  http://launchpad.net/
+  >>> print(without_slash.normalize(u'http://launchpad.net'))
+  http://launchpad.net/
 
 
 == Null values ==
@@ -208,5 +209,5 @@ Multiple values will cause an UnexpectedFormData exception:
 
 Values with leading and trailing whitespace are stripped.
 
-   >>> widget._toFieldValue('  http://www.ubuntu.com/   ')
-   u'http://www.ubuntu.com/'
+   >>> print(widget._toFieldValue('  http://www.ubuntu.com/   '))
+   http://www.ubuntu.com/
diff --git a/lib/lp/services/geoip/doc/geoip.txt b/lib/lp/services/geoip/doc/geoip.txt
index 8767ee2..aeba9fb 100644
--- a/lib/lp/services/geoip/doc/geoip.txt
+++ b/lib/lp/services/geoip/doc/geoip.txt
@@ -11,8 +11,8 @@ from a given IP address.
 The getCountryByAddr() method will return the country of the given IP
 address.
 
-    >>> geoip.getCountryByAddr('201.13.165.145').name
-    u'Brazil'
+    >>> print(geoip.getCountryByAddr('201.13.165.145').name)
+    Brazil
 
 When running tests the IP address will start with '127.', and GeoIP
 would, obviously, fail to find the country for that, so we return None.
diff --git a/lib/lp/services/gpg/doc/gpg-encryption.txt b/lib/lp/services/gpg/doc/gpg-encryption.txt
index d26a497..d453e14 100644
--- a/lib/lp/services/gpg/doc/gpg-encryption.txt
+++ b/lib/lp/services/gpg/doc/gpg-encryption.txt
@@ -26,8 +26,8 @@ Sample Person has public and secret keys set.
     True
 
     >>> login('test@xxxxxxxxxxxxx')
-    >>> bag.user.name
-    u'name12'
+    >>> print(bag.user.name)
+    name12
 
     >>> from lp.services.gpg.interfaces import IGPGHandler
     >>> gpghandler = getUtility(IGPGHandler)
@@ -46,8 +46,8 @@ property, since both are generating using GPGKeyset.getGPGKeys)
 
 Note fingerprint is also unicode.
 
-    >>> fingerprint
-    u'A419AE861E88BC9E04B9C26FBA2B9389DFD20543'
+    >>> print(fingerprint)
+    A419AE861E88BC9E04B9C26FBA2B9389DFD20543
 
     >>> key = gpghandler.retrieveKey(fingerprint)
     >>> cipher = gpghandler.encryptContent(content.encode('utf-8'), key)
@@ -62,16 +62,16 @@ only way we can decrypt the cipher content.
 
 voilá, the same content shows up again. 
     
-    >>> plain.decode('utf-8')
-    u'\ufcber'	
+    >>> print(backslashreplace(plain.decode('utf-8')))
+    \ufcber
 
 Verify if the encrytion process support passing another charset string
 
     >>> content = u'a\xe7ucar'
     >>> cipher = gpghandler.encryptContent(content.encode('iso-8859-1'), key)
     >>> plain = decrypt_content(cipher, 'test')    
-    >>> plain.decode('iso-8859-1')
-    u'a\xe7ucar'
+    >>> print(backslashreplace(plain.decode('iso-8859-1')))
+    a\xe7ucar
 
 Let's try to pass unicode and see if it fails
 
diff --git a/lib/lp/services/gpg/doc/gpg-signatures.txt b/lib/lp/services/gpg/doc/gpg-signatures.txt
index 6ba60df..2f98077 100644
--- a/lib/lp/services/gpg/doc/gpg-signatures.txt
+++ b/lib/lp/services/gpg/doc/gpg-signatures.txt
@@ -20,8 +20,8 @@ OpenPGP Signature Verification
     True
 
     >>> login('test@xxxxxxxxxxxxx')
-    >>> bag.user.name
-    u'name12'
+    >>> print(bag.user.name)
+    name12
 
     >>> from zope.component import getUtility
     >>> from lp.services.gpg.interfaces import IGPGHandler
diff --git a/lib/lp/services/helpers.py b/lib/lp/services/helpers.py
index b102871..f676e61 100644
--- a/lib/lp/services/helpers.py
+++ b/lib/lp/services/helpers.py
@@ -42,8 +42,11 @@ def text_replaced(text, replacements, _cache={}):
 
     Unicode strings work too.
 
-    >>> text_replaced(u'1 2 3 4', {u'1': u'2', u'2': u'1'})
-    u'2 1 3 4'
+    >>> replaced = text_replaced(u'1 2 3 4', {u'1': u'2', u'2': u'1'})
+    >>> isinstance(replaced, six.text_type)
+    True
+    >>> print(replaced)
+    2 1 3 4
 
     The argument _cache is used as a cache of replacements that were requested
     before, so we only compute regular expressions once.
diff --git a/lib/lp/services/identity/doc/account.txt b/lib/lp/services/identity/doc/account.txt
index b9eabb1..f12fd42 100644
--- a/lib/lp/services/identity/doc/account.txt
+++ b/lib/lp/services/identity/doc/account.txt
@@ -125,8 +125,8 @@ database. Only an admin can change the status.
 An Account has at least one OpenID identifier used to generate the
 OpenID identity URL.
 
-    >>> account.openid_identifiers.any().identifier
-    u'no-priv_oid'
+    >>> print(account.openid_identifiers.any().identifier)
+    no-priv_oid
 
 
 Creating new accounts
diff --git a/lib/lp/services/identity/doc/emailaddress.txt b/lib/lp/services/identity/doc/emailaddress.txt
index 5d8d9d6..607250b 100644
--- a/lib/lp/services/identity/doc/emailaddress.txt
+++ b/lib/lp/services/identity/doc/emailaddress.txt
@@ -53,8 +53,8 @@ Registering a new email address works -- and preserves case -- though:
 
     >>> emailaddress = emailset.new(
     ...     'oink@xxxxxxxxxxxxx', foobar)
-    >>> emailaddress.email
-    u'oink@xxxxxxxxxxxxx'
+    >>> print(emailaddress.email)
+    oink@xxxxxxxxxxxxx
 
 Generating SHA1 hashes for RDF output is easy:
 
@@ -65,13 +65,19 @@ There's a convenience method on IEmailAddressSet to pull preferred email
 addresses for a set of people:
 
     >>> guadamen = personset.getByName('guadamen')
-    >>> [emailaddress.email for emailaddress in
-    ...  emailset.getPreferredEmailForPeople(guadamen.allmembers)]
-    [u'celso.providelo@xxxxxxxxxxxxx', u'colin.watson@xxxxxxxxxxxxxxx',
-     u'daniel.silverstone@xxxxxxxxxxxxx', u'edgar@xxxxxxxxxxxxxxxx',
-     u'foo.bar@xxxxxxxxxxxxx', u'jeff.waugh@xxxxxxxxxxxxxxx',
-     u'limi@xxxxxxxxx', u'mark@xxxxxxxxxxx',
-     u'steve.alexander@xxxxxxxxxxxxxxx', u'support@xxxxxxxxxx']
+    >>> for emailaddress in emailset.getPreferredEmailForPeople(
+    ...         guadamen.allmembers):
+    ...     print(emailaddress.email)
+    celso.providelo@xxxxxxxxxxxxx
+    colin.watson@xxxxxxxxxxxxxxx
+    daniel.silverstone@xxxxxxxxxxxxx
+    edgar@xxxxxxxxxxxxxxxx
+    foo.bar@xxxxxxxxxxxxx
+    jeff.waugh@xxxxxxxxxxxxxxx
+    limi@xxxxxxxxx
+    mark@xxxxxxxxxxx
+    steve.alexander@xxxxxxxxxxxxxxx
+    support@xxxxxxxxxx
 
 
 Deleting email addresses
diff --git a/lib/lp/services/mail/doc/notification-recipient-set.txt b/lib/lp/services/mail/doc/notification-recipient-set.txt
index a1030ee..059cd7b 100644
--- a/lib/lp/services/mail/doc/notification-recipient-set.txt
+++ b/lib/lp/services/mail/doc/notification-recipient-set.txt
@@ -49,8 +49,10 @@ existing values if they apply to your context.
 The getPersons() method returns the list of recipients sorted by display
 name.
 
-    >>> [person.displayname for person in recipients.getRecipients()]
-    [u'Celso Providelo', u'Sample Person']
+    >>> for person in recipients.getRecipients():
+    ...     print(person.displayname)
+    Celso Providelo
+    Sample Person
 
 It's also possible to iterate over the recipients:
 
@@ -152,8 +154,11 @@ person:
     >>> 'support@xxxxxxxxxx' in recipients
     True
 
-    >>> [person.displayname for person in recipients]
-    [u'Celso Providelo', u'Sample Person', u'Ubuntu Team']
+    >>> for person in recipients:
+    ...     print(person.displayname)
+    Celso Providelo
+    Sample Person
+    Ubuntu Team
 
     >>> recipients.getEmails()
     ['celso.providelo@xxxxxxxxxxxxx', 'support@xxxxxxxxxx',
@@ -180,8 +185,9 @@ addresses are added to the recipients list, and this recursively.
 
 But looking at the recipients list, only the team is listed:
 
-    >>> [person.displayname for person in recipients]
-    [u'Ubuntu Gnome Team']
+    >>> for person in recipients:
+    ...     print(person.displayname)
+    Ubuntu Gnome Team
 
 So Sample Person is not in the recipients list, even if their email will
 be notified for they're a member of Warty Security Team, itself a member of
@@ -224,8 +230,10 @@ be added with the same rationale:
     >>> recipients = NotificationRecipientSet()
     >>> recipients.add(
     ...     [sample_person, no_priv], 'Notified for fun.', 'Fun')
-    >>> [person.displayname for person in recipients.getRecipients()]
-    [u'No Privileges Person', u'Sample Person']
+    >>> for person in recipients.getRecipients():
+    ...     print(person.displayname)
+    No Privileges Person
+    Sample Person
 
     >>> print_reason(recipients.getReason(no_priv))
     Fun (Notified for fun.)
@@ -243,12 +251,16 @@ NotificationRecipientSet():
     >>> recipients = NotificationRecipientSet()
     >>> recipients.add(
     ...     [sample_person, no_priv, cprov], 'Notified for fun.', 'Fun')
-    >>> [person.displayname for person in recipients.getRecipients()]
-    [u'Celso Providelo', u'No Privileges Person', u'Sample Person']
+    >>> for person in recipients.getRecipients():
+    ...     print(person.displayname)
+    Celso Providelo
+    No Privileges Person
+    Sample Person
 
     >>> recipients.remove([sample_person, cprov])
-    >>> [person.displayname for person in recipients.getRecipients()]
-    [u'No Privileges Person']
+    >>> for person in recipients.getRecipients():
+    ...     print(person.displayname)
+    No Privileges Person
 
     >>> recipients.getEmails()
     ['no-priv@xxxxxxxxxxxxx']
diff --git a/lib/lp/services/mail/doc/sending-mail.txt b/lib/lp/services/mail/doc/sending-mail.txt
index f4f0e7f..88bf4c9 100644
--- a/lib/lp/services/mail/doc/sending-mail.txt
+++ b/lib/lp/services/mail/doc/sending-mail.txt
@@ -73,8 +73,8 @@ simple_sendmail_from_person uses the Person's preferred email address:
     ...     'testing@xxxxxxxxxxxxx')
     >>> sample_person.setPreferredEmail(testing)
 
-    >>> sample_person.preferredemail.email
-    u'testing@xxxxxxxxxxxxx'
+    >>> print(sample_person.preferredemail.email)
+    testing@xxxxxxxxxxxxx
     >>> msgid = simple_sendmail_from_person(
     ...     person=sample_person,
     ...     to_addrs='test@xxxxxxxxxxxxx',
@@ -131,11 +131,12 @@ Now let's look at the sent email again.
 
     >>> from email.header import decode_header
     >>> subject_str, charset = decode_header(msg['Subject'])[0]
-    >>> subject_str.decode(charset)
-    u'\xc4mnesrad'
+    >>> print(backslashreplace(subject_str.decode(charset)))
+    \xc4mnesrad
 
-    >>> msg.get_payload(decode=True).decode(msg.get_content_charset())
-    u'Inneh\xe5ll'
+    >>> print(backslashreplace(
+    ...     msg.get_payload(decode=True).decode(msg.get_content_charset())))
+    Inneh\xe5ll
 
 
 If we use simple_sendmail_from_person, the person's display_name can
@@ -158,15 +159,16 @@ contain non-ASCII characters:
     >>> from_name_str, charset = decode_header(from_name_encoded)[0]
     >>> from_addr
     'foo.bar@xxxxxxxxxxxxx'
-    >>> from_name_str.decode(charset)
-    u'F\xf6\xf6 B\u0105r'
+    >>> print(backslashreplace(from_name_str.decode(charset)))
+    F\xf6\xf6 B\u0105r
 
     >>> subject_str, charset = decode_header(msg['Subject'])[0]
-    >>> subject_str.decode(charset)
-    u'\xc4mnesrad'
+    >>> print(backslashreplace(subject_str.decode(charset)))
+    \xc4mnesrad
 
-    >>> msg.get_payload(decode=True).decode(msg.get_content_charset())
-    u'Inneh\xe5ll'
+    >>> print(backslashreplace(
+    ...     msg.get_payload(decode=True).decode(msg.get_content_charset())))
+    Inneh\xe5ll
 
 simple_sendmail_from_person also makes sure that the name gets
 surrounded by quotes and quoted if necessary:
diff --git a/lib/lp/services/messages/doc/message.txt b/lib/lp/services/messages/doc/message.txt
index 3113703..06d63e5 100644
--- a/lib/lp/services/messages/doc/message.txt
+++ b/lib/lp/services/messages/doc/message.txt
@@ -16,15 +16,15 @@ on bugs. Bugs are linked to Messages via the BugMessage table.
     ...     subject='The Title', content='The Content', bug=bug_one,
     ...     owner=current_user)
     >>> msg = bmsg.message
-    >>> msg.subject
-    u'The Title'
+    >>> print(msg.subject)
+    The Title
 
 We can retrieve the text chunks as a unicode string. (However, note that
 if generating HTML you should use the MessageChunks detailed below
 to handle attachments correctly)
 
-    >>> msg.text_contents
-    u'The Content'
+    >>> print(msg.text_contents)
+    The Content
 
 Messages are threaded, although this is not necessarily displayed in the
 UI. Each message has a parent attribute. If the message was created via
@@ -52,8 +52,8 @@ may be accessed using the chunks attribute, or simply by iterating over
 the Message.
 
     >>> for chunk in msg:
-    ...     print repr([chunk.sequence, chunk.content, chunk.blob])
-    [1, u'The Content', None]
+    ...     print pretty([chunk.sequence, chunk.content, chunk.blob])
+    [1, 'The Content', None]
     >>> msg.chunks[0].message == msg
     True
 
@@ -124,20 +124,22 @@ normal.
     >>> msg = msg_set.get(
     ...     rfc822msgid="<20050405054002.22134.71562@localhost.localdomain>"
     ...     )[0]
-    >>> msg.title
-    u'Unicode\u2122'
+    >>> print(msg.title)
+    Unicode™
     >>> chunks = msg.chunks
     >>> for chunk in chunks:
     ...     if chunk.content:
-    ...         print '%2d - %r' % (chunk.sequence, chunk.content)
-     1 - u'Plain text'
-     3 - u'Unicode\u2122 text'
+    ...         print '%2d - %s' % (chunk.sequence, pretty(chunk.content))
+     1 - 'Plain text'
+     3 - 'Unicode\u2122 text'
 
 The text_contents attribute contains only the text parts, since that is
 what we want to display in the UI.
 
-    >>> msg.text_contents
-    u'Plain text\n\nUnicode\u2122 text'
+    >>> print(msg.text_contents)
+    Plain text
+    <BLANKLINE>
+    Unicode™ text
 
 Stripping non-text parts is useful, since a lot of messages will contain
 detach GPG signatures, and they shouldn't be shown in the UI since they
@@ -176,16 +178,16 @@ of type text/plain, so are stored as blobs.
     >>> chunks[1].content is None
     True
     >>> blob = chunks[1].blob
-    >>> blob.filename
-    u'anna.jpg.exe'
+    >>> print(blob.filename)
+    anna.jpg.exe
     >>> blob.http_url.endswith(u'/anna.jpg.exe')
     True
 
     >>> blob2 = chunks[3].blob
-    >>> blob2.filename
-    u'unnamed'
-    >>> blob2.mimetype
-    u'application/xml; charset="utf16"'
+    >>> print(blob2.filename)
+    unnamed
+    >>> print(blob2.mimetype)
+    application/xml; charset="utf16"
 
 Only those chunks of content type text/plain with content-disposition
 "inline" that have no filename are stored as content. If an inline
@@ -251,12 +253,12 @@ used as a default.
     >>> msg = msgset.fromEmail(raw_msg)
     >>> for chunk in msg.chunks:
     ...     if chunk.content is not None:
-    ...         print '%d - %r' % (chunk.sequence, chunk.content)
+    ...         print '%d - %s' % (chunk.sequence, pretty(chunk.content))
     ...     else:
     ...         print '%d - file: %s (%s)' % (
     ...             chunk.sequence, chunk.blob.filename, chunk.blob.mimetype)
-    1 - u'Plain text'
-    2 - u'Plain text without a ch\xc4\x83\xc5\x95\xc5\x9d\xc4\x9b\xc5\xa3.'
+    1 - 'Plain text'
+    2 - 'Plain text without a ch\xc4\x83\xc5\x95\xc5\x9d\xc4\x9b\xc5\xa3.'
     3 - file: attachment.txt   (text/plain; charset="us-ascii")
     4 - file: unnamed          (text/plain; charset="us-ascii")
     5 - file: attachment2.txt  (text/plain; charset="us-ascii")
@@ -270,8 +272,9 @@ as the request. I don't think this is important outside of tests.
     >>> blob.read()
     '\x00\x01\x02\x03'
 
-    >>> blob2.read().decode('utf16')
-    u'<?xml version="1.0" encoding="utf16"?>\n<unicode>\u2122</unicode>'
+    >>> print(blob2.read().decode('utf16'))
+    <?xml version="1.0" encoding="utf16"?>
+    <unicode>™</unicode>
 
 
 We can also retrieve a byte-identical copy of the original message
@@ -349,13 +352,14 @@ quite happily.
     ...
     ... Foo Bar
     ... ''')
-    >>> msg.title
-    u'Test'
+    >>> print(msg.title)
+    Test
     >>> chunks = list(msg.chunks)
     >>> len(chunks)
     1
-    >>> chunks[0].content
-    u'Foo Bar\n'
+    >>> print(chunks[0].content)
+    Foo Bar
+    <BLANKLINE>
 
 It also handles the case where the subject line is folded.
 
@@ -369,8 +373,8 @@ It also handles the case where the subject line is folded.
     ... Foo Bar
     ... ''')
 
-    >>> msg.title
-    u'Folded subject'
+    >>> print(msg.title)
+    Folded subject
 
 
 However, there are some things it refuses to deal with. In particular, it
@@ -398,8 +402,8 @@ explicitly told to do so:
     ...
     ... Foo Bar
     ... ''', create_missing_persons=True)
-    >>> msg.subject
-    u'Foo Bar Bazarooney!'
+    >>> print(msg.subject)
+    Foo Bar Bazarooney!
 
 When the fromEmail() method creates a new Person entry, it'll set the
 creation_rationale accordingly.
diff --git a/lib/lp/services/oauth/doc/oauth-pages.txt b/lib/lp/services/oauth/doc/oauth-pages.txt
index 8c29dc9..5b9cd98 100644
--- a/lib/lp/services/oauth/doc/oauth-pages.txt
+++ b/lib/lp/services/oauth/doc/oauth-pages.txt
@@ -71,8 +71,8 @@ context either.
     oauth_token...
 
     >>> view.reviewToken(OAuthPermission.READ_PRIVATE, None)
-    >>> token.person.name
-    u'salgado'
+    >>> print(token.person.name)
+    salgado
     >>> print(token.context)
     None
     >>> token.permission
@@ -84,8 +84,8 @@ The context can be a product, and if it's specified it will be carried
 over to the token once it's reviewed.
 
     >>> view, token = get_view_with_fresh_token({'lp.context': 'firefox'})
-    >>> view.token_context.name
-    u'firefox'
+    >>> print(view.token_context.name)
+    firefox
 
     # The context is also stored in a hidden field in the HTML so that
     # it's submitted together with the user's chosen permission.
@@ -95,14 +95,14 @@ over to the token once it's reviewed.
     lp.context firefox
 
     >>> view.reviewToken(OAuthPermission.READ_PUBLIC, None)
-    >>> token.context.name
-    u'firefox'
+    >>> print(token.context.name)
+    firefox
 
 Likewise for a project.
 
     >>> view, token = get_view_with_fresh_token({'lp.context': 'mozilla'})
-    >>> view.token_context.name
-    u'mozilla'
+    >>> print(view.token_context.name)
+    mozilla
 
     # The context is also stored in a hidden field in the HTML so that
     # it's submitted together with the user's chosen permission.
@@ -112,14 +112,14 @@ Likewise for a project.
     lp.context mozilla
 
     >>> view.reviewToken(OAuthPermission.READ_PUBLIC, None)
-    >>> token.context.name
-    u'mozilla'
+    >>> print(token.context.name)
+    mozilla
 
 And a distribution.
 
     >>> view, token = get_view_with_fresh_token({'lp.context': 'ubuntu'})
-    >>> view.token_context.name
-    u'ubuntu'
+    >>> print(view.token_context.name)
+    ubuntu
 
     # The context is also stored in a hidden field in the HTML so that
     # it's submitted together with the user's chosen permission.
@@ -129,16 +129,16 @@ And a distribution.
     lp.context ubuntu
 
     >>> view.reviewToken(OAuthPermission.READ_PUBLIC, None)
-    >>> token.context.name
-    u'ubuntu'
+    >>> print(token.context.name)
+    ubuntu
 
 If the consumer wants to access only things related to a distribution's
 package, it must specify the distribution and the package's name.
 
     >>> view, token = get_view_with_fresh_token(
     ...     {'lp.context': 'ubuntu/evolution'})
-    >>> view.token_context.title
-    u'...evolution... package in Ubuntu'
+    >>> print(view.token_context.title)
+    evolution package in Ubuntu
 
     # The context is also stored in a hidden field in the HTML so that
     # it's submitted together with the user's chosen permission.
@@ -148,8 +148,8 @@ package, it must specify the distribution and the package's name.
     lp.context ubuntu/evolution
 
     >>> view.reviewToken(OAuthPermission.READ_PUBLIC, None)
-    >>> token.context.title
-    u'...evolution... package in Ubuntu'
+    >>> print(token.context.title)
+    evolution package in Ubuntu
 
 An error is raised if the context is not found.
 
diff --git a/lib/lp/services/oauth/stories/authorize-token.txt b/lib/lp/services/oauth/stories/authorize-token.txt
index e217b8a..23f4911 100644
--- a/lib/lp/services/oauth/stories/authorize-token.txt
+++ b/lib/lp/services/oauth/stories/authorize-token.txt
@@ -198,8 +198,8 @@ person set.
     # transaction.
     >>> login(ANONYMOUS)
     >>> token = consumer.getRequestToken(token.key)
-    >>> token.person.name
-    u'no-priv'
+    >>> print(token.person.name)
+    no-priv
     >>> token.permission
     <DBItem OAuthPermission.READ_PUBLIC...
     >>> token.is_reviewed
diff --git a/lib/lp/services/verification/doc/logintoken.txt b/lib/lp/services/verification/doc/logintoken.txt
index 3f3a50a..14e6335 100644
--- a/lib/lp/services/verification/doc/logintoken.txt
+++ b/lib/lp/services/verification/doc/logintoken.txt
@@ -56,8 +56,9 @@ As the process is not yet finished, foobar will see this as one of their
 unconfirmed email addresses.
 
     >>> flush_database_updates()
-    >>> foobar.unvalidatedemails
-    [u'foo.bar2@xxxxxxxxxxxxx']
+    >>> for email in foobar.unvalidatedemails:
+    ...     print(email)
+    foo.bar2@xxxxxxxxxxxxx
 
 It's possible to create another token for the same purpose, but this
 won't cause that email to show up twice on foobar's list of unconfirmed
@@ -67,8 +68,9 @@ emails.
     ...     foobar, 'foo.bar@xxxxxxxxxxxxx', 'foo.bar2@xxxxxxxxxxxxx',
     ...     LoginTokenType.VALIDATEEMAIL)
     >>> flush_database_updates()
-    >>> foobar.unvalidatedemails
-    [u'foo.bar2@xxxxxxxxxxxxx']
+    >>> for email in foobar.unvalidatedemails:
+    ...     print(email)
+    foo.bar2@xxxxxxxxxxxxx
 
 Once foobar finished the process, confirming their new email address, we
 mark the token as consumed.
@@ -113,8 +115,9 @@ our own making.
     ...     LoginTokenType.VALIDATEEMAIL)
     >>> flush_database_updates()
 
-    >>> foobar.unvalidatedemails
-    [u'foo.bar2@xxxxxxxxxxxxx']
+    >>> for email in foobar.unvalidatedemails:
+    ...     print(email)
+    foo.bar2@xxxxxxxxxxxxx
 
     >>> token3.consume()
     >>> token3.date_consumed is not None
diff --git a/lib/lp/services/webapp/doc/canonical_url.txt b/lib/lp/services/webapp/doc/canonical_url.txt
index ba277c9..2730fa2 100644
--- a/lib/lp/services/webapp/doc/canonical_url.txt
+++ b/lib/lp/services/webapp/doc/canonical_url.txt
@@ -132,8 +132,8 @@ Now, there is an ICanonicalUrlData registered for ITown.
     >>> from lp.testing import verifyObject
     >>> verifyObject(ICanonicalUrlData, town_urldata)
     True
-    >>> town_urldata.path
-    u'+towns/London'
+    >>> print(town_urldata.path)
+    +towns/London
     >>> town_urldata.inside is country_instance
     True
 
@@ -248,14 +248,14 @@ stitching together the various ICanonicalUrlData adapters for that object
 and the objects it is inside of (or in other words, hierarchically below).
 
     >>> from lp.services.webapp import canonical_url
-    >>> canonical_url(getUtility(ILaunchpadRoot))
-    u'http://launchpad.test/'
-    >>> canonical_url(countryset_instance)
-    u'http://launchpad.test/countries'
-    >>> canonical_url(country_instance)
-    u'http://launchpad.test/countries/England'
-    >>> canonical_url(town_instance)
-    u'http://launchpad.test/countries/England/+towns/London'
+    >>> print(canonical_url(getUtility(ILaunchpadRoot)))
+    http://launchpad.test/
+    >>> print(canonical_url(countryset_instance))
+    http://launchpad.test/countries
+    >>> print(canonical_url(country_instance))
+    http://launchpad.test/countries/England
+    >>> print(canonical_url(town_instance))
+    http://launchpad.test/countries/England/+towns/London
 
 We can see that this is the mainsite rooturl as configured in launchpad.conf.
 
@@ -375,20 +375,20 @@ zope.publisher.interfaces.http.IHTTPApplicationRequest.
     ...         return self.applicationurl
 
     >>> mandrill_request = FakeRequest('https://mandrill.example.org:23')
-    >>> canonical_url(country_instance)
-    u'http://launchpad.test/countries/England'
-    >>> canonical_url(country_instance, mandrill_request)
-    u'https://mandrill.example.org:23/countries/England'
+    >>> print(canonical_url(country_instance))
+    http://launchpad.test/countries/England
+    >>> print(canonical_url(country_instance, mandrill_request))
+    https://mandrill.example.org:23/countries/England
 
 However, if we log in, then that request should be used when none is explicitly
 given otherwise.
 
     >>> sesame_request = FakeRequest('http://muppet.example.com')
     >>> login(ANONYMOUS, sesame_request)
-    >>> canonical_url(country_instance)
-    u'http://muppet.example.com/countries/England'
-    >>> canonical_url(country_instance, mandrill_request)
-    u'https://mandrill.example.org:23/countries/England'
+    >>> print(canonical_url(country_instance))
+    http://muppet.example.com/countries/England
+    >>> print(canonical_url(country_instance, mandrill_request))
+    https://mandrill.example.org:23/countries/England
 
 
 == canonical_url and overriding rootsite ==
@@ -402,10 +402,10 @@ a rootsite.
 
 Overriding the rootsite from the default request:
 
-    >>> canonical_url(country_instance)
-    u'http://muppet.example.com/countries/England'
-    >>> canonical_url(country_instance, rootsite='code')
-    u'http://code.launchpad.test/countries/England'
+    >>> print(canonical_url(country_instance))
+    http://muppet.example.com/countries/England
+    >>> print(canonical_url(country_instance, rootsite='code'))
+    http://code.launchpad.test/countries/England
 
 Webapp vhost overrides can be ignored by setting the
 app.mainsite_only.canonical_url feature flag, so all links end up on
@@ -413,17 +413,18 @@ mainsite. Non-webapp vhosts (eg. api and feeds) are unaffected.
 
     >>> from lp.services.features.testing import MemoryFeatureFixture
     >>> with MemoryFeatureFixture({'app.mainsite_only.canonical_url': 'on'}):
-    ...     canonical_url(country_instance, rootsite='code')
-    ...     canonical_url(country_instance, rootsite='api')
-    u'http://launchpad.test/countries/England'
-    u'http://api.launchpad.test/countries/England'
+    ...     print(canonical_url(country_instance, rootsite='code'))
+    ...     print(canonical_url(country_instance, rootsite='api'))
+    http://launchpad.test/countries/England
+    http://api.launchpad.test/countries/England
 
 Overriding the rootsite from the specified request:
 
-    >>> canonical_url(country_instance, mandrill_request)
-    u'https://mandrill.example.org:23/countries/England'
-    >>> canonical_url(country_instance, mandrill_request, rootsite='code')
-    u'http://code.launchpad.test/countries/England'
+    >>> print(canonical_url(country_instance, mandrill_request))
+    https://mandrill.example.org:23/countries/England
+    >>> print(canonical_url(
+    ...     country_instance, mandrill_request, rootsite='code'))
+    http://code.launchpad.test/countries/England
 
 And if the configuration does provide a rootsite:
 
@@ -446,15 +447,16 @@ And if the configuration does provide a rootsite:
     ... </configure>
     ... """.format(module_name=module.__name__))
 
-    >>> canonical_url(country_instance)
-    u'http://bugs.launchpad.test/countries/England'
-    >>> canonical_url(country_instance, rootsite='code')
-    u'http://code.launchpad.test/countries/England'
-    >>> canonical_url(country_instance, mandrill_request, rootsite='code')
-    u'http://code.launchpad.test/countries/England'
+    >>> print(canonical_url(country_instance))
+    http://bugs.launchpad.test/countries/England
+    >>> print(canonical_url(country_instance, rootsite='code'))
+    http://code.launchpad.test/countries/England
+    >>> print(canonical_url(
+    ...     country_instance, mandrill_request, rootsite='code'))
+    http://code.launchpad.test/countries/England
     >>> with MemoryFeatureFixture({'app.mainsite_only.canonical_url': 'on'}):
-    ...     canonical_url(country_instance)
-    u'http://launchpad.test/countries/England'
+    ...     print(canonical_url(country_instance))
+    http://launchpad.test/countries/England
 
 
 == canonical_url and named views ==
@@ -462,20 +464,20 @@ And if the configuration does provide a rootsite:
 The url for a particular view of an object can be generated by specifying
 the view's name.
 
-    >>> canonical_url(country_instance, view_name="+map")
-    u'http://bugs.launchpad.test/countries/England/+map'
+    >>> print(canonical_url(country_instance, view_name="+map"))
+    http://bugs.launchpad.test/countries/England/+map
 
 view_name also works when the view_name refers to a Navigation stepto,
 stepthrough, or redirection:
 
-    >>> canonical_url(country_instance, view_name="+greenwich")
-    u'http://bugs.launchpad.test/countries/England/+greenwich'
+    >>> print(canonical_url(country_instance, view_name="+greenwich"))
+    http://bugs.launchpad.test/countries/England/+greenwich
 
-    >>> canonical_url(country_instance, view_name="+capital")
-    u'http://bugs.launchpad.test/countries/England/+capital'
+    >>> print(canonical_url(country_instance, view_name="+capital"))
+    http://bugs.launchpad.test/countries/England/+capital
 
-    >>> canonical_url(country_instance, view_name="+towns")
-    u'http://bugs.launchpad.test/countries/England/+towns'
+    >>> print(canonical_url(country_instance, view_name="+towns"))
+    http://bugs.launchpad.test/countries/England/+towns
 
 Giving an unregistered view name will trigger an assertion failure.
 
@@ -525,15 +527,15 @@ a web service request
     >>> get_current_browser_request() is api_request
     True
 
-    >>> canonical_url(countryset_instance)
-    u'http://launchpad.test/countries'
+    >>> print(canonical_url(countryset_instance))
+    http://launchpad.test/countries
 
 
 If an URL that can be used in the web service is required, a web service
 request has to be passed in explicitly.
 
-    >>> canonical_url(countryset_instance, request=api_request)
-    u'http://api.launchpad.test/countries'
+    >>> print(canonical_url(countryset_instance, request=api_request))
+    http://api.launchpad.test/countries
 
 It is often the case that the web application wants to provide URLs that will
 be written out onto the pages that the Javascript can process using the
@@ -541,8 +543,8 @@ LP.client code to get access to the object entries using the API.  In these
 cases, the "force_local_path" parameter can be passed to canonical_url to have
 only the relative local path returned.
 
-    >>> canonical_url(countryset_instance, force_local_path=True)
-    u'/countries'
+    >>> print(canonical_url(countryset_instance, force_local_path=True))
+    /countries
 
 
 == The end ==
diff --git a/lib/lp/services/webapp/doc/canonical_url_examples.txt b/lib/lp/services/webapp/doc/canonical_url_examples.txt
index 6db9c6a..ec107a5 100644
--- a/lib/lp/services/webapp/doc/canonical_url_examples.txt
+++ b/lib/lp/services/webapp/doc/canonical_url_examples.txt
@@ -29,23 +29,23 @@ Application homepages
 
 The Launchpad homepage.
 
-    >>> canonical_url(getUtility(ILaunchpadRoot))
-    u'http://launchpad.test/'
+    >>> print(canonical_url(getUtility(ILaunchpadRoot)))
+    http://launchpad.test/
 
 The Malone homepage.
 
-    >>> canonical_url(getUtility(IMaloneApplication))
-    u'http://launchpad.test/bugs'
+    >>> print(canonical_url(getUtility(IMaloneApplication)))
+    http://launchpad.test/bugs
 
 The Bazaar homepage.
 
-    >>> canonical_url(getUtility(IBazaarApplication))
-    u'http://code.launchpad.test/+code'
+    >>> print(canonical_url(getUtility(IBazaarApplication)))
+    http://code.launchpad.test/+code
 
 The Answer Tracker
 
-    >>> canonical_url(getUtility(IQuestionSet))
-    u'http://answers.launchpad.test/questions'
+    >>> print(canonical_url(getUtility(IQuestionSet)))
+    http://answers.launchpad.test/questions
 
 Launchpad Translations (Rosetta) canonical_url examples are in
 lib/lp/translations/doc/canonical_url_examples.txt.
@@ -62,40 +62,40 @@ Persons and Teams
 
 The IPersonSet.
 
-    >>> canonical_url(getUtility(IPersonSet))
-    u'http://launchpad.test/people'
+    >>> print(canonical_url(getUtility(IPersonSet)))
+    http://launchpad.test/people
 
 An IPerson.
 
-    >>> canonical_url(getUtility(IPersonSet).getByName('mark'))
-    u'http://launchpad.test/~mark'
+    >>> print(canonical_url(getUtility(IPersonSet).getByName('mark')))
+    http://launchpad.test/~mark
 
 An ITeam.
 
-    >>> canonical_url(celebs.rosetta_experts)
-    u'http://launchpad.test/~rosetta-admins'
+    >>> print(canonical_url(celebs.rosetta_experts))
+    http://launchpad.test/~rosetta-admins
 
 An ICodeOfConductSet
 
     >>> cocset = getUtility(ICodeOfConductSet)
-    >>> canonical_url(cocset)
-    u'http://launchpad.test/codeofconduct'
+    >>> print(canonical_url(cocset))
+    http://launchpad.test/codeofconduct
 
 An ISignedCodeOfConductSet
 
     >>> signedcocset = getUtility(ISignedCodeOfConductSet)
-    >>> canonical_url(signedcocset)
-    u'http://launchpad.test/codeofconduct/console'
+    >>> print(canonical_url(signedcocset))
+    http://launchpad.test/codeofconduct/console
 
 An ISignedCodeOfConduct
 
-    >>> canonical_url(signedcocset['1'])
-    u'http://launchpad.test/codeofconduct/console/1'
+    >>> print(canonical_url(signedcocset['1']))
+    http://launchpad.test/codeofconduct/console/1
 
 An ICodeOfConduct
 
-    >>> canonical_url(cocset['2.0'])
-    u'http://launchpad.test/codeofconduct/2.0'
+    >>> print(canonical_url(cocset['2.0']))
+    http://launchpad.test/codeofconduct/2.0
 
 
 Distributions, distroseriess and so on
@@ -107,24 +107,24 @@ The IDistributionSet.
 
     >>> distroset = getUtility(IDistributionSet)
 
-    >>> canonical_url(distroset)
-    u'http://launchpad.test/distros'
+    >>> print(canonical_url(distroset))
+    http://launchpad.test/distros
 
 An IDistribution.
 
-    >>> canonical_url(celebs.ubuntu)
-    u'http://launchpad.test/ubuntu'
+    >>> print(canonical_url(celebs.ubuntu))
+    http://launchpad.test/ubuntu
 
 An IDistroSeries.
 
     >>> hoary = celebs.ubuntu.getSeries('hoary')
-    >>> canonical_url(hoary)
-    u'http://launchpad.test/ubuntu/hoary'
+    >>> print(canonical_url(hoary))
+    http://launchpad.test/ubuntu/hoary
 
 An ISourcePackage.
 
-    >>> canonical_url(hoary.getSourcePackage('evolution'))
-    u'http://launchpad.test/ubuntu/hoary/+source/evolution'
+    >>> print(canonical_url(hoary.getSourcePackage('evolution')))
+    http://launchpad.test/ubuntu/hoary/+source/evolution
 
 An IDistributionSourcePackage.
 
@@ -133,8 +133,8 @@ An IDistributionSourcePackage.
     >>> sourcepackagenameset = getUtility(ISourcePackageNameSet)
     >>> ubuntu_firefox = celebs.ubuntu.getSourcePackage(
     ...     sourcepackagenameset['mozilla-firefox'])
-    >>> canonical_url(ubuntu_firefox)
-    u'http://launchpad.test/ubuntu/+source/mozilla-firefox'
+    >>> print(canonical_url(ubuntu_firefox))
+    http://launchpad.test/ubuntu/+source/mozilla-firefox
 
 
 Projects groups and products
@@ -145,37 +145,37 @@ Projects groups and products
 
 The IProjectGroupSet.
 
-    >>> canonical_url(getUtility(IProjectGroupSet))
-    u'http://launchpad.test/projectgroups'
+    >>> print(canonical_url(getUtility(IProjectGroupSet)))
+    http://launchpad.test/projectgroups
 
 An IProjectGroup.
 
-    >>> canonical_url(getUtility(IProjectGroupSet)['apache'])
-    u'http://launchpad.test/apache'
+    >>> print(canonical_url(getUtility(IProjectGroupSet)['apache']))
+    http://launchpad.test/apache
 
 The IProductSet.
 
     >>> productset = getUtility(IProductSet)
-    >>> canonical_url(productset)
-    u'http://launchpad.test/projects'
+    >>> print(canonical_url(productset))
+    http://launchpad.test/projects
 
 An IProduct.
 
     >>> evolution_product = productset['evolution']
-    >>> canonical_url(evolution_product)
-    u'http://launchpad.test/evolution'
+    >>> print(canonical_url(evolution_product))
+    http://launchpad.test/evolution
 
 An IProductSeries.
 
     >>> evolution_trunk_series = evolution_product.getSeries('trunk')
-    >>> canonical_url(evolution_trunk_series)
-    u'http://launchpad.test/evolution/trunk'
+    >>> print(canonical_url(evolution_trunk_series))
+    http://launchpad.test/evolution/trunk
 
 An IProductRelease.
 
     >>> evolution_release = evolution_trunk_series.getRelease('2.1.6')
-    >>> canonical_url(evolution_release)
-    u'http://launchpad.test/evolution/trunk/2.1.6'
+    >>> print(canonical_url(evolution_release))
+    http://launchpad.test/evolution/trunk/2.1.6
 
 
 Bugs and bugtasks
@@ -186,34 +186,34 @@ Bugs and bugtasks
 
 The IBugSet.
 
-    >>> canonical_url(getUtility(IBugSet))
-    u'http://launchpad.test/bugs/bugs'
+    >>> print(canonical_url(getUtility(IBugSet)))
+    http://launchpad.test/bugs/bugs
 
 An IBug.
 
-    >>> canonical_url(getUtility(IBugSet).get(1))
-    u'http://bugs.launchpad.test/bugs/1'
+    >>> print(canonical_url(getUtility(IBugSet).get(1)))
+    http://bugs.launchpad.test/bugs/1
 
 An IBugTask on a product.
 
-    >>> canonical_url(getUtility(IBugTaskSet).get(2))
-    u'http://bugs.launchpad.test/firefox/+bug/1'
+    >>> print(canonical_url(getUtility(IBugTaskSet).get(2)))
+    http://bugs.launchpad.test/firefox/+bug/1
 
 An IMessage on a bug.
 
-    >>> canonical_url(getUtility(IBugSet).get(1).messages[0])
-    u'http://bugs.launchpad.test/firefox/+bug/1/comments/0'
+    >>> print(canonical_url(getUtility(IBugSet).get(1).messages[0]))
+    http://bugs.launchpad.test/firefox/+bug/1/comments/0
 
 An IMessage on a question.
 
-    >>> canonical_url(getUtility(IQuestionSet).get(6).messages[0])
-    u'http://answers.launchpad.test/firefox/+question/6/messages/1'
+    >>> print(canonical_url(getUtility(IQuestionSet).get(6).messages[0]))
+    http://answers.launchpad.test/firefox/+question/6/messages/1
 
 An IBugTask on a distribution source package.
 
     >>> distro_task = getUtility(IBugTaskSet).get(4)
-    >>> canonical_url(distro_task)
-    u'http://bugs.launchpad.test/debian/+source/mozilla-firefox/+bug/1'
+    >>> print(canonical_url(distro_task))
+    http://bugs.launchpad.test/debian/+source/mozilla-firefox/+bug/1
 
 An IBugTask on a distribution without a sourcepackage.
 
@@ -223,23 +223,23 @@ An IBugTask on a distribution without a sourcepackage.
     >>> temp_target = distro_task.target
     >>> distro_task.transitionToTarget(
     ...     distro_task.target.distribution, getUtility(ILaunchBag).user)
-    >>> canonical_url(distro_task)
-    u'http://bugs.launchpad.test/debian/+bug/1'
+    >>> print(canonical_url(distro_task))
+    http://bugs.launchpad.test/debian/+bug/1
     >>> distro_task.transitionToTarget(temp_target, getUtility(ILaunchBag).user)
 
 An IBugTask on a distribution series source package.
 
     >>> distro_series_task = getUtility(IBugTaskSet).get(19)
-    >>> canonical_url(distro_series_task)
-    u'http://bugs.launchpad.test/debian/sarge/+source/mozilla-firefox/+bug/3'
+    >>> print(canonical_url(distro_series_task))
+    http://bugs.launchpad.test/debian/sarge/+source/mozilla-firefox/+bug/3
 
 An IBugTask on a distribution series without a sourcepackage.
 
     >>> temp_target = distro_series_task.target
     >>> distro_series_task.transitionToTarget(
     ...     distro_series_task.target.distroseries, getUtility(ILaunchBag).user)
-    >>> canonical_url(distro_series_task)
-    u'http://bugs.launchpad.test/debian/sarge/+bug/3'
+    >>> print(canonical_url(distro_series_task))
+    http://bugs.launchpad.test/debian/sarge/+bug/3
     >>> distro_series_task.transitionToTarget(
     ...     temp_target, getUtility(ILaunchBag).user)
 
@@ -257,13 +257,13 @@ private.)
 
     >>> login(ANONYMOUS)
 
-    >>> canonical_url(distro_series_task.bug)
-    u'http://bugs.launchpad.test/bugs/3'
+    >>> print(canonical_url(distro_series_task.bug))
+    http://bugs.launchpad.test/bugs/3
 
 A private bugtask, as an anonymous user.
 
-    >>> canonical_url(distro_series_task)
-    u'http://bugs.launchpad.test/debian/sarge/+source/mozilla-firefox/+bug/3'
+    >>> print(canonical_url(distro_series_task))
+    http://bugs.launchpad.test/debian/sarge/+source/mozilla-firefox/+bug/3
 
     >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> distro_series_task.bug.setPrivate(False, getUtility(ILaunchBag).user)
@@ -275,8 +275,8 @@ An IBugWatchSet.
     This doesn't work, because BugWatchSet.bug is an int, not an IBug object.
 
     xxx bug_one_watches = BugWatchSet(bug=1)
-    xxx canonical_url(bug_one_watches)
-    u'http://launchpad.test/bugs/1/watches'
+    xxx print(canonical_url(bug_one_watches))
+    http://launchpad.test/bugs/1/watches
 
 An IBugComment.
 
@@ -285,15 +285,15 @@ An IBugComment.
     >>> bugtask_one = bug_one.bugtasks[0]
     >>> bug_comment = BugComment(
     ...     1, bug_one.initial_message, bugtask_one, True)
-    >>> canonical_url(bug_comment)
-    u'http://bugs.launchpad.test/firefox/+bug/1/comments/1'
+    >>> print(canonical_url(bug_comment))
+    http://bugs.launchpad.test/firefox/+bug/1/comments/1
 
 An IBugNomination.
 
     >>> from lp.bugs.interfaces.bugnomination import IBugNominationSet
     >>> bug_nomination = getUtility(IBugNominationSet).get(1)
-    >>> canonical_url(bug_nomination)
-    u'http://bugs.launchpad.test/bugs/1/nominations/1'
+    >>> print(canonical_url(bug_nomination))
+    http://bugs.launchpad.test/bugs/1/nominations/1
 
 
 Remote Bug Trackers and Remote Bugs
@@ -304,21 +304,21 @@ Remote Bug Trackers and Remote Bugs
 
 An IBugTrackerSet.
 
-    >>> canonical_url(getUtility(IBugTrackerSet))
-    u'http://bugs.launchpad.test/bugs/bugtrackers'
+    >>> print(canonical_url(getUtility(IBugTrackerSet)))
+    http://bugs.launchpad.test/bugs/bugtrackers
 
 A remote bug tracker.
 
     >>> mozilla_bugtracker = getUtility(IBugTrackerSet)['mozilla.org']
-    >>> canonical_url(mozilla_bugtracker)
-    u'http://bugs.launchpad.test/bugs/bugtrackers/mozilla.org'
+    >>> print(canonical_url(mozilla_bugtracker))
+    http://bugs.launchpad.test/bugs/bugtrackers/mozilla.org
 
 A bug from a remote bug tracker.
 
     >>> remote_bug = RemoteBug(mozilla_bugtracker, '42',
     ...                        mozilla_bugtracker.getBugsWatching('42'))
-    >>> canonical_url(remote_bug)
-    u'http://bugs.launchpad.test/bugs/bugtrackers/mozilla.org/42'
+    >>> print(canonical_url(remote_bug))
+    http://bugs.launchpad.test/bugs/bugtrackers/mozilla.org/42
 
 
 Branches
@@ -330,16 +330,16 @@ An IBranch.
 
     >>> branch = getUtility(IBranchLookup).get(10)
 
-    >>> canonical_url(branch)
-    u'http://code.launchpad.test/~mark/firefox/release-0.9.2'
+    >>> print(canonical_url(branch))
+    http://code.launchpad.test/~mark/firefox/release-0.9.2
 
 An IBugBranch.
 
     >>> bug = getUtility(IBugSet).get(1)
     >>> bug.linkBranch(branch, getUtility(IPersonSet).getByName('mark'))
     >>> [bug_branch] = bug.linked_bugbranches
-    >>> canonical_url(bug_branch)
-    u'http://launchpad.test/~mark/firefox/release-0.9.2/+bug/1'
+    >>> print(canonical_url(bug_branch))
+    http://launchpad.test/~mark/firefox/release-0.9.2/+bug/1
 
 
 BranchMergeProposals
@@ -385,8 +385,9 @@ Specifications
 
     >>> from lp.blueprints.interfaces.specification import ISpecificationSet
     >>> spec_set = getUtility(ISpecificationSet)
-    >>> canonical_url(spec_set)
-    u'http://blueprints.launchpad.test/'
+    >>> print(canonical_url(spec_set))
+    http://blueprints.launchpad.test/
 
-    >>> canonical_url(celebs.ubuntu.getSpecification('media-integrity-check'))
-    u'http://blueprints.launchpad.test/ubuntu/+spec/media-integrity-check'
+    >>> print(canonical_url(
+    ...     celebs.ubuntu.getSpecification('media-integrity-check')))
+    http://blueprints.launchpad.test/ubuntu/+spec/media-integrity-check
diff --git a/lib/lp/services/webapp/doc/menus.txt b/lib/lp/services/webapp/doc/menus.txt
index 0803475..ada19bd 100644
--- a/lib/lp/services/webapp/doc/menus.txt
+++ b/lib/lp/services/webapp/doc/menus.txt
@@ -45,11 +45,11 @@ Let's make three objects that together make a url hierarchy:
     >>> root = ExampleContentObject('', None)
     >>> street = ExampleContentObject('sesamestreet', root)
     >>> house = ExampleContentObject('number73', street)
-    >>> canonical_url(house)
-    u'http://launchpad.test/sesamestreet/number73'
+    >>> print(canonical_url(house))
+    http://launchpad.test/sesamestreet/number73
 
-    >>> canonical_url(street)
-    u'http://launchpad.test/sesamestreet'
+    >>> print(canonical_url(street))
+    http://launchpad.test/sesamestreet
 
 
 == The Link class ==
@@ -303,20 +303,20 @@ Checking out the escapedtext attribute.
 
     >>> link = Link('+target', 'text -->')
 
-    >>> ILink(link).escapedtext
-    u'text --&gt;'
+    >>> print(ILink(link).escapedtext)
+    text --&gt;
 
-    >>> IFacetLink(link).escapedtext
-    u'text --&gt;'
+    >>> print(IFacetLink(link).escapedtext)
+    text --&gt;
 
     >>> text = structured('some <b> %s </b> text', '-->')
     >>> link = Link('+target', text)
 
-    >>> ILink(link).escapedtext
-    u'some <b> --&gt; </b> text'
+    >>> print(ILink(link).escapedtext)
+    some <b> --&gt; </b> text
 
-    >>> IFacetLink(link).escapedtext
-    u'some <b> --&gt; </b> text'
+    >>> print(IFacetLink(link).escapedtext)
+    some <b> --&gt; </b> text
 
 Next, we return the link as HTML.
 
diff --git a/lib/lp/services/webapp/doc/navigation.txt b/lib/lp/services/webapp/doc/navigation.txt
index 9abe14f..cd96dcf 100644
--- a/lib/lp/services/webapp/doc/navigation.txt
+++ b/lib/lp/services/webapp/doc/navigation.txt
@@ -455,37 +455,37 @@ Let's make another navigation class to test redirection.
     >>> navigation4 = ThingSetNavigation4(thingset, request)
     >>> navigation4.publishTraverse(request, 'tree')
     <...RedirectionView...>
-    >>> navigation4.publishTraverse(request, 'tree')()
-    u''
+    >>> print(navigation4.publishTraverse(request, 'tree')())
+    <BLANKLINE>
     >>> request.response.redirected_to
     'trees'
     >>> print request.response.status
     301
 
-    >>> navigation4.publishTraverse(request, 'toad')()
-    u''
+    >>> print(navigation4.publishTraverse(request, 'toad')())
+    <BLANKLINE>
     >>> request.response.redirected_to
     'toads'
     >>> print request.response.status
     None
 
-    >>> navigation4.publishTraverse(request, 'something')()
-    u''
+    >>> print(navigation4.publishTraverse(request, 'something')())
+    <BLANKLINE>
     >>> request.response.redirected_to
     '/another/place'
     >>> print request.response.status
     301
 
     >>> request.traversal_stack = ['tundra']
-    >>> navigation4.publishTraverse(request, 'outerspace')()
-    u''
+    >>> print(navigation4.publishTraverse(request, 'outerspace')())
+    <BLANKLINE>
     >>> request.response.redirected_to
     '/siberia/tundra'
     >>> print request.response.status
     None
 
-    >>> navigation4.publishTraverse(request, 'here')()
-    u''
+    >>> print(navigation4.publishTraverse(request, 'here')())
+    <BLANKLINE>
     >>> request.response.redirected_to
     '/there'
     >>> print request.response.status
@@ -509,8 +509,8 @@ with the remainder of the URL and or query string.
     >>> navigation5 = ThingSetNavigation5(thingset, request)
     >>> navigation5.publishTraverse(request, 'jobs')
     <...RedirectionView...>
-    >>> navigation5.publishTraverse(request, 'jobs')()
-    u''
+    >>> print(navigation5.publishTraverse(request, 'jobs')())
+    <BLANKLINE>
     >>> request.response.redirected_to
     'http://ubuntu.com/jobs'
     >>> print request.response.status
@@ -518,8 +518,8 @@ with the remainder of the URL and or query string.
 
     >>> request.traversal_stack = ['LaunchpadMeeting']
     >>> request.query_string = 'hilight=Time'
-    >>> navigation5.publishTraverse(request, '+foo')()
-    u''
+    >>> print(navigation5.publishTraverse(request, '+foo')())
+    <BLANKLINE>
     >>> request.response.redirected_to
     'http://wiki.canonical.com/LaunchpadMeeting?hilight=Time'
     >>> print request.response.status
@@ -575,8 +575,8 @@ Check out the traversals defined directly.
     >>> ubernav.publishTraverse(request, 'diplodocus')
     'diplodocus called frank'
 
-    >>> ubernav.publishTraverse(request, 'topology')()
-    u''
+    >>> print(ubernav.publishTraverse(request, 'topology')())
+    <BLANKLINE>
     >>> request.response.redirected_to
     'topologies'
 
@@ -600,18 +600,18 @@ Check those from ThingSetNavigation3:
 
 Check those from ThingSetNavigation4:
 
-    >>> ubernav.publishTraverse(request, 'tree')()
-    u''
+    >>> print(ubernav.publishTraverse(request, 'tree')())
+    <BLANKLINE>
     >>> request.response.redirected_to
     'trees'
 
-    >>> ubernav.publishTraverse(request, 'toad')()
-    u''
+    >>> print(ubernav.publishTraverse(request, 'toad')())
+    <BLANKLINE>
     >>> request.response.redirected_to
     'toads'
 
-    >>> ubernav.publishTraverse(request, 'ttt')()
-    u''
+    >>> print(ubernav.publishTraverse(request, 'ttt')())
+    <BLANKLINE>
     >>> request.response.redirected_to
     '/another/place'
 
diff --git a/lib/lp/services/webapp/doc/notification-text-escape.txt b/lib/lp/services/webapp/doc/notification-text-escape.txt
index 01cab67..3275f9b 100644
--- a/lib/lp/services/webapp/doc/notification-text-escape.txt
+++ b/lib/lp/services/webapp/doc/notification-text-escape.txt
@@ -33,8 +33,8 @@ But text containing markup is CGI-escaped:
     >>> response = new_response()
     >>> response.addNotification(u'<br/>dirty')
     >>> for notification in response.notifications:
-    ...     notification.message
-    u'&lt;br/&gt;dirty'
+    ...     print(notification.message)
+    &lt;br/&gt;dirty
 
 
 If the object passed to addNotification() publishes the
@@ -47,14 +47,14 @@ appropriate sections escaped and unescaped.
     >>> structured_text = structured(msg, escaped=u'<br/>foo')
     >>> IStructuredString.providedBy(structured_text)
     True
-    >>> structured_text.escapedtext
-    u'<b>&lt;br/&gt;foo</b>'
+    >>> print(structured_text.escapedtext)
+    <b>&lt;br/&gt;foo</b>
 
     >>> response = new_response()
     >>> response.addNotification(structured_text)
     >>> for notification in response.notifications:
-    ...     notification.message
-    u'<b>&lt;br/&gt;foo</b>'
+    ...     print(notification.message)
+    <b>&lt;br/&gt;foo</b>
 
 Passing an object to addNotification() that is an instance of
 zope.i18n.Message will be escaped in the same
@@ -65,8 +65,8 @@ manner as raw text.
     >>> response = new_response()
     >>> response.addNotification(msgtxt)
     >>> for notification in response.notifications:
-    ...     notification.message
-    u'&lt;br/&gt;foo'
+    ...     print(notification.message)
+    &lt;br/&gt;foo
 
 To pass internationalized text that contains markup, one may call
 structured() directly with an internationalized object.  structured()
@@ -77,11 +77,11 @@ may then be passed to addNotification().
     >>> msgid   = _(u'<good/>%(evil)s')
     >>> escapee = '<evil/>'
     >>> text    = structured(msgid, evil=escapee)
-    >>> text.escapedtext
-    u'<good/>&lt;evil/&gt;'
+    >>> print(text.escapedtext)
+    <good/>&lt;evil/&gt;
 
     >>> response = new_response()
     >>> response.addNotification(text)
     >>> for notification in response.notifications:
-    ...     notification.message
-    u'<good/>&lt;evil/&gt;'
+    ...     print(notification.message)
+    <good/>&lt;evil/&gt;
diff --git a/lib/lp/services/webapp/doc/renamed-view.txt b/lib/lp/services/webapp/doc/renamed-view.txt
index fafb350..2a8fd70 100644
--- a/lib/lp/services/webapp/doc/renamed-view.txt
+++ b/lib/lp/services/webapp/doc/renamed-view.txt
@@ -29,8 +29,8 @@ the new name. The redirection status is 301 (Moved permently) which
 will make search engines discards the old URLs and some browser to
 update bookmarks.
 
-    >>> view()
-    u''
+    >>> print(view())
+    <BLANKLINE>
     >>> request.response.getStatus()
     301
     >>> print request.response.getHeader('Location')
@@ -42,8 +42,8 @@ new_name can be a relative path.
     >>> from lp.services.webapp.interfaces import ILaunchpadRoot
     >>> root = getUtility(ILaunchpadRoot)
     >>> view = RenamedView(root, LaunchpadTestRequest(), '+tour/index.html')
-    >>> view()
-    u''
+    >>> print(view())
+    <BLANKLINE>
     >>> request.response.getStatus()
     301
     >>> print view.request.response.getHeader('Location')
@@ -58,8 +58,8 @@ to the redirected URL.
     >>> request = LaunchpadTestRequest(
     ...     QUERY_STRING='field.status=Open')
     >>> view = RenamedView(ubuntu, request, '+questions')
-    >>> view()
-    u''
+    >>> print(view())
+    <BLANKLINE>
     >>> print request.response.getHeader('Location')
     http://launchpad.test/ubuntu/+questions?field.status=Open
 
@@ -72,8 +72,8 @@ change the virtual host used for the redirection.
     >>> request = LaunchpadTestRequest()
     >>> view = RenamedView(
     ...     ubuntu, request, '+questions', rootsite='answers')
-    >>> view()
-    u''
+    >>> print(view())
+    <BLANKLINE>
     >>> print request.response.getHeader('Location')
     http://answers.launchpad.test/ubuntu/+questions
 
@@ -115,7 +115,7 @@ browser:renamed-page is available for this purpose.
     >>> from zope.component import getMultiAdapter
     >>> request = LaunchpadTestRequest()
     >>> view = getMultiAdapter((ubuntu, request), name='+old_tickets_page')
-    >>> view()
-    u''
+    >>> print(view())
+    <BLANKLINE>
     >>> print request.response.getHeader('Location')
     http://answers.launchpad.test/ubuntu/+questions
diff --git a/lib/lp/services/webapp/doc/test_adapter.txt b/lib/lp/services/webapp/doc/test_adapter.txt
index 2cb559d..d91cfb2 100644
--- a/lib/lp/services/webapp/doc/test_adapter.txt
+++ b/lib/lp/services/webapp/doc/test_adapter.txt
@@ -351,8 +351,8 @@ The statement about to be executed is not recorded in the statement log.
 The request time limit was exceeded before the statement was issued to
 the database.
 
-    >>> get_request_statements()
-    [(0, ..., u'SQL-main-master', u'SELECT 2', ...)]
+    >>> print(pretty(get_request_statements()))
+    [(0, ..., 'SQL-main-master', 'SELECT 2', ...)]
 
 
 When a RequestExpired exception is raised, the current
diff --git a/lib/lp/services/webapp/doc/test_adapter_permissions.txt b/lib/lp/services/webapp/doc/test_adapter_permissions.txt
index aaf56ec..17423b5 100644
--- a/lib/lp/services/webapp/doc/test_adapter_permissions.txt
+++ b/lib/lp/services/webapp/doc/test_adapter_permissions.txt
@@ -43,6 +43,6 @@ Store's replication set.
     >>> main_master.find(Person, name='janitor').one().display_name = 'BenD'
     >>> transaction.commit()
     >>> t = transaction.begin()
-    >>> main_master.find(Person, name='janitor').one().display_name
-    u'BenD'
+    >>> print(main_master.find(Person, name='janitor').one().display_name)
+    BenD
     >>> transaction.abort()
diff --git a/lib/lp/services/webapp/doc/webapp-publication.txt b/lib/lp/services/webapp/doc/webapp-publication.txt
index 7329dbb..fa178cd 100644
--- a/lib/lp/services/webapp/doc/webapp-publication.txt
+++ b/lib/lp/services/webapp/doc/webapp-publication.txt
@@ -526,8 +526,8 @@ The advantage of the IBrowserFormNG API is that it offers methods that
 checks the number of values you are expecting. The getOne() method
 should be used when you expect only one value for the field.
 
-    >>> request.form_ng.getOne('a_field')
-    u'a_value'
+    >>> print(request.form_ng.getOne('a_field'))
+    a_value
 
 UnexpectedFormData is raised if more than one value was submitted for
 the field:
@@ -546,20 +546,24 @@ None is returned if the field wasn't submitted:
 You can provide a default value that is returned if the field wasn't
 submitted:
 
-    >>> request.form_ng.getOne('another_field', u'default')
-    u'default'
+    >>> print(request.form_ng.getOne('another_field', u'default'))
+    default
 
 The getAll() method should be used when you are expecting a list of
 values.
 
-    >>> request.form_ng.getAll('items_field')
-    [u'1', u'2', u'3']
+    >>> for item in request.form_ng.getAll('items_field'):
+    ...     print(item)
+    1
+    2
+    3
 
 If only one value was submitted, it will still be returned as part of
 a list:
 
-    >>> request.form_ng.getAll('a_field')
-    [u'a_value']
+    >>> for item in request.form_ng.getAll('a_field'):
+    ...     print(item)
+    a_value
 
 An empty list is returned when no value was submitted for the field:
 
@@ -569,8 +573,9 @@ An empty list is returned when no value was submitted for the field:
 That method also accepts a default value that is to be returned when
 no value was submitted with the field.
 
-    >>> request.form_ng.getAll('another_field', [u'default'])
-    [u'default']
+    >>> for item in request.form_ng.getAll('another_field', [u'default']):
+    ...     print(item)
+    default
 
 All the submitted field names can be iterated over:
 
@@ -666,8 +671,9 @@ current thread.
     False
     >>> 'launchpad.publicationthreadduration' in request._orig_env
     False
-    >>> publication.callObject(request, TestView(TestContext(), request))
-    u'Result'
+    >>> print(publication.callObject(
+    ...     request, TestView(TestContext(), request)))
+    Result
     >>> publication.afterCall(request, None)
     >>> 'launchpad.publicationduration' in request._orig_env
     True
@@ -732,8 +738,9 @@ completed), we'll have the duration for the traversal and the duration for
 the publication, up to the point where it was forcefully stopped.
 
     >>> publication.afterTraversal(request, None)
-    >>> publication.callObject(request, TestView(TestContext(), request))
-    u'Result'
+    >>> print(publication.callObject(
+    ...     request, TestView(TestContext(), request)))
+    Result
     >>> set_request_started()
     >>> publication.handleException(
     ...     None, request, exc_info, retry_allowed=False)
@@ -1149,8 +1156,8 @@ token.
     Guilherme Salgado
     >>> principal.access_level
     <DBItem AccessLevel.WRITE_PUBLIC...
-    >>> principal.scope_url
-    u'/firefox'
+    >>> print(principal.scope_url)
+    /firefox
 
 If the token is expired or doesn't exist, an Unauthorized exception is
 raised, though.
diff --git a/lib/lp/services/webapp/doc/zcmldirectives.txt b/lib/lp/services/webapp/doc/zcmldirectives.txt
index 0652b1a..71b14e4 100644
--- a/lib/lp/services/webapp/doc/zcmldirectives.txt
+++ b/lib/lp/services/webapp/doc/zcmldirectives.txt
@@ -302,8 +302,8 @@ an IPermission.
     >>> permission = getUtility(ILaunchpadPermission, 'foo.bar')
     >>> verifyObject(ILaunchpadPermission, permission)
     True
-    >>> permission.access_level
-    u'read'
+    >>> print(permission.access_level)
+    read
 
 
 == Cleaning up the interfaces and objects to test with ==
diff --git a/lib/lp/services/webapp/notifications.py b/lib/lp/services/webapp/notifications.py
index a0fb1c2..151ab02 100644
--- a/lib/lp/services/webapp/notifications.py
+++ b/lib/lp/services/webapp/notifications.py
@@ -60,8 +60,9 @@ class NotificationRequest:
     >>> notifications = NotificationList()
     >>> session['notifications'] = notifications
     >>> notifications.append(Notification(0, 'Fnord'))
-    >>> [notification.message for notification in request.notifications]
-    ['Fnord']
+    >>> for notification in request.notifications:
+    ...     print(notification.message)
+    Fnord
 
     Note that NotificationRequest.notifications also returns any notifications
     that have been added so far in this request, making it the single source
@@ -69,8 +70,10 @@ class NotificationRequest:
 
     >>> response = INotificationResponse(request)
     >>> response.addNotification('Aargh')
-    >>> [notification.message for notification in request.notifications]
-    ['Fnord', u'Aargh']
+    >>> for notification in request.notifications:
+    ...     print(notification.message)
+    Fnord
+    Aargh
     """
 
     @property
@@ -263,17 +266,17 @@ class NotificationList(list):
     >>> notifications.append(Notification(error, u'An error'))
     >>> notifications.append(Notification(debug, u'A debug message'))
     >>> for notification in notifications:
-    ...     print(repr(notification.message))
-    u'An error'
-    u'A debug message'
+    ...     print(notification.message)
+    An error
+    A debug message
 
     The __getitem__ method is also overloaded to allow TALES expressions
     to easily retrieve lists of notifications that match a particular
     notification level.
 
     >>> for notification in notifications['debug']:
-    ...     print(repr(notification.message))
-    u'A debug message'
+    ...     print(notification.message)
+    A debug message
     """
 
     created = None
diff --git a/lib/lp/services/webapp/servers.py b/lib/lp/services/webapp/servers.py
index 0e2bf17..49ac4dd 100644
--- a/lib/lp/services/webapp/servers.py
+++ b/lib/lp/services/webapp/servers.py
@@ -1018,8 +1018,8 @@ class LaunchpadTestResponse(LaunchpadBrowserResponse):
     True
 
     >>> response.addWarningNotification('Warning Notification')
-    >>> request.notifications[0].message
-    u'Warning Notification'
+    >>> print(request.notifications[0].message)
+    Warning Notification
     """
 
     uuid = 'LaunchpadTestResponse'
diff --git a/lib/lp/services/webapp/tests/test_doc.py b/lib/lp/services/webapp/tests/test_doc.py
index fab33d6..7af6c91 100644
--- a/lib/lp/services/webapp/tests/test_doc.py
+++ b/lib/lp/services/webapp/tests/test_doc.py
@@ -17,6 +17,7 @@ from lp.testing.layers import (
     )
 from lp.testing.systemdocs import (
     LayeredDocFileSuite,
+    setGlobs,
     setUp,
     tearDown,
     )
@@ -37,7 +38,7 @@ special = {
         stdout_logging=False, layer=None),
     'test_adapter.txt': LayeredDocFileSuite(
         '../doc/test_adapter.txt',
-        layer=LaunchpadFunctionalLayer),
+        setUp=setGlobs, layer=LaunchpadFunctionalLayer),
 # XXX Julian 2009-05-13, bug=376171
 # Temporarily disabled because of intermittent failures.
 #    'test_adapter_timeout.txt': LayeredDocFileSuite(
diff --git a/lib/lp/services/webservice/doc/webservice-marshallers.txt b/lib/lp/services/webservice/doc/webservice-marshallers.txt
index 17cfa94..2b4504d 100644
--- a/lib/lp/services/webservice/doc/webservice-marshallers.txt
+++ b/lib/lp/services/webservice/doc/webservice-marshallers.txt
@@ -52,13 +52,13 @@ returns that item. Otherwise it raises a ValueError.
     ...     "http://api.launchpad.test/beta/~salgado";)
     >>> IPerson.providedBy(person)
     True
-    >>> person.name
-    u'salgado'
+    >>> print(person.name)
+    salgado
 
     >>> ubuntu_team = marshaller.marshall_from_json_data(
     ...     "http://api.launchpad.test/beta/~ubuntu-team";)
-    >>> ubuntu_team.name
-    u'ubuntu-team'
+    >>> print(ubuntu_team.name)
+    ubuntu-team
 
     >>> marshaller.marshall_from_request(
     ...     "http://api.launchpad.test/beta/~nosuchperson";)
@@ -77,8 +77,8 @@ the URL to that object.
 
     >>> person_resource = EntryResource(person, request)
 
-    >>> marshaller.unmarshall(person_resource, person)
-    u'http://.../~salgado'
+    >>> print(marshaller.unmarshall(person_resource, person))
+    http://.../~salgado
 
 This marshaller also appends '_link' to the representation name of
 this field, so that clients can know this is a link to another
diff --git a/lib/lp/services/webservice/stories/datamodel.txt b/lib/lp/services/webservice/stories/datamodel.txt
index f47d6c3..5ca2388 100644
--- a/lib/lp/services/webservice/stories/datamodel.txt
+++ b/lib/lp/services/webservice/stories/datamodel.txt
@@ -19,8 +19,12 @@ Normally, the total size of a collection is not served along with the
 collection; it's available by following the total_size_link.
 
   >>> collection = get_collection()
-  >>> print(sorted(collection.keys()))
-  [u'entries', u'next_collection_link', u'start', u'total_size_link']
+  >>> for key in sorted(collection.keys()):
+  ...     print(key)
+  entries
+  next_collection_link
+  start
+  total_size_link
   >>> print(webservice.get(collection['total_size_link']).jsonBody())
   9
 
@@ -29,8 +33,11 @@ collection obvious), 'total_size' is served instead of
 'total_size_link'.
 
   >>> collection = get_collection(size=100)
-  >>> print(sorted(collection.keys()))
-  [u'entries', u'start', u'total_size']
+  >>> for key in sorted(collection.keys()):
+  ...     print(key)
+  entries
+  start
+  total_size
   >>> print(collection['total_size'])
   9
 
@@ -39,7 +46,11 @@ of the collection semi-obvious), 'total_size' is served instead of
 'total_size_link'.
 
   >>> collection = get_collection(start=8)
-  >>> print(sorted(collection.keys()))
-  [u'entries', u'prev_collection_link', u'start', u'total_size']
+  >>> for key in sorted(collection.keys()):
+  ...     print(key)
+  entries
+  prev_collection_link
+  start
+  total_size
   >>> print(collection['total_size'])
   9
diff --git a/lib/lp/services/webservice/stories/multiversion.txt b/lib/lp/services/webservice/stories/multiversion.txt
index bc4f855..22f9722 100644
--- a/lib/lp/services/webservice/stories/multiversion.txt
+++ b/lib/lp/services/webservice/stories/multiversion.txt
@@ -17,8 +17,12 @@ total size of the collection.
   ...     return collection.jsonBody()
 
   >>> collection = get_collection("devel")
-  >>> print(sorted(collection.keys()))
-  [u'entries', u'next_collection_link', u'start', u'total_size_link']
+  >>> for key in sorted(collection.keys()):
+  ...     print(key)
+  entries
+  next_collection_link
+  start
+  total_size_link
   >>> print(webservice.get(collection['total_size_link']).jsonBody())
   9
 
@@ -26,8 +30,12 @@ In previous versions, the same named operations will return a
 'total_size' containing the actual size of the collection.
 
   >>> collection = get_collection("1.0")
-  >>> print(sorted(collection.keys()))
-  [u'entries', u'next_collection_link', u'start', u'total_size']
+  >>> for key in sorted(collection.keys()):
+  ...     print(key)
+  entries
+  next_collection_link
+  start
+  total_size
   >>> print(collection['total_size'])
   9
 
diff --git a/lib/lp/services/webservice/stories/xx-hostedfile.txt b/lib/lp/services/webservice/stories/xx-hostedfile.txt
index bda79d6..a318435 100644
--- a/lib/lp/services/webservice/stories/xx-hostedfile.txt
+++ b/lib/lp/services/webservice/stories/xx-hostedfile.txt
@@ -12,12 +12,12 @@ The librarian manages hosted files
 Firefox starts out with a link to branding images, but no actual images.
 
     >>> project = webservice.get('/firefox').jsonBody()
-    >>> project['icon_link']
-    u'http://.../firefox/icon'
-    >>> project['logo_link']
-    u'http://.../firefox/logo'
-    >>> project['brand_link']
-    u'http://.../firefox/brand'
+    >>> print(project['icon_link'])
+    http://.../firefox/icon
+    >>> print(project['logo_link'])
+    http://.../firefox/logo
+    >>> print(project['brand_link'])
+    http://.../firefox/brand
 
     >>> print(webservice.get(project['icon_link']))
     HTTP/1.1 404 Not Found
diff --git a/lib/lp/services/webservice/stories/xx-wadl.txt b/lib/lp/services/webservice/stories/xx-wadl.txt
index f7180ef..70e1dcf 100644
--- a/lib/lp/services/webservice/stories/xx-wadl.txt
+++ b/lib/lp/services/webservice/stories/xx-wadl.txt
@@ -39,8 +39,8 @@ from disk.
 
 The fake WADL is now present in the cache.
 
-    >>> WebServiceApplication.cached_wadl
-    {u'devel': u'Some fake WADL.'}
+    >>> print(pretty(WebServiceApplication.cached_wadl))
+    {'devel': 'Some fake WADL.'}
 
 Change the cached value, and we change the document served.
 
@@ -91,8 +91,10 @@ we'll get a document keyed to the '1.0' version.
 
 All of these documents were cached as they were generated:
 
-    >>> sorted(WebServiceApplication.cached_wadl.keys())
-    [u'1.0', u'devel']
+    >>> for key in sorted(WebServiceApplication.cached_wadl.keys()):
+    ...     print(key)
+    1.0
+    devel
 
 Finally, restore the cache so that other tests will have a clean
 slate.
diff --git a/lib/lp/services/worlddata/doc/language.txt b/lib/lp/services/worlddata/doc/language.txt
index 62feb61..b5e5c0c 100644
--- a/lib/lp/services/worlddata/doc/language.txt
+++ b/lib/lp/services/worlddata/doc/language.txt
@@ -11,8 +11,8 @@ getLanguageByCode
 We can get hold of languages by their language code.
 
   >>> language = language_set.getLanguageByCode('es')
-  >>> language.englishname
-  u'Spanish'
+  >>> print(language.englishname)
+  Spanish
 
 Or if it doesn't exist, we return None.
 
@@ -273,11 +273,28 @@ Property holding a list of countries a language is spoken in, and allowing
 reading and setting them.
 
   >>> es = language_set.getLanguageByCode('es')
-  >>> print([country.name for country in es.countries])
-  [u'Argentina', u'Bolivia', u'Chile', u'Colombia', u'Costa Rica',
-   u'Dominican Republic', u'Ecuador', u'El Salvador', u'Guatemala',
-   u'Honduras', u'Mexico', u'Nicaragua', u'Panama', u'Paraguay', u'Peru',
-   u'Puerto Rico', u'Spain', u'United States', u'Uruguay', u'Venezuela']
+  >>> for country in es.countries:
+  ...     print(country.name)
+  Argentina
+  Bolivia
+  Chile
+  Colombia
+  Costa Rica
+  Dominican Republic
+  Ecuador
+  El Salvador
+  Guatemala
+  Honduras
+  Mexico
+  Nicaragua
+  Panama
+  Paraguay
+  Peru
+  Puerto Rico
+  Spain
+  United States
+  Uruguay
+  Venezuela
 
 We can add countries using `ILanguage.addCountry` method.
 
@@ -285,26 +302,63 @@ We can add countries using `ILanguage.addCountry` method.
   >>> country_set = getUtility(ICountrySet)
   >>> germany = country_set['DE']
   >>> es.addCountry(germany)
-  >>> print([country.name for country in es.countries])
-  [u'Argentina', u'Bolivia', u'Chile', u'Colombia', u'Costa Rica',
-   u'Dominican Republic', u'Ecuador', u'El Salvador', u'Germany', u'Guatemala',
-   u'Honduras', u'Mexico', u'Nicaragua', u'Panama', u'Paraguay', u'Peru',
-   u'Puerto Rico', u'Spain', u'United States', u'Uruguay', u'Venezuela']
+  >>> for country in es.countries:
+  ...     print(country.name)
+  Argentina
+  Bolivia
+  Chile
+  Colombia
+  Costa Rica
+  Dominican Republic
+  Ecuador
+  El Salvador
+  Germany
+  Guatemala
+  Honduras
+  Mexico
+  Nicaragua
+  Panama
+  Paraguay
+  Peru
+  Puerto Rico
+  Spain
+  United States
+  Uruguay
+  Venezuela
 
 Or, we can remove countries using `ILanguage.removeCountry` method.
 
   >>> argentina = country_set['AR']
   >>> es.removeCountry(argentina)
-  >>> print([country.name for country in es.countries])
-  [u'Bolivia', u'Chile', u'Colombia', u'Costa Rica', u'Dominican Republic',
-   u'Ecuador', u'El Salvador', u'Germany', u'Guatemala', u'Honduras',
-   u'Mexico', u'Nicaragua', u'Panama', u'Paraguay', u'Peru', u'Puerto Rico',
-   u'Spain', u'United States', u'Uruguay', u'Venezuela']
+  >>> for country in es.countries:
+  ...     print(country.name)
+  Bolivia
+  Chile
+  Colombia
+  Costa Rica
+  Dominican Republic
+  Ecuador
+  El Salvador
+  Germany
+  Guatemala
+  Honduras
+  Mexico
+  Nicaragua
+  Panama
+  Paraguay
+  Peru
+  Puerto Rico
+  Spain
+  United States
+  Uruguay
+  Venezuela
 
 We can also assign a complete set of languages directly to `countries`,
 but we need to log in as a translations administrator first.
 
   >>> login('carlos@xxxxxxxxxxxxx')
   >>> es.countries = set([argentina, germany])
-  >>> print([country.name for country in es.countries])
-  [u'Argentina', u'Germany']
+  >>> for country in es.countries:
+  ...     print(country.name)
+  Argentina
+  Germany
diff --git a/lib/lp/services/worlddata/doc/vocabularies.txt b/lib/lp/services/worlddata/doc/vocabularies.txt
index ab100ea..6b91d7a 100644
--- a/lib/lp/services/worlddata/doc/vocabularies.txt
+++ b/lib/lp/services/worlddata/doc/vocabularies.txt
@@ -131,8 +131,10 @@ excluded from the vocabulary.
     >>> len(hidden_languages)
     89
 
-    >>> [lang.displayname for lang in difference if lang.visible]
-    [u'English (en)']
+    >>> for lang in difference:
+    ...     if lang.visible:
+    ...         print(lang.displayname)
+    English (en)
 
 The vocabulary will raise a LookupError if asked to return English.
 
diff --git a/lib/lp/services/worlddata/stories/webservice/xx-language.txt b/lib/lp/services/worlddata/stories/webservice/xx-language.txt
index 56484ae..baa37c9 100644
--- a/lib/lp/services/worlddata/stories/webservice/xx-language.txt
+++ b/lib/lp/services/worlddata/stories/webservice/xx-language.txt
@@ -8,8 +8,8 @@ The language information from Launchpad can be queried using
 '/+languages/CC', where CC is the language code.
 
     >>> es = anon_webservice.get('/+languages/es').jsonBody()
-    >>> es['resource_type_link']
-    u'http.../#language'
+    >>> print(es['resource_type_link'])
+    http.../#language
     >>> print(es['text_direction'])
     Left to Right
     >>> print(es['code'])
@@ -42,8 +42,8 @@ at '/+languages'.
     ...             list += language['english_name'] + '(hidden)' + "\n"
     ...     return list
     >>> default_languages = anon_webservice.get('/+languages').jsonBody()
-    >>> default_languages['resource_type_link']
-        u'http.../#languages'
+    >>> print(default_languages['resource_type_link'])
+    http.../#languages
     >>> languages = get_languages_entries(default_languages)
     >>> print(languages)
     Abkhazian