← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:py3-services-webapp-doctests-future-imports into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:py3-services-webapp-doctests-future-imports into launchpad:master.

Commit message:
Convert lp.services.webapp doctests to __future__ imports

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/398506
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:py3-services-webapp-doctests-future-imports into launchpad:master.
diff --git a/lib/lp/services/webapp/doc/canonical_url.txt b/lib/lp/services/webapp/doc/canonical_url.txt
index 2730fa2..b08a560 100644
--- a/lib/lp/services/webapp/doc/canonical_url.txt
+++ b/lib/lp/services/webapp/doc/canonical_url.txt
@@ -16,7 +16,8 @@ will put in a temporary module.
     >>> from lp.services.webapp.interfaces import ICanonicalUrlData
     >>> from zope.interface import Interface, Attribute, implementer
 
-    >>> module = types.ModuleType(factory.getUniqueString().replace('-', '_'))
+    >>> module = types.ModuleType(
+    ...     six.ensure_str(factory.getUniqueString().replace('-', '_')))
     >>> sys.modules[module.__name__] = module
 
     >>> class ICountrySet(Interface):
@@ -101,7 +102,7 @@ Configure a browser:url for ITown.  Our first attempt fails because we mistyped
     >>> from zope.configuration import xmlconfig
     >>> zcmlcontext = xmlconfig.string("""
     ... <configure xmlns:browser="http://namespaces.zope.org/browser";>
-    ...   <include package="zope.component" file="meta.zcml" />
+    ...   <include package="zope.component" file="meta.zcml" />
     ...   <include package="lp.services.webapp" file="meta.zcml" />
     ...   <browser:url
     ...       for="{module_name}.ITown"
@@ -171,8 +172,8 @@ Now, there is an ICanonicalUrlData registered for ICountry.
     >>> from lp.testing import verifyObject
     >>> verifyObject(ICanonicalUrlData, country_urldata)
     True
-    >>> country_urldata.path
-    'England'
+    >>> print(country_urldata.path)
+    England
     >>> country_urldata.inside is countryset_instance
     True
 
@@ -222,8 +223,8 @@ Now, there is an ICanonicalUrlData registered for ICountrySet.
     >>> from lp.testing import verifyObject
     >>> verifyObject(ICanonicalUrlData, countryset_urldata)
     True
-    >>> countryset_urldata.path
-    'countries'
+    >>> print(countryset_urldata.path)
+    countries
     >>> countryset_urldata.inside is getUtility(ILaunchpadRoot)
     True
 
@@ -260,7 +261,7 @@ and the objects it is inside of (or in other words, hierarchically below).
 We can see that this is the mainsite rooturl as configured in launchpad.conf.
 
     >>> from lp.services.webapp.vhosts import allvhosts
-    >>> print allvhosts.configs['mainsite'].rooturl
+    >>> print(allvhosts.configs['mainsite'].rooturl)
     http://launchpad.test/
 
 If anywhere in the chain we have an object that cannot be adapted to
@@ -311,7 +312,7 @@ First, let's define a helper function to help us test canonical_url_iterator.
 
     >>> def print_url_iterator(obj):
     ...     for obj in canonical_url_iterator(obj):
-    ...         print obj.__class__.__name__
+    ...         print(obj.__class__.__name__)
 
     >>> print_url_iterator(getUtility(ILaunchpadRoot))
     RootObject
@@ -432,7 +433,7 @@ And if the configuration does provide a rootsite:
     ... <configure
     ...     xmlns="http://namespaces.zope.org/zope";
     ...     xmlns:browser="http://namespaces.zope.org/browser";>
-    ...   <include package="zope.component" file="meta.zcml" />
+    ...   <include package="zope.component" file="meta.zcml" />
     ...   <include package="lp.services.webapp" file="meta.zcml" />
     ...   <utility
     ...       provides="{module_name}.ICountrySet"
@@ -503,7 +504,7 @@ canonical url chain that provides at least one of the interfaces given.
     True
     >>> nearest(town_instance, ICountry) is country_instance
     True
-    >>> print nearest(unrooted_object, ICountry)
+    >>> print(nearest(unrooted_object, ICountry))
     None
 
 
diff --git a/lib/lp/services/webapp/doc/canonical_url_examples.txt b/lib/lp/services/webapp/doc/canonical_url_examples.txt
index ec107a5..761581b 100644
--- a/lib/lp/services/webapp/doc/canonical_url_examples.txt
+++ b/lib/lp/services/webapp/doc/canonical_url_examples.txt
@@ -354,7 +354,7 @@ Set up example Branch Merge Proposal
 Branch merge proposals should have a canonical URL.  (Based on their source
 branch.)
 
-    >>> print canonical_url(merge_proposal)
+    >>> print(canonical_url(merge_proposal))
     http://code.launchpad.test/~name12/gnome-terminal/main/+merge/...
 
 Create example CodeReviewComment.
@@ -365,7 +365,7 @@ Create example CodeReviewComment.
 CodeReviewComment should have a canonical URL.  (It should extend the URL of
 the merge proposal)
 
-    >>> print canonical_url(comment)
+    >>> print(canonical_url(comment))
     http://code....test/~name12/gnome-terminal/main/+merge/.../comments/...
 
 
@@ -377,7 +377,7 @@ that they import to.
 
     >>> from lp.code.interfaces.codeimport import ICodeImportSet
     >>> code_import = getUtility(ICodeImportSet).get(1)
-    >>> print canonical_url(code_import)
+    >>> print(canonical_url(code_import))
     http://code.launchpad.test/~vcs-imports/gnome-terminal/import/+code-import
 
 Specifications
diff --git a/lib/lp/services/webapp/doc/canonicalurl.txt b/lib/lp/services/webapp/doc/canonicalurl.txt
index 75a67b3..547954e 100644
--- a/lib/lp/services/webapp/doc/canonicalurl.txt
+++ b/lib/lp/services/webapp/doc/canonicalurl.txt
@@ -62,11 +62,11 @@ directions.
     >>> from lp.services.webapp.canonicalurl import (nearest_adapter,
     ...     nearest_context_with_adapter)
 
-    >>> print nearest_adapter(root, ICookingDirections)
+    >>> print(nearest_adapter(root, ICookingDirections))
     None
-    >>> print nearest_adapter(cookbook, ICookingDirections)
+    >>> print(nearest_adapter(cookbook, ICookingDirections))
     None
-    >>> print nearest_adapter(recipe, ICookingDirections)
+    >>> print(nearest_adapter(recipe, ICookingDirections))
     None
 
 The same holds true for nearest_context_with_adapter():
@@ -88,26 +88,26 @@ that has the requested adaptation.
 "recipe" does not provide ICookingDirections, but "cookbook" does, so
 cookbook's adapter will be returned.
 
-    >>> print nearest_adapter(recipe, ICookingDirections)
+    >>> print(nearest_adapter(recipe, ICookingDirections))
     <...CookingDirections ...>
 
 We can verify that the adapter is actually for the Cookbook using
 nearest_adapter_with_context().
 
-    >>> print nearest_context_with_adapter(recipe, ICookingDirections)
+    >>> print(nearest_context_with_adapter(recipe, ICookingDirections))
     (<...Cookbook ...>, <...CookingDirections ...>)
 
 Calling nearest_adapter() on "cookbook" itself will return the
 CookingDirections:
 
-    >>> print nearest_adapter(cookbook, ICookingDirections)
+    >>> print(nearest_adapter(cookbook, ICookingDirections))
     <...CookingDirections ...>
 
 Calling nearest_adapter() on the hierarchy root returns nothing:
 the root does not have the requested adaptation, and there are no higher
 objects to search.
 
-    >>> print nearest_adapter(root, ICookingDirections)
+    >>> print(nearest_adapter(root, ICookingDirections))
     None
 
 
@@ -130,7 +130,7 @@ First we need a named adapter to use:
     >>> provideAdapter(LabelledCookbook, [ICookbook], ILabelledCookbook,
     ...     name='foo')
 
-    >>> print queryAdapter(cookbook, ILabelledCookbook)
+    >>> print(queryAdapter(cookbook, ILabelledCookbook))
     None
     >>> queryAdapter(cookbook, ILabelledCookbook, name='foo')
     <...LabelledCookbook ...>
@@ -145,14 +145,14 @@ For a recipe, this is the adapter for the cookbook:
 We can verify that the adapter is for the Cookbook using
 nearest_context_with_adapter():
 
-    >>> print nearest_context_with_adapter(
-    ...     recipe, ILabelledCookbook, name='foo')
+    >>> print(nearest_context_with_adapter(
+    ...     recipe, ILabelledCookbook, name='foo'))
     (<...Cookbook ...>, <...LabelledCookbook ...>)
 
 And we can see that the adapter is not returned if we omit the 'name'
 keyword argument:
 
-    >>> print nearest_adapter(recipe, ILabelledCookbook)
+    >>> print(nearest_adapter(recipe, ILabelledCookbook))
     None
 
 If we search for the adapter on the cookbook object, the lookup works as
@@ -163,5 +163,5 @@ expected:
 
 And searching for the adapter on the root object returns nothing:
 
-    >>> print nearest_adapter(root, ILabelledCookbook, name='foo')
+    >>> print(nearest_adapter(root, ILabelledCookbook, name='foo'))
     None
diff --git a/lib/lp/services/webapp/doc/launchbag.txt b/lib/lp/services/webapp/doc/launchbag.txt
index f846331..d5bb810 100644
--- a/lib/lp/services/webapp/doc/launchbag.txt
+++ b/lib/lp/services/webapp/doc/launchbag.txt
@@ -35,7 +35,7 @@ First, we'll set up various imports and stub objects.
 There have been no logins, so launchbag.login will be None.
 
 >>> launchbag = getUtility(ILaunchBag)
->>> print launchbag.login
+>>> print(launchbag.login)
 None
 
 Let's send a basic auth login event.
@@ -47,19 +47,19 @@ Let's send a basic auth login event.
 
 Now, launchbag.login will be 'foo.bar@xxxxxxxxxxxxx'.
 
->>> print launchbag.login
+>>> print(launchbag.login)
 foo.bar@xxxxxxxxxxxxx
 
 Login should be set back to None on a logout.
 
 >>> event = LoggedOutEvent(request)
 >>> notify(event)
->>> print launchbag.login
+>>> print(launchbag.login)
 None
 
 'user' will also be set to None:
 
->>> print launchbag.user
+>>> print(launchbag.user)
 None
 
 Let's do a cookie auth principal identification.  In this case, the login
@@ -68,7 +68,7 @@ will be cookie@xxxxxxxxxxx.
 >>> event = CookieAuthPrincipalIdentifiedEvent(
 ...     principal, request, 'cookie@xxxxxxxxxxx')
 >>> notify(event)
->>> print launchbag.login
+>>> print(launchbag.login)
 cookie@xxxxxxxxxxx
 
 
diff --git a/lib/lp/services/webapp/doc/menus.txt b/lib/lp/services/webapp/doc/menus.txt
index ada19bd..620a889 100644
--- a/lib/lp/services/webapp/doc/menus.txt
+++ b/lib/lp/services/webapp/doc/menus.txt
@@ -78,16 +78,16 @@ defined in ILink and IFacetLink.
     >>> ILinkData.providedBy(no_summary_link)
     True
 
-    >>> no_summary_link.target
-    'target'
+    >>> print(no_summary_link.target)
+    target
 
-    >>> no_summary_link.text
-    'text -->'
+    >>> print(no_summary_link.text)
+    text -->
 
-    >>> print no_summary_link.summary
+    >>> print(no_summary_link.summary)
     None
 
-    >>> print no_summary_link.icon
+    >>> print(no_summary_link.icon)
     None
 
     >>> no_summary_link.enabled
@@ -103,17 +103,17 @@ mark is as being "structured".
     >>> ILinkData.providedBy(full_link)
     True
 
-    >>> full_link.target
-    'target'
+    >>> print(full_link.target)
+    target
 
     >>> full_link.text
     <structured-string 'some <b>%s</b> text'>
 
-    >>> full_link.summary
-    'summary'
+    >>> print(full_link.summary)
+    summary
 
-    >>> full_link.icon
-    'icon'
+    >>> print(full_link.icon)
+    icon
 
     >>> full_link.enabled
     False
@@ -142,11 +142,11 @@ not shared or reused.
     >>> link2 = Link('target', 'text', 'summary', icon='icon', enabled=False)
 
     >>> for menu_link in ILink(link1), IFacetLink(link2):
-    ...     print menu_link.name, menu_link.url, menu_link.linked
+    ...     print(menu_link.name, menu_link.url, menu_link.linked)
     ...     menu_link.name = 'name'
     ...     menu_link.url = 'url'
     ...     menu_link.linked = False
-    ...     print menu_link.name, menu_link.url, menu_link.linked
+    ...     print(menu_link.name, menu_link.url, menu_link.linked)
     None None True
     name url False
     None None True
@@ -209,9 +209,9 @@ We can go through each attribute of each of the links, checking that
 they are as we expect.
 
     >>> for link in facetmenu.iterlinks():
-    ...     print '--- link %s ---' % link.name
+    ...     print('--- link %s ---' % link.name)
     ...     for attrname in sorted(IFacetLink.names(all=True)):
-    ...         print '%s: %s' % (attrname, getattr(link, attrname))
+    ...         print('%s: %s' % (attrname, getattr(link, attrname)))
     --- link foo ---
     enabled: True
     escapedtext: Foo
@@ -277,17 +277,17 @@ links.
     >>> IFacetLink.providedBy(adaptedtolink)
     False
 
-    >>> adaptedtolink.target
-    'target'
+    >>> print(adaptedtolink.target)
+    target
 
-    >>> adaptedtolink.text
-    'text'
+    >>> print(adaptedtolink.text)
+    text
 
-    >>> adaptedtolink.summary
-    'summary'
+    >>> print(adaptedtolink.summary)
+    summary
 
-    >>> adaptedtolink.icon
-    'icon'
+    >>> print(adaptedtolink.icon)
+    icon
 
     >>> adaptedtolink.enabled
     True
@@ -333,7 +333,7 @@ Next, we return the link as HTML.
     >>> login(ANONYMOUS, request)
 
     >>> link = Link('+target', 'text-->', 'summary', icon='icon')
-    >>> print ILink(link).render() #doctest: +NORMALIZE_WHITESPACE
+    >>> print(ILink(link).render()) #doctest: +NORMALIZE_WHITESPACE
     <a class="menu-link-None sprite icon" title="summary">text--&gt;</a>
 
     # Clean up our special login.
@@ -342,7 +342,7 @@ Next, we return the link as HTML.
 A menu item can be marked as hidden even though it is enabled.
 
     >>> link = Link('z', 'text', 'summary', icon='icon', hidden=True)
-    >>> print ILink(link).render() #doctest: +NORMALIZE_WHITESPACE
+    >>> print(ILink(link).render()) #doctest: +NORMALIZE_WHITESPACE
     <a class="menu-link-None sprite icon hidden" title="summary">text</a>
 
 
@@ -352,8 +352,8 @@ A link will be selected if its name is passed to the facet menu's
 iterlinks method, or otherwise, if its name is the defaultlink.
 
     >>> for link in facetmenu.iterlinks(selectedfacetname='bar'):
-    ...     print '--- link %s ---' % link.name
-    ...     print 'selected:', link.selected
+    ...     print('--- link %s ---' % link.name)
+    ...     print('selected:', link.selected)
     --- link foo ---
     selected: False
     --- link bar ---
@@ -363,8 +363,8 @@ When a link name is passed in, but no link of that name is in the menu,
 it is not an error.  No link is selected.
 
     >>> for link in facetmenu.iterlinks(selectedfacetname='nosuchname'):
-    ...     print '--- link %s ---' % link.name
-    ...     print 'selected:', link.selected
+    ...     print('--- link %s ---' % link.name)
+    ...     print('selected:', link.selected)
     --- link foo ---
     selected: False
     --- link bar ---
@@ -375,8 +375,8 @@ selected.
 
     >>> facetmenu.defaultlink = 'foo'
     >>> for link in facetmenu.iterlinks():
-    ...     print '--- link %s ---' % link.name
-    ...     print 'selected:', link.selected
+    ...     print('--- link %s ---' % link.name)
+    ...     print('selected:', link.selected)
     --- link foo ---
     selected: True
     --- link bar ---
@@ -386,8 +386,8 @@ Now, 'foo' is still the default, but 'bar' has been selected.  So only
 'bar' will be selected.
 
     >>> for link in facetmenu.iterlinks(selectedfacetname='bar'):
-    ...     print '--- link %s ---' % link.name
-    ...     print 'selected:', link.selected
+    ...     print('--- link %s ---' % link.name)
+    ...     print('selected:', link.selected)
     --- link foo ---
     selected: False
     --- link bar ---
@@ -397,8 +397,8 @@ We still have 'foo' as the default.  This time, 'nosuchlink' has been
 selected. As there is no such link, nothing will be selected.
 
     >>> for link in facetmenu.iterlinks(selectedfacetname='nosuchlink'):
-    ...     print '--- link %s ---' % link.name
-    ...     print 'selected:', link.selected
+    ...     print('--- link %s ---' % link.name)
+    ...     print('selected:', link.selected)
     --- link foo ---
     selected: False
     --- link bar ---
@@ -455,9 +455,9 @@ We can go through each attribute of each of the links, checking that
 they are as we expect.
 
     >>> for link in housefooappmenu.iterlinks():
-    ...     print '--- link %s ---' % link.name
+    ...     print('--- link %s ---' % link.name)
     ...     for attrname in sorted(ILink.names(all=True)):
-    ...         print '%s: %s' % (attrname, getattr(link, attrname))
+    ...         print('%s: %s' % (attrname, getattr(link, attrname)))
     --- link first ---
     enabled: True
     escapedtext: First menu
@@ -522,9 +522,9 @@ We can go through each attribute of each of the links, checking that
 they are as we expect.
 
     >>> for link in housefoocontextmenu.iterlinks():
-    ...     print '--- link %s ---' % link.name
+    ...     print('--- link %s ---' % link.name)
     ...     for attrname in sorted(ILink.names(all=True)):
-    ...         print '%s: %s' % (attrname, getattr(link, attrname))
+    ...         print('%s: %s' % (attrname, getattr(link, attrname)))
     --- link first ---
     enabled: True
     escapedtext: First context menu item
@@ -551,7 +551,8 @@ First, we define a couple of interfaces, and put them in a temporary module.
     >>> import sys
     >>> import types
 
-    >>> module = types.ModuleType(factory.getUniqueString().replace('-', '_'))
+    >>> module = types.ModuleType(
+    ...     six.ensure_str(factory.getUniqueString().replace('-', '_')))
     >>> sys.modules[module.__name__] = module
 
     >>> class IThingHavingFacets(Interface):
@@ -616,12 +617,12 @@ We also need to check that we have no IApplicationMenu adapter named
     ... class SomeOtherThing:
     ...     pass
     >>> something_with_menus = SomeOtherThing()
-    >>> print queryAdapter(something_with_menus, IApplicationMenu, 'foo')
+    >>> print(queryAdapter(something_with_menus, IApplicationMenu, 'foo'))
     None
 
 Same for an IContextMenu adapter.
 
-    >>> print queryAdapter(something_with_menus, IContextMenu, 'foo')
+    >>> print(queryAdapter(something_with_menus, IContextMenu, 'foo'))
     None
 
     >>> from zope.configuration import xmlconfig
@@ -725,7 +726,7 @@ link should appear linked. The request is also set as the menu's
     >>> links = test_tales('view/menu:facet', view=view)
 
     >>> for link in links:
-    ...     print link.url, link.selected, link.linked, link.summary
+    ...     print(link.url, link.selected, link.linked, link.summary)
     http://launchpad.test/sesamestreet/+foo False True None
     http://launchpad.test/sesamestreet/+bar True False More explanation about
                                                       Bar of sesamestreet
@@ -742,7 +743,7 @@ Let's try again, this time with a request from the participation.
 
     >>> links = test_tales('context/menu:facet', context=house)
     >>> for link in links:
-    ...     print link.url, link.selected, link.linked
+    ...     print(link.url, link.selected, link.linked)
     http://launchpad.test/sesamestreet/+foo False True
     http://launchpad.test/sesamestreet/+bar False False
     http://launchpad.test/sesamestreet False True
@@ -772,8 +773,8 @@ name.
     ...     url1='http://launchpad.test/sesamestreet/')
 
     >>> from zope.publisher.defaultview import getDefaultViewName
-    >>> getDefaultViewName(street, request)
-    '+baz'
+    >>> print(getDefaultViewName(street, request))
+    +baz
 
 So, in this example, the last link should not be 'linked' because it is
 equivalent to the default view name for a street.  The TALES
@@ -783,7 +784,7 @@ infrastructure actually calculates a shortened URL for this case.
     >>> view.__launchpad_facetname__ = 'bar'
     >>> links = test_tales('view/menu:facet', view=view)
     >>> for link in links:
-    ...     print link.url, link.linked
+    ...     print(link.url, link.linked)
     http://launchpad.test/sesamestreet/+foo True
     http://launchpad.test/sesamestreet/+bar True
     http://launchpad.test/sesamestreet False
@@ -795,7 +796,7 @@ You can traverse to an individual menu item from the facet menu:
     >>> view = LaunchpadView(house, request)
     >>> view.__launchpad_facetname__ = 'bar'
     >>> link = test_tales('view/menu:foo/first', view=view, request=request)
-    >>> print link.url
+    >>> print(link.url)
     http://launchpad.test/sesamestreet/number73/+first
 
 But if a non-existing entry is requested, a KeyError is raised:
@@ -821,13 +822,13 @@ object.
 
     >>> links = test_tales('view/menu:context', view=view)
     >>> for link in links.values():
-    ...     print link.url
+    ...     print(link.url)
     http://launchpad.test/sesamestreet/number73/+firstcontext
 
 The link is also reachable by name:
 
     >>> link = test_tales('context/menu:context/first', context=house)
-    >>> print link.url
+    >>> print(link.url)
     http://launchpad.test/sesamestreet/number73/+firstcontext
 
 When there is no menu for a thing, we get an empty iterator.
@@ -864,7 +865,7 @@ render() method.
 
     >>> html = test_tales('context/menu:foo/first/render',
     ...                   context=house, view=view, request=request)
-    >>> print html #doctest: +NORMALIZE_WHITESPACE
+    >>> print(html) #doctest: +NORMALIZE_WHITESPACE
     <a...class="menu-link-first"
     ...href="http://127.0.0.1/sesamestreet/number73/+first";>First menu</a>
 
@@ -903,8 +904,8 @@ If we're logged in as an anonymous user, then the link will be disabled.
     >>> login(ANONYMOUS)
 
     >>> foolink = somemenu.foo()
-    >>> foolink.text
-    'Admin the foo'
+    >>> print(foolink.text)
+    Admin the foo
 
     >>> foolink.enabled
     False
diff --git a/lib/lp/services/webapp/doc/navigation.txt b/lib/lp/services/webapp/doc/navigation.txt
index cd96dcf..6a09124 100644
--- a/lib/lp/services/webapp/doc/navigation.txt
+++ b/lib/lp/services/webapp/doc/navigation.txt
@@ -287,8 +287,8 @@ This time, we get the view object for the page that was registered.
 
     >>> navigation.publishTraverse(request, 'thingview')
     <...ThingSetView object...>
-    >>> navigation.publishTraverse(request, 'thingview')()
-    'a view on a thingset'
+    >>> print(navigation.publishTraverse(request, 'thingview')())
+    a view on a thingset
 
 
 == stepto traversals ==
@@ -328,10 +328,10 @@ Let's create a subclass of ThingSetNavigation, and add a 'stepto'.
 
     >>> navigation2.publishTraverse(request, 'ttt')
     <Thing 'TTT'>
-    >>> navigation2.publishTraverse(request, 'thingview')()
-    'a view on a thingset'
-    >>> navigation2.publishTraverse(request, 'thistle')
-    'A little thistle'
+    >>> print(navigation2.publishTraverse(request, 'thingview')())
+    a view on a thingset
+    >>> print(navigation2.publishTraverse(request, 'thistle'))
+    A little thistle
     >>> navigation2.publishTraverse(request, 'neverthere')
     ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
@@ -379,8 +379,8 @@ Let's create another subclass and add a stepthrough.
 
     >>> request.traversal_stack = ['prince', 'charming']
     >>> navigation3 = ThingSetNavigation3(thingset, request)
-    >>> navigation3.publishTraverse(request, 'toad')
-    'the toad called charming'
+    >>> print(navigation3.publishTraverse(request, 'toad'))
+    the toad called charming
     >>> request.traversal_stack = ['prince', 'charming']
     >>> navigation3 = ThingSetNavigation3(thingset, request)
     >>> navigation3.publishTraverse(request, 'neverland')
@@ -399,7 +399,7 @@ Let's create another subclass and add a stepthrough.
 
 Check that the request's state is as it should be.
 
-    >>> request.traversal_stack
+    >>> print(pretty(request.traversal_stack))
     ['prince']
 
 
@@ -457,38 +457,38 @@ Let's make another navigation class to test redirection.
     <...RedirectionView...>
     >>> print(navigation4.publishTraverse(request, 'tree')())
     <BLANKLINE>
-    >>> request.response.redirected_to
-    'trees'
-    >>> print request.response.status
+    >>> print(request.response.redirected_to)
+    trees
+    >>> print(request.response.status)
     301
 
     >>> print(navigation4.publishTraverse(request, 'toad')())
     <BLANKLINE>
-    >>> request.response.redirected_to
-    'toads'
-    >>> print request.response.status
+    >>> print(request.response.redirected_to)
+    toads
+    >>> print(request.response.status)
     None
 
     >>> print(navigation4.publishTraverse(request, 'something')())
     <BLANKLINE>
-    >>> request.response.redirected_to
-    '/another/place'
-    >>> print request.response.status
+    >>> print(request.response.redirected_to)
+    /another/place
+    >>> print(request.response.status)
     301
 
     >>> request.traversal_stack = ['tundra']
     >>> print(navigation4.publishTraverse(request, 'outerspace')())
     <BLANKLINE>
-    >>> request.response.redirected_to
-    '/siberia/tundra'
-    >>> print request.response.status
+    >>> print(request.response.redirected_to)
+    /siberia/tundra
+    >>> print(request.response.status)
     None
 
     >>> print(navigation4.publishTraverse(request, 'here')())
     <BLANKLINE>
-    >>> request.response.redirected_to
-    '/there'
-    >>> print request.response.status
+    >>> print(request.response.redirected_to)
+    /there
+    >>> print(request.response.status)
     301
 
 
@@ -511,18 +511,18 @@ with the remainder of the URL and or query string.
     <...RedirectionView...>
     >>> print(navigation5.publishTraverse(request, 'jobs')())
     <BLANKLINE>
-    >>> request.response.redirected_to
-    'http://ubuntu.com/jobs'
-    >>> print request.response.status
+    >>> print(request.response.redirected_to)
+    http://ubuntu.com/jobs
+    >>> print(request.response.status)
     301
 
     >>> request.traversal_stack = ['LaunchpadMeeting']
     >>> request.query_string = 'hilight=Time'
     >>> print(navigation5.publishTraverse(request, '+foo')())
     <BLANKLINE>
-    >>> request.response.redirected_to
-    'http://wiki.canonical.com/LaunchpadMeeting?hilight=Time'
-    >>> print request.response.status
+    >>> print(request.response.redirected_to)
+    http://wiki.canonical.com/LaunchpadMeeting?hilight=Time
+    >>> print(request.response.status)
     303
 
 
@@ -565,36 +565,36 @@ so far, and also defining some more stepthroughs and steptos and redirections.
 
 Check out the traversals defined directly.
 
-    >>> ubernav.publishTraverse(request, 'teeth')
-    'some teeth'
+    >>> print(ubernav.publishTraverse(request, 'teeth'))
+    some teeth
 
     >>> ubernav.publishTraverse(request, 'diplodocus')
     <...RedirectionView...>
 
     >>> request.traversal_stack = ['frank']
-    >>> ubernav.publishTraverse(request, 'diplodocus')
-    'diplodocus called frank'
+    >>> print(ubernav.publishTraverse(request, 'diplodocus'))
+    diplodocus called frank
 
     >>> print(ubernav.publishTraverse(request, 'topology')())
     <BLANKLINE>
-    >>> request.response.redirected_to
-    'topologies'
+    >>> print(request.response.redirected_to)
+    topologies
 
 Check those from ThingSetNavigation, implicitly:
 
-    >>> ubernav.publishTraverse(request, 'thingview')()
-    'a view on a thingset'
+    >>> print(ubernav.publishTraverse(request, 'thingview')())
+    a view on a thingset
 
 Check those from ThingSetNavigation2:
 
-    >>> ubernav.publishTraverse(request, 'thistle')
-    'A little thistle'
+    >>> print(ubernav.publishTraverse(request, 'thistle'))
+    A little thistle
 
 Check those from ThingSetNavigation3:
 
     >>> request.traversal_stack = ['prince', 'charming']
-    >>> ubernav.publishTraverse(request, 'toad')
-    'the toad called charming'
+    >>> print(ubernav.publishTraverse(request, 'toad'))
+    the toad called charming
 
     >>> request.traversal_stack = []
 
@@ -602,18 +602,18 @@ Check those from ThingSetNavigation4:
 
     >>> print(ubernav.publishTraverse(request, 'tree')())
     <BLANKLINE>
-    >>> request.response.redirected_to
-    'trees'
+    >>> print(request.response.redirected_to)
+    trees
 
     >>> print(ubernav.publishTraverse(request, 'toad')())
     <BLANKLINE>
-    >>> request.response.redirected_to
-    'toads'
+    >>> print(request.response.redirected_to)
+    toads
 
     >>> print(ubernav.publishTraverse(request, 'ttt')())
     <BLANKLINE>
-    >>> request.response.redirected_to
-    '/another/place'
+    >>> print(request.response.redirected_to)
+    /another/place
 
 
 == Testing that multiple inheritance involving decorators works ==
@@ -645,14 +645,14 @@ Check those from ThingSetNavigation4:
     ...         return 'foo2 from C'
 
     >>> instance_of_c = C(thingset, request)
-    >>> instance_of_c.publishTraverse(request, 'foo')
-    'foo'
-    >>> instance_of_c.publishTraverse(request, 'bar')
-    'bar'
-    >>> instance_of_c.publishTraverse(request, 'baz')
-    'baz'
-    >>> instance_of_c.publishTraverse(request, 'foo2')
-    'foo2 from C'
+    >>> print(instance_of_c.publishTraverse(request, 'foo'))
+    foo
+    >>> print(instance_of_c.publishTraverse(request, 'bar'))
+    bar
+    >>> print(instance_of_c.publishTraverse(request, 'baz'))
+    baz
+    >>> print(instance_of_c.publishTraverse(request, 'foo2'))
+    foo2 from C
 
 
 == Showing that the name of the function really doesn't matter ==
@@ -668,12 +668,12 @@ Check those from ThingSetNavigation4:
     ...         return 'bar'
 
     >>> instance_of_dupenames = DupeNames(thingset, request)
-    >>> instance_of_dupenames.publishTraverse(request, 'foo')
-    'foo'
-    >>> instance_of_dupenames.publishTraverse(request, 'bar')
-    'bar'
+    >>> print(instance_of_dupenames.publishTraverse(request, 'foo'))
+    foo
+    >>> print(instance_of_dupenames.publishTraverse(request, 'bar'))
+    bar
 
 The doit_*() methods still work though.
 
-    >>> instance_of_dupenames.doit_bar()
-    'bar'
+    >>> print(instance_of_dupenames.doit_bar())
+    bar
diff --git a/lib/lp/services/webapp/doc/notification-text-escape.txt b/lib/lp/services/webapp/doc/notification-text-escape.txt
index 3275f9b..ad6b26f 100644
--- a/lib/lp/services/webapp/doc/notification-text-escape.txt
+++ b/lib/lp/services/webapp/doc/notification-text-escape.txt
@@ -25,7 +25,7 @@ unchanged:
     >>> response = new_response()
     >>> response.addNotification('clean')
     >>> for notification in response.notifications:
-    ...   print notification.message
+    ...   print(notification.message)
     clean
 
 But text containing markup is CGI-escaped:
diff --git a/lib/lp/services/webapp/doc/renamed-view.txt b/lib/lp/services/webapp/doc/renamed-view.txt
index 5d12bec..2b125a1 100644
--- a/lib/lp/services/webapp/doc/renamed-view.txt
+++ b/lib/lp/services/webapp/doc/renamed-view.txt
@@ -33,7 +33,7 @@ update bookmarks.
     <BLANKLINE>
     >>> request.response.getStatus()
     301
-    >>> print request.response.getHeader('Location')
+    >>> print(request.response.getHeader('Location'))
     http://launchpad.test/ubuntu/+questions
 
 The view can also work for names registered on the root, and the
@@ -46,7 +46,7 @@ new_name can be a relative path.
     <BLANKLINE>
     >>> request.response.getStatus()
     301
-    >>> print view.request.response.getHeader('Location')
+    >>> print(view.request.response.getHeader('Location'))
     http://launchpad.test/+tour/index.html
 
 
@@ -60,7 +60,7 @@ to the redirected URL.
     >>> view = RenamedView(ubuntu, request, '+questions')
     >>> print(view())
     <BLANKLINE>
-    >>> print request.response.getHeader('Location')
+    >>> print(request.response.getHeader('Location'))
     http://launchpad.test/ubuntu/+questions?field.status=Open
 
 
@@ -74,7 +74,7 @@ change the virtual host used for the redirection.
     ...     ubuntu, request, '+questions', rootsite='answers')
     >>> print(view())
     <BLANKLINE>
-    >>> print request.response.getHeader('Location')
+    >>> print(request.response.getHeader('Location'))
     http://answers.launchpad.test/ubuntu/+questions
 
 
@@ -117,5 +117,5 @@ browser:renamed-page is available for this purpose.
     >>> view = getMultiAdapter((ubuntu, request), name='+old_tickets_page')
     >>> print(view())
     <BLANKLINE>
-    >>> print request.response.getHeader('Location')
+    >>> 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 d91cfb2..bd8d2e0 100644
--- a/lib/lp/services/webapp/doc/test_adapter.txt
+++ b/lib/lp/services/webapp/doc/test_adapter.txt
@@ -27,7 +27,7 @@ IStoreSelector utility.
     >>> store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
     >>> dbname = DatabaseLayer._db_fixture.dbname
     >>> active_name = store.execute("SELECT current_database()").get_one()[0]
-    >>> if active_name != dbname: print '%s != %s' % (active_name, dbname)
+    >>> if active_name != dbname: print('%s != %s' % (active_name, dbname))
     >>> active_name == dbname
     True
 
@@ -63,7 +63,7 @@ correctly, especially for the case of LIKE which uses % characters.
     >>> store.execute("SELECT * FROM person where name like '%d foo'||'%%'",
     ...     noresult=True)
     >>> for _, _, _, statement, _ in get_request_statements():
-    ...     print statement
+    ...     print(statement)
     SELECT 1
     SELECT 2
     SELECT * FROM person where name = E'fred'
@@ -78,7 +78,7 @@ A timeline is created too:
     >>> for action in timeline.actions:
     ...    if not action.category.startswith("SQL-"):
     ...        continue
-    ...    print action.detail
+    ...    print(action.detail)
     SELECT 1
     SELECT 2
     SELECT * FROM person where name = E'fred'
@@ -104,7 +104,7 @@ statements.
     >>> store.execute('SELECT 2', noresult=True)
     >>> store.execute('SELECT 3', noresult=True)
     >>> for _, _, _, statement, _ in get_request_statements():
-    ...     print statement
+    ...     print(statement)
     SELECT 2
     SELECT 3
     >>> clear_request_started()
@@ -120,7 +120,7 @@ that aborted transactions are still in the status "Active".
     >>> store.execute('SELECT 3', noresult=True)
     >>> transaction.abort()
     >>> for _, _, _, statement, _ in get_request_statements():
-    ...     print statement
+    ...     print(statement)
     SELECT 1
     SELECT 2
     Transaction completed, status: Committed
@@ -135,7 +135,7 @@ it pass.
     >>> import warnings
     >>> with warnings.catch_warnings(record=True) as no_request_warning:
     ...     clear_request_started()
-    >>> print no_request_warning[0].message
+    >>> print(no_request_warning[0].message)
     clear_request_started() called outside of a request
 
 Some requests are expected to log actions with very large details, such as
@@ -151,7 +151,7 @@ memory on logging.
     >>> store.execute('SELECT 1', noresult=True)
     >>> transaction.abort()
     >>> for _, _, _, statement, _ in get_request_statements():
-    ...     print statement
+    ...     print(statement)
     <redacted>
     Transaction completed, status: Active
     >>> clear_request_started()
@@ -163,7 +163,7 @@ Statement Timeout
 The timeout is set in launchpad.conf file.  By default it is unset,
 which corresponds to no timeout:
 
-    >>> print config.database.db_statement_timeout
+    >>> print(config.database.db_statement_timeout)
     None
 
 
@@ -193,7 +193,7 @@ the Postgres statement timeout (a value of zero means no timeout):
     ...     store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
 
     >>> set_request_started()
-    >>> print current_statement_timeout(store)
+    >>> print(current_statement_timeout(store))
     0ms
     >>> clear_request_started()
 
@@ -209,9 +209,9 @@ timeout by sleeping for 200ms with a 100ms statement timeout:
     >>> config.push('base_test_data', test_data)
     >>> reset_store()
     >>> set_request_started()
-    >>> print current_statement_timeout(store)
+    >>> print(current_statement_timeout(store))
     100ms
-    >>> store.execute('SELECT pg_sleep(0.200)', noresult=True)
+    >>> store.execute(six.ensure_str('SELECT pg_sleep(0.200)'), noresult=True)
     ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
@@ -219,7 +219,7 @@ timeout by sleeping for 200ms with a 100ms statement timeout:
 
 Even though the statement timed out, it is recorded in the statement log:
 
-    >>> print get_request_statements()[-1][3]
+    >>> print(get_request_statements()[-1][3])
     SELECT pg_sleep(0.200)
 
     >>> clear_request_started()
@@ -229,7 +229,7 @@ set_request_started() is called in scripts.
 
     >>> reset_store()
     >>> set_request_started(enable_timeout=False)
-    >>> print current_statement_timeout(store)
+    >>> print(current_statement_timeout(store))
     0ms
     >>> store.execute('SELECT pg_sleep(0.200)', noresult=True)
     >>> clear_request_started()
@@ -261,12 +261,12 @@ to fake how long things take.
     >>> set_request_started()
 
     >>> store.execute('SELECT TRUE', noresult=True)
-    >>> print current_statement_timeout(store)
+    >>> print(current_statement_timeout(store))
     10000ms
     >>> time_travel(0.5) # Forward in time 0.5 seconds
 
     >>> store.execute('SELECT TRUE', noresult=True)
-    >>> print current_statement_timeout(store)
+    >>> print(current_statement_timeout(store))
     10000ms
     >>> time_travel(0.6) # Forward in time 0.6 seconds, now over precision
 
@@ -274,7 +274,7 @@ This invokation, the PostgreSQL statement timeout will be updated before
 issuing the SQL command as we have exceeded the precision period:
 
     >>> store.execute('SELECT TRUE', noresult=True)
-    >>> print current_statement_timeout(store)
+    >>> print(current_statement_timeout(store))
     8900ms
     >>> time_travel(8.89) # 0.01s remaining before hard timeout
 
@@ -282,7 +282,7 @@ issuing the SQL command as we have exceeded the precision period:
 This final invokation, we will actually sleep to ensure that the
 timeout being reported by PostgreSQL is actually working:
 
-    >>> store.execute('SELECT pg_sleep(0.2)', noresult=True)
+    >>> store.execute(six.ensure_str('SELECT pg_sleep(0.2)'), noresult=True)
     ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
     Traceback (most recent call last):
       ...
@@ -299,7 +299,7 @@ Set the timeout to 5000ms for the next tests:
     >>> config.push('test', test_data)
     >>> reset_store()
     >>> set_request_started()
-    >>> print current_statement_timeout(store)
+    >>> print(current_statement_timeout(store))
     5000ms
     >>> clear_request_started()
 
@@ -436,13 +436,13 @@ The Launchpad store uses lp.services.config to configure its
 connection.  This can be adjusted by choosing a different database
 config section.  By default we connect as "launchpad"
 
-    >>> print store.execute("select current_user").get_one()[0]
+    >>> print(store.execute("select current_user").get_one()[0])
     launchpad_main
 
     >>> from lp.services.config import dbconfig
     >>> dbconfig.override(dbuser='statistician')
     >>> reset_store()
-    >>> print store.execute("select current_user").get_one()[0]
+    >>> print(store.execute("select current_user").get_one()[0])
     statistician
     >>> store.execute("""
     ...     INSERT INTO SourcePackageName(name) VALUES ('fnord4')
@@ -455,7 +455,7 @@ config section.  By default we connect as "launchpad"
 This is not reset at the end of the transaction:
 
     >>> transaction.abort()
-    >>> print store.execute("select current_user").get_one()[0]
+    >>> print(store.execute("select current_user").get_one()[0])
     statistician
     >>> store.execute("""
     ...     INSERT INTO SourcePackageName(name) VALUES ('fnord4')
@@ -470,7 +470,7 @@ So you need to explicitly set the user back to the default:
 
     >>> dbconfig.override(dbuser=None)
     >>> reset_store()
-    >>> print store.execute("select current_user").get_one()[0]
+    >>> print(store.execute("select current_user").get_one()[0])
     launchpad_main
     >>> store.execute("""
     ...     INSERT INTO SourcePackageName(name) VALUES ('fnord4')
diff --git a/lib/lp/services/webapp/doc/test_adapter_timeout.txt.disabled b/lib/lp/services/webapp/doc/test_adapter_timeout.txt.disabled
index b2fe604..c1955cf 100644
--- a/lib/lp/services/webapp/doc/test_adapter_timeout.txt.disabled
+++ b/lib/lp/services/webapp/doc/test_adapter_timeout.txt.disabled
@@ -91,12 +91,12 @@ Now we actually demonstrate the behaviour.  The view did raise a TimeoutError.
 
 The exception view did not.
 
-    >>> print timeout_in_exception_view
+    >>> print(timeout_in_exception_view)
     False
 
 The OOPS has the SQL from the main view.
 
-    >>> print oops_capture.oopses[-1]['timeline']
+    >>> print(oops_capture.oopses[-1]['timeline'])
     [(0, 0, 'SELECT ... FROM ... WHERE ...'), ...]
 
 All that's left is to clean up after ourselves.
diff --git a/lib/lp/services/webapp/doc/timeout.txt b/lib/lp/services/webapp/doc/timeout.txt
index 9a80f1d..102eb93 100644
--- a/lib/lp/services/webapp/doc/timeout.txt
+++ b/lib/lp/services/webapp/doc/timeout.txt
@@ -98,7 +98,7 @@ and a TimeoutError is never raised.
     ... [database]
     ... db_statement_timeout = None'''))
 
-    >>> print timeout_func()
+    >>> print(timeout_func())
     None
 
     >>> wait_a_little()
diff --git a/lib/lp/services/webapp/doc/uri.txt b/lib/lp/services/webapp/doc/uri.txt
index c356db0..d76f851 100644
--- a/lib/lp/services/webapp/doc/uri.txt
+++ b/lib/lp/services/webapp/doc/uri.txt
@@ -15,23 +15,23 @@ security proxies.
 
 We can access the various URI components:
 
-  >>> print proxied_uri1.scheme
+  >>> print(proxied_uri1.scheme)
   http
-  >>> print proxied_uri1.userinfo
+  >>> print(proxied_uri1.userinfo)
   None
-  >>> print proxied_uri1.host
+  >>> print(proxied_uri1.host)
   a
-  >>> print proxied_uri1.port
+  >>> print(proxied_uri1.port)
   None
-  >>> print proxied_uri1.path
+  >>> print(proxied_uri1.path)
   /b/c/d;p
-  >>> print proxied_uri1.query
+  >>> print(proxied_uri1.query)
   q
-  >>> print proxied_uri1.fragment
+  >>> print(proxied_uri1.fragment)
   None
-  >>> print proxied_uri1.authority
+  >>> print(proxied_uri1.authority)
   a
-  >>> print proxied_uri1.hier_part
+  >>> print(proxied_uri1.hier_part)
   //a/b/c/d;p
 
 We can test for equality:
@@ -55,19 +55,19 @@ Similarly, inequality can be checked:
 
 We can get the string value and representation of a URI:
 
-  >>> print str(proxied_uri1)
+  >>> print(str(proxied_uri1))
   http://a/b/c/d;p?q
-  >>> print repr(proxied_uri1)
+  >>> print(repr(proxied_uri1))
   URI('http://a/b/c/d;p?q')
 
 We can replace components:
 
-  >>> print proxied_uri1.replace(scheme='https')
+  >>> print(proxied_uri1.replace(scheme='https'))
   https://a/b/c/d;p?q
 
 We can append a component:
 
-  >>> print proxied_uri1.append('e/f')
+  >>> print(proxied_uri1.append('e/f'))
   http://a/b/c/d;p/e/f
 
 We can check for containment:
@@ -80,7 +80,7 @@ We can check for containment:
 We can create a URI that ensures it has or does not have a trailing
 slash:
 
-  >>> print proxied_uri1.ensureSlash()
+  >>> print(proxied_uri1.ensureSlash())
   http://a/b/c/d;p/?q
-  >>> print proxied_uri1.ensureNoSlash()
+  >>> print(proxied_uri1.ensureNoSlash())
   http://a/b/c/d;p?q
diff --git a/lib/lp/services/webapp/doc/webapp-publication.txt b/lib/lp/services/webapp/doc/webapp-publication.txt
index fa178cd..8af06ab 100644
--- a/lib/lp/services/webapp/doc/webapp-publication.txt
+++ b/lib/lp/services/webapp/doc/webapp-publication.txt
@@ -31,10 +31,10 @@ These are parsed into webapp.vhost.allvhosts.
     >>> allvhosts.use_https
     False
     >>> for confname, vhost in sorted(allvhosts.configs.items()):
-    ...     print confname, '@', vhost.hostname
-    ...     print 'rooturl:', vhost.rooturl
-    ...     print 'althosts:', (', '.join(vhost.althostnames))
-    ...     print '----'
+    ...     print(confname, '@', vhost.hostname)
+    ...     print('rooturl:', vhost.rooturl)
+    ...     print('althosts:', (', '.join(vhost.althostnames)))
+    ...     print('----')
     answers @ answers.launchpad.test
     rooturl: http://answers.launchpad.test/
     althosts:
@@ -85,7 +85,7 @@ collected into a set.  This provides a quick way to determine if a
 request is headed to one of the officialy-used Launchpad host names:
 
     >>> for hostname in sorted(allvhosts.hostnames):
-    ...     print hostname
+    ...     print(hostname)
     answers.launchpad.test
     api.launchpad.test
     blueprints.launchpad.test
@@ -162,7 +162,7 @@ canHandle() by saving the environment to a thread-local variable. This
 information is retrieved later on, in __call__().
 
     >>> for key, value in sorted(factory._thread_local.environment.items()):
-    ...     print '%s: %s' % (key, value)
+    ...     print('%s: %s' % (key, value))
     HTTP_HOST: launchpad.test
     REQUEST_METHOD: GET
     SERVER_PORT: 1234
@@ -459,13 +459,13 @@ less nice.
 
     >>> logout()
     >>> from lp.testing.pages import http
-    >>> print http("GET / HTTP/1.1\n"
-    ...            "Host: nosuchhost.launchpad.test")
+    >>> print(http("GET / HTTP/1.1\n"
+    ...            "Host: nosuchhost.launchpad.test"))
     HTTP/1.1 404 Not Found
     ...
 
-    >>> print http("GET /foo/bar HTTP/1.1\n"
-    ...            "Host: xmlrpc.launchpad.test")
+    >>> print(http("GET /foo/bar HTTP/1.1\n"
+    ...            "Host: xmlrpc.launchpad.test"))
     HTTP/1.1 405 Method Not Allowed
     Allow: POST
     ...
@@ -580,7 +580,7 @@ no value was submitted with the field.
 All the submitted field names can be iterated over:
 
     >>> for name in sorted(request.form_ng):
-    ...     print name
+    ...     print(name)
     a_field
     items_field
 
@@ -622,7 +622,7 @@ name of the context class and the view class name.
     >>> view = TestView(TestContext(), request)
     >>> publication.beforeTraversal(request)
     >>> publication.afterTraversal(request, view)
-    >>> print request._orig_env['launchpad.pageid']
+    >>> print(request._orig_env['launchpad.pageid'])
     TestContext:TestView
     >>> from lp.services.webapp.adapter import (
     ...     clear_request_started, set_request_started)
@@ -875,7 +875,7 @@ The user attribute is an empty string, when no user is logged in.
     >>> import transaction
     >>> txn = transaction.begin()
     >>> request, publication = get_request_and_publication()
-    >>> print request.principal
+    >>> print(request.principal)
     None
 
     # Our afterCall() implementation expects to find _publication_start and
@@ -885,7 +885,7 @@ The user attribute is an empty string, when no user is logged in.
     >>> request._publication_start = 1.345
     >>> request._publication_thread_start = None
     >>> publication.afterCall(request, None)
-    >>> print txn.user
+    >>> print(txn.user)
     <BLANKLINE>
 
 But if there is a logged in user, the transaction user attribute will
@@ -900,7 +900,7 @@ allowing different authentication based on the traversed objects):
     16
     >>> request.setPrincipal(foo_bar)
     >>> publication.afterCall(request, None)
-    >>> print txn.user
+    >>> print(txn.user)
      / 16
 
 
@@ -918,7 +918,7 @@ some string in its finishReadOnlyRequest().
 
     >>> class MyPublication(LaunchpadBrowserPublication):
     ...     def finishReadOnlyRequest(self, request, ob, txn):
-    ...         print "booo!"
+    ...         print("booo!")
 
     >>> publication = MyPublication(None)
     >>> publication.afterCall(request, None)
@@ -938,7 +938,7 @@ be automatically reverted in a GET request.
     ...         Person.id == EmailAddress.personID,
     ...         EmailAddress.email == 'foo.bar@xxxxxxxxxxxxx').one()
     >>> foo_bar = get_foo_bar_person()
-    >>> print foo_bar.description
+    >>> print(foo_bar.description)
     None
     >>> foo_bar.description = 'Montreal'
 
@@ -953,7 +953,7 @@ be automatically reverted in a GET request.
     >>> publication.afterCall(request, None)
     >>> txn = transaction.begin()
     >>> foo_bar = get_foo_bar_person()
-    >>> print foo_bar.description
+    >>> print(foo_bar.description)
     None
 
 But not if the request uses POST, the changes will be preserved.
@@ -971,7 +971,7 @@ But not if the request uses POST, the changes will be preserved.
     >>> request._publication_thread_start = None
     >>> publication.afterCall(request, None)
     >>> txn = transaction.begin()
-    >>> print get_foo_bar_person().description
+    >>> print(get_foo_bar_person().description)
     Darwin
 
 
@@ -988,7 +988,7 @@ Doomed transactions are aborted.
     >>> bound_abort = txn.abort
     >>> def faux_abort():
     ...     bound_abort()
-    ...     print 'Aborted'
+    ...     print('Aborted')
     ...
     >>> txn.abort = faux_abort
 
@@ -1064,7 +1064,7 @@ In other cases, like a GET, the body would be unchanged.
     >>> request._publication_start = 1.345
     >>> request._publication_thread_start = None
     >>> publication.afterCall(request, None)
-    >>> print request.response.consumeBody()
+    >>> print(request.response.consumeBody())
     Some boring content.
 
 
@@ -1107,7 +1107,7 @@ utility to setup the request.
     >>> request, publication = get_request_and_publication(
     ...     extra_environment=dict(HTTP_AUTHORIZATION=foo_bar_auth))
     >>> principal = publication.getPrincipal(request)
-    >>> print principal.title
+    >>> print(principal.title)
     Foo Bar
 
 The feeds implementation always returns the anonymous user.
@@ -1152,7 +1152,7 @@ token.
     ...     extra_environment=dict(QUERY_STRING=urlencode(form)))
     >>> test_request.processInputs()
     >>> principal = publication.getPrincipal(test_request)
-    >>> print principal.title
+    >>> print(principal.title)
     Guilherme Salgado
     >>> principal.access_level
     <DBItem AccessLevel.WRITE_PUBLIC...
diff --git a/lib/lp/services/webapp/doc/zcmldirectives.txt b/lib/lp/services/webapp/doc/zcmldirectives.txt
index 71b14e4..e70ceed 100644
--- a/lib/lp/services/webapp/doc/zcmldirectives.txt
+++ b/lib/lp/services/webapp/doc/zcmldirectives.txt
@@ -101,14 +101,14 @@ its __launchpad_facetname__ attribute.
     >>> cls = get_simplelaunchpadviewclass(context.actions)
     >>> cls
     <class 'zope.browserpage.metaconfigure.SimpleLaunchpadViewClass'>
-    >>> cls.__launchpad_facetname__
-    'FACET'
+    >>> print(cls.__launchpad_facetname__)
+    FACET
 
 Next, a functional/integration test of the overridden directive.
 
-    >>> print queryMultiAdapter((fooobject, request), name='+whatever')
+    >>> print(queryMultiAdapter((fooobject, request), name='+whatever'))
     None
-    >>> print queryMultiAdapter((fooobject, request), name='+mandrill')
+    >>> print(queryMultiAdapter((fooobject, request), name='+mandrill'))
     None
 
     >>> from zope.configuration import xmlconfig
@@ -139,16 +139,16 @@ Next, a functional/integration test of the overridden directive.
     >>> whatever_view = queryMultiAdapter(
     ...     (fooobject, request), name='+whatever')
 
-    >>> print whatever_view.__class__.__name__
+    >>> print(whatever_view.__class__.__name__)
     FooView
-    >>> print whatever_view.__launchpad_facetname__
+    >>> print(whatever_view.__launchpad_facetname__)
     the_evil_facet
     >>> mandrill_view = queryMultiAdapter(
     ...     (fooobject, request), name='+mandrill')
 
-    >>> print mandrill_view.__class__.__name__
+    >>> print(mandrill_view.__class__.__name__)
     SimpleViewClass from ...base-layout.pt
-    >>> print mandrill_view.__launchpad_facetname__
+    >>> print(mandrill_view.__launchpad_facetname__)
     another-mister-lizard
 
 
@@ -184,17 +184,17 @@ its __launchpad_facetname__ attribute.
     >>> cls = get_simplelaunchpadviewclass(context.actions)
     >>> cls
     <class 'zope.browserpage.metaconfigure.SimpleLaunchpadViewClass'>
-    >>> cls.__launchpad_facetname__
-    'FACET'
+    >>> print(cls.__launchpad_facetname__)
+    FACET
     >>> cls2 = context.actions[3][2][1]
     >>> cls2
     <class 'zope.browserpage.metaconfigure.SimpleLaunchpadViewClass'>
-    >>> cls2.__launchpad_facetname__
-    'OUTERFACET'
+    >>> print(cls2.__launchpad_facetname__)
+    OUTERFACET
 
 Next, a functional/integration test of the overridden directive.
 
-    >>> print queryMultiAdapter((fooobject, request), name='+whatever2')
+    >>> print(queryMultiAdapter((fooobject, request), name='+whatever2'))
     None
 
     >>> zcmlcontext = xmlconfig.string("""
@@ -222,16 +222,16 @@ Next, a functional/integration test of the overridden directive.
 
     >>> whatever2_view = queryMultiAdapter(
     ...     (fooobject, request), name='+whatever2')
-    >>> print whatever2_view.__class__.__name__
+    >>> print(whatever2_view.__class__.__name__)
     FooView
-    >>> print whatever2_view.__launchpad_facetname__
+    >>> print(whatever2_view.__launchpad_facetname__)
     another_evil_facet
 
     >>> whatever3_view = queryMultiAdapter(
     ...     (fooobject, request), name='+whatever3')
-    >>> print whatever3_view.__class__.__name__
+    >>> print(whatever3_view.__class__.__name__)
     FooView
-    >>> print whatever3_view.__launchpad_facetname__
+    >>> print(whatever3_view.__launchpad_facetname__)
     outerspace
 
 
@@ -248,12 +248,12 @@ Name some variables to mirror the names used as arguments.
     >>> facet = 'whole-file-facet'
 
     >>> gc = GroupingFacet(context, facet=facet)
-    >>> gc.facet
-    'whole-file-facet'
+    >>> print(gc.facet)
+    whole-file-facet
 
 Next, a functional/integration test of the overridden directive.
 
-    >>> print queryMultiAdapter((fooobject, request), name='+impliedfacet')
+    >>> print(queryMultiAdapter((fooobject, request), name='+impliedfacet'))
     None
 
     >>> zcmlcontext = xmlconfig.string("""
@@ -277,9 +277,9 @@ Next, a functional/integration test of the overridden directive.
 
     >>> impliedfacet_view = queryMultiAdapter(
     ...     (fooobject, request), name='+impliedfacet')
-    >>> print impliedfacet_view.__class__.__name__
+    >>> print(impliedfacet_view.__class__.__name__)
     FooView
-    >>> print impliedfacet_view.__launchpad_facetname__
+    >>> print(impliedfacet_view.__launchpad_facetname__)
     whole-facet
 
 
diff --git a/lib/lp/services/webapp/servers.py b/lib/lp/services/webapp/servers.py
index 0bd6152..d1e17fe 100644
--- a/lib/lp/services/webapp/servers.py
+++ b/lib/lp/services/webapp/servers.py
@@ -3,6 +3,8 @@
 
 """Definition of the internet servers that Launchpad uses."""
 
+from __future__ import absolute_import, print_function
+
 __metaclass__ = type
 
 import threading
@@ -137,19 +139,19 @@ class StepsToGo(six.Iterator):
     False
     >>> len(stepstogo)
     3
-    >>> print stepstogo.consume()
+    >>> print(stepstogo.consume())
     foo
     >>> request._traversed_names
     ['foo']
     >>> request.stack
     ['baz', 'bar']
-    >>> print stepstogo.consume()
+    >>> print(stepstogo.consume())
     bar
     >>> bool(stepstogo)
     True
-    >>> print stepstogo.consume()
+    >>> print(stepstogo.consume())
     baz
-    >>> print stepstogo.consume()
+    >>> print(stepstogo.consume())
     None
     >>> bool(stepstogo)
     False
diff --git a/lib/lp/services/webapp/sorting.py b/lib/lp/services/webapp/sorting.py
index 6d34142..052c669 100644
--- a/lib/lp/services/webapp/sorting.py
+++ b/lib/lp/services/webapp/sorting.py
@@ -3,6 +3,8 @@
 
 """This module contains sorting utility functions."""
 
+from __future__ import absolute_import, print_function
+
 __metaclass__ = type
 __all__ = ['expand_numbers',
            'sorted_version_numbers',
diff --git a/lib/lp/services/webapp/tests/cookie-authentication.txt b/lib/lp/services/webapp/tests/cookie-authentication.txt
index 503ba9d..9f73e5d 100644
--- a/lib/lp/services/webapp/tests/cookie-authentication.txt
+++ b/lib/lp/services/webapp/tests/cookie-authentication.txt
@@ -5,8 +5,8 @@ so it cannot tell that it will end up overwriting the existing cookie.
     >>> from lp.testing.layers import BaseLayer
     >>> feeds_root_url = BaseLayer.appserver_root_url('feeds')
     >>> browser.open('%s/announcements.atom' % feeds_root_url)
-    >>> browser.vhost
-    'http://feeds.launchpad.test'
+    >>> print(browser.vhost)
+    http://feeds.launchpad.test
     >>> browser.urlpath
     '/announcements.atom'
     >>> len(browser.cookies)
@@ -21,14 +21,14 @@ are sent to other vhosts in the same domain.
     # On a browser with JS support, this page would've been automatically
     # submitted (thanks to the onload handler), but testbrowser doesn't
     # support JS, so we have to submit the form manually.
-    >>> print browser.contents
+    >>> print(browser.contents)
     <html>...<body onload="document.forms[0].submit();"...
     >>> browser.getControl('Continue').click()
 
     >>> from lp.services.webapp.tests.test_login import (
     ...     fill_login_form_and_submit)
     >>> fill_login_form_and_submit(browser, 'foo.bar@xxxxxxxxxxxxx')
-    >>> print extract_text(find_tag_by_id(browser.contents, 'logincontrol'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'logincontrol')))
     Foo Bar (name16) ...
 
     # Open a page again so that we see the cookie for a launchpad.test request
@@ -46,11 +46,11 @@ If we visit another vhost in the domain, we remain logged in.
 
     >>> root_url = BaseLayer.appserver_root_url()
     >>> browser.open(root_url)
-    >>> browser.vhost
-    'http://launchpad.test'
+    >>> print(browser.vhost)
+    http://launchpad.test
     >>> browser.urlpath
     '/'
-    >>> print extract_text(find_tag_by_id(browser.contents, 'logincontrol'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'logincontrol')))
     Foo Bar (name16) ...
     >>> browser.cookies.getinfo(session_cookie_name)['domain']
     '.launchpad.test'
@@ -58,11 +58,11 @@ If we visit another vhost in the domain, we remain logged in.
 Even if the browser passes in a cookie, the feeds vhost should not set one.
 
     >>> browser.open('%s/announcements.atom' % feeds_root_url)
-    >>> browser.vhost
-    'http://feeds.launchpad.test'
+    >>> print(browser.vhost)
+    http://feeds.launchpad.test
     >>> browser.urlpath
     '/announcements.atom'
-    >>> print browser.headers.get('Set-Cookie')
+    >>> print(browser.headers.get('Set-Cookie'))
     None
 
 # XXX stub 20060816 bug=56601: We need to test that for https: URLs, the
diff --git a/lib/lp/services/webapp/tests/login.txt b/lib/lp/services/webapp/tests/login.txt
index 5b81348..156b5d2 100644
--- a/lib/lp/services/webapp/tests/login.txt
+++ b/lib/lp/services/webapp/tests/login.txt
@@ -20,13 +20,13 @@ link, they'll be sent to the OP to authenticate.
     # On a browser with JS support, this page would've been automatically
     # submitted (thanks to the onload handler), but testbrowser doesn't support
     # JS, so we have to submit the form manually.
-    >>> print browser.contents
+    >>> print(browser.contents)
     <html>...<body onload="document.forms[0].submit();"...
     >>> browser.getControl('Continue').click()
 
 The OpenID Provider will ask us to authenticate.
 
-    >>> print browser.title
+    >>> print(browser.title)
     Login
     >>> from lp.services.webapp.tests.test_login import (
     ...     fill_login_form_and_submit)
@@ -35,19 +35,19 @@ The OpenID Provider will ask us to authenticate.
 Once authenticated, we're redirected back to the page where we started, with
 the query args preserved.
 
-    >>> browser.vhost
-    'http://launchpad.test'
+    >>> print(browser.vhost)
+    http://launchpad.test
     >>> browser.urlpath
     '/people'
     >>> import re
-    >>> print sorted(re.sub('.*\?', '', browser.url).split('&'))
+    >>> print(pretty(sorted(re.sub('.*\?', '', browser.url).split('&'))))
     ['name=foo', 'searchfor=all']
 
 If we load the +login page while already logged in, it will say we're already
 logged in and ask us to log out if we're somebody else.
 
     >>> browser.open('%s/+login' % root_url)
-    >>> print extract_text(find_main_content(browser.contents))
+    >>> print(extract_text(find_main_content(browser.contents)))
     You are already logged in...
 
 The same thing works if the user has non-ASCII characters in their display
@@ -69,16 +69,16 @@ name.
     >>> browser.open(
     ...     '%s/people/?name=foo&searchfor=all' % root_url)
     >>> browser.getLink('Log in / Register').click()
-    >>> print browser.contents
+    >>> print(browser.contents)
     <html>...<body onload="document.forms[0].submit();"...
     >>> browser.getControl('Continue').click()
-    >>> print browser.title
+    >>> print(browser.title)
     Login
     >>> fill_login_form_and_submit(browser, 'unicode@xxxxxxxxxxx')
-    >>> browser.vhost
-    'http://launchpad.test'
+    >>> print(browser.vhost)
+    http://launchpad.test
     >>> browser.urlpath
     '/people'
     >>> import re
-    >>> print sorted(re.sub('.*\?', '', browser.url).split('&'))
+    >>> print(pretty(sorted(re.sub('.*\?', '', browser.url).split('&'))))
     ['name=foo', 'searchfor=all']
diff --git a/lib/lp/services/webapp/tests/no-anonymous-session-cookies.txt b/lib/lp/services/webapp/tests/no-anonymous-session-cookies.txt
index 441d5e9..a7d8df5 100644
--- a/lib/lp/services/webapp/tests/no-anonymous-session-cookies.txt
+++ b/lib/lp/services/webapp/tests/no-anonymous-session-cookies.txt
@@ -23,14 +23,14 @@ Now let's log in and show that the session cookie is set.
     # On a browser with JS support, this page would've been automatically
     # submitted (thanks to the onload handler), but testbrowser doesn't
     # support JS, so we have to submit the form manually.
-    >>> print browser.contents
+    >>> print(browser.contents)
     <html>...<body onload="document.forms[0].submit();"...
     >>> browser.getControl('Continue').click()
 
     >>> from lp.services.webapp.tests.test_login import (
     ...     fill_login_form_and_submit)
     >>> fill_login_form_and_submit(browser, 'foo.bar@xxxxxxxxxxxxx')
-    >>> print extract_text(find_tag_by_id(browser.contents, 'logincontrol'))
+    >>> print(extract_text(find_tag_by_id(browser.contents, 'logincontrol')))
     Foo Bar (name16) ...
 
     # Open a page again so that we see the cookie for a launchpad.test request
diff --git a/lib/lp/services/webapp/tests/test_authentication.py b/lib/lp/services/webapp/tests/test_authentication.py
index a348b0b..ff8036d 100644
--- a/lib/lp/services/webapp/tests/test_authentication.py
+++ b/lib/lp/services/webapp/tests/test_authentication.py
@@ -100,5 +100,6 @@ def test_suite():
     suite = unittest.TestLoader().loadTestsFromName(__name__)
     suite.addTest(LayeredDocFileSuite(
         'test_launchpad_login_source.txt',
-        layer=LaunchpadFunctionalLayer, setUp=setUp, tearDown=tearDown))
+        layer=LaunchpadFunctionalLayer,
+        setUp=lambda test: setUp(test, future=True), tearDown=tearDown))
     return suite
diff --git a/lib/lp/services/webapp/tests/test_doc.py b/lib/lp/services/webapp/tests/test_doc.py
index 7af6c91..b4e50d8 100644
--- a/lib/lp/services/webapp/tests/test_doc.py
+++ b/lib/lp/services/webapp/tests/test_doc.py
@@ -29,7 +29,7 @@ here = os.path.dirname(os.path.realpath(__file__))
 special = {
     'canonical_url.txt': LayeredDocFileSuite(
         '../doc/canonical_url.txt',
-        setUp=setUp, tearDown=tearDown,
+        setUp=lambda test: setUp(test, future=True), tearDown=tearDown,
         layer=FunctionalLayer,),
     'notification-text-escape.txt': LayeredDocFileSuite(
         '../doc/notification-text-escape.txt',
@@ -38,23 +38,27 @@ special = {
         stdout_logging=False, layer=None),
     'test_adapter.txt': LayeredDocFileSuite(
         '../doc/test_adapter.txt',
-        setUp=setGlobs, layer=LaunchpadFunctionalLayer),
+        setUp=lambda test: setGlobs(test, future=True),
+        layer=LaunchpadFunctionalLayer),
 # XXX Julian 2009-05-13, bug=376171
 # Temporarily disabled because of intermittent failures.
 #    'test_adapter_timeout.txt': LayeredDocFileSuite(
 #        '../doc/test_adapter_timeout.txt',
-#        setUp=setUp,
+#        setUp=lambda test: setUp(test, future=True),
 #        tearDown=tearDown,
 #        layer=LaunchpadFunctionalLayer),
     'test_adapter_permissions.txt': LayeredDocFileSuite(
         '../doc/test_adapter_permissions.txt',
+        setUp=lambda test: setGlobs(test, future=True),
         layer=LaunchpadFunctionalLayer),
     'uri.txt': LayeredDocFileSuite(
         '../doc/uri.txt',
-        setUp=setUp, tearDown=tearDown,
+        setUp=lambda test: setUp(test, future=True), tearDown=tearDown,
         layer=FunctionalLayer),
     }
 
 
 def test_suite():
-    return build_test_suite(here, special, layer=LaunchpadFunctionalLayer)
+    return build_test_suite(
+        here, special, setUp=lambda test: setUp(test, future=True),
+        layer=LaunchpadFunctionalLayer)
diff --git a/lib/lp/services/webapp/tests/test_launchpad_login_source.txt b/lib/lp/services/webapp/tests/test_launchpad_login_source.txt
index 27a520c..b574c5d 100644
--- a/lib/lp/services/webapp/tests/test_launchpad_login_source.txt
+++ b/lib/lp/services/webapp/tests/test_launchpad_login_source.txt
@@ -8,7 +8,7 @@ person is found with the given email address, None is returned
     >>> from lp.services.webapp.authentication import (
     ...     LaunchpadLoginSource)
     >>> login_source = LaunchpadLoginSource()
-    >>> print login_source.getPrincipalByLogin('no-such-email@xxxxxxxxxxx')
+    >>> print(login_source.getPrincipalByLogin('no-such-email@xxxxxxxxxxx'))
     None
 
 Giving getPrincipalByLogin() an existing email address, returns a
diff --git a/lib/lp/services/webapp/tests/test_notifications.py b/lib/lp/services/webapp/tests/test_notifications.py
index d3f48ee..1b9b335 100644
--- a/lib/lp/services/webapp/tests/test_notifications.py
+++ b/lib/lp/services/webapp/tests/test_notifications.py
@@ -7,6 +7,7 @@ from __future__ import absolute_import, print_function
 
 __metaclass__ = type
 
+import __future__
 from doctest import DocTestSuite
 import unittest
 
@@ -86,6 +87,8 @@ def setUp(test):
         lambda x: mock_browser_request, (INotificationRequest,),
         IBrowserRequest)
 
+    for future_item in 'absolute_import', 'print_function':
+        test.globs[future_item] = getattr(__future__, future_item)
     test.globs['MockResponse'] = MockHTTPApplicationResponse
     test.globs['structured'] = structured
 
diff --git a/lib/lp/services/webapp/tests/test_preferredcharsets.py b/lib/lp/services/webapp/tests/test_preferredcharsets.py
index 0c5f57e..a350a3b 100644
--- a/lib/lp/services/webapp/tests/test_preferredcharsets.py
+++ b/lib/lp/services/webapp/tests/test_preferredcharsets.py
@@ -5,9 +5,13 @@
 
 __metaclass__ = type
 
-from lp.testing.systemdocs import LayeredDocFileSuite
+from lp.testing.systemdocs import (
+    LayeredDocFileSuite,
+    setGlobs,
+    )
 
 
 def test_suite():
     return LayeredDocFileSuite(
-        'test_preferredcharsets.txt')
+        'test_preferredcharsets.txt',
+        setUp=lambda test: setGlobs(test, future=True))
diff --git a/lib/lp/testing/browser.py b/lib/lp/testing/browser.py
index 7bacfbe..aed02bc 100644
--- a/lib/lp/testing/browser.py
+++ b/lib/lp/testing/browser.py
@@ -9,11 +9,14 @@ child process.  The Zope testing browser fakes its connections in-process, so
 that's not good enough.
 """
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 __all__ = [
     'setUp',
     ]
 
+import __future__
 import ssl
 
 from lazr.uri import URI
@@ -32,6 +35,7 @@ from lp.testing.pages import (
     find_tag_by_id,
     print_feedback_messages,
     )
+from lp.testing.systemdocs import PrettyPrinter
 
 
 class Browser(_Browser):
@@ -68,9 +72,12 @@ class Browser(_Browser):
 
 def setUp(test):
     """Set up appserver tests."""
+    for future_item in 'absolute_import', 'print_function', 'unicode_literals':
+        test.globs[future_item] = getattr(__future__, future_item)
     test.globs['Browser'] = Browser
     test.globs['browser'] = Browser()
     test.globs['find_tag_by_id'] = find_tag_by_id
     test.globs['find_main_content'] = find_main_content
     test.globs['print_feedback_messages'] = print_feedback_messages
     test.globs['extract_text'] = extract_text
+    test.globs['pretty'] = PrettyPrinter(width=1).pformat