← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:app-future-imports-prepare into launchpad:master

 

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

Commit message:
Prepare for __future__ imports in lp.app doctests

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

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

These are various non-mechanical fixes in preparation for converting the doctests under lp.app to Launchpad's preferred __future__ imports.  Most of these are to avoid problems with unicode_literals; the fix is normally either to use print() rather than testing a value's __repr__, or to use six.ensure_str to cause input data to be str rather than unicode where appropriate.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:app-future-imports-prepare into launchpad:master.
diff --git a/lib/lp/app/browser/doc/launchpad-search-pages.txt b/lib/lp/app/browser/doc/launchpad-search-pages.txt
index 8edcf03..49442e7 100644
--- a/lib/lp/app/browser/doc/launchpad-search-pages.txt
+++ b/lib/lp/app/browser/doc/launchpad-search-pages.txt
@@ -32,12 +32,17 @@ when there is no search text.
 
 When text is not None, the title indicates what was searched.
 
+    >>> from lp.services.encoding import wsgi_native_string
+
     >>> def getSearchView(form):
     ...     search_param_list = []
     ...     for name in sorted(form):
     ...         value = form[name]
-    ...         search_param_list.append('%s=%s' % (name, value))
-    ...     query_string = '&'.join(search_param_list)
+    ...         if isinstance(value, six.text_type):
+    ...             value = wsgi_native_string(value)
+    ...         search_param_list.append(b'%s=%s' % (
+    ...             wsgi_native_string(name), value))
+    ...     query_string = b'&'.join(search_param_list)
     ...     request = LaunchpadTestRequest(
     ...         SERVER_URL='https://launchpad.test/+search',
     ...         QUERY_STRING=query_string, form=form, PATH_INFO='/+search')
@@ -540,7 +545,7 @@ error. Also disable warnings, since we are tossing around malformed Unicode.
     >>> with warnings.catch_warnings():
     ...     warnings.simplefilter('ignore')
     ...     search_view = getSearchView(
-    ...         form={'field.text': '\xfe\xfckr\xfc'})
+    ...         form={'field.text': b'\xfe\xfckr\xfc'})
     >>> html = search_view()
     >>> 'Can not convert your search term' in html
     True
diff --git a/lib/lp/app/doc/displaying-numbers.txt b/lib/lp/app/doc/displaying-numbers.txt
index bc89a9f..fcae7bc 100644
--- a/lib/lp/app/doc/displaying-numbers.txt
+++ b/lib/lp/app/doc/displaying-numbers.txt
@@ -45,13 +45,13 @@ to how the Python "%f" string formatter works:
 For instance:
 
     >>> foo = 12345.67890
-    >>> test_tales('foo/fmt:float/7.2', foo=foo)
-    '12345.68'
+    >>> print(test_tales('foo/fmt:float/7.2', foo=foo))
+    12345.68
 
 Is the same as:
 
-    >>> "%7.2f" % foo
-    '12345.68'
+    >>> print("%7.2f" % foo)
+    12345.68
 
 Here's a set of exhaustive examples:
 
diff --git a/lib/lp/app/doc/displaying-paragraphs-of-text.txt b/lib/lp/app/doc/displaying-paragraphs-of-text.txt
index a2df0b4..1c48b85 100644
--- a/lib/lp/app/doc/displaying-paragraphs-of-text.txt
+++ b/lib/lp/app/doc/displaying-paragraphs-of-text.txt
@@ -485,7 +485,7 @@ we want to replace a variable number of spaces with the same number of
 
     >>> from lp.app.browser.stringformatter import FormattersAPI
     >>> import re
-    >>> matchobj = re.match('foo(.*)bar', 'fooX Ybar')
+    >>> matchobj = re.match('foo(.*)bar', six.ensure_str('fooX Ybar'))
     >>> matchobj.groups()
     ('X Y',)
     >>> FormattersAPI._substitute_matchgroup_for_spaces(matchobj)
@@ -499,9 +499,10 @@ First, let's try a match of nothing it understands.  This is a bug, so we get
 an AssertionError.
 
     >>> matchobj = re.match(
-    ...     ('(?P<bug>xxx)?(?P<faq>www)?(?P<url>yyy)?(?P<oops>zzz)?'
-    ...      '(?P<lpbranchurl>www)?(?P<clbug>vvv)?'),
-    ...     'fish')
+    ...     six.ensure_str(
+    ...         '(?P<bug>xxx)?(?P<faq>www)?(?P<url>yyy)?(?P<oops>zzz)?'
+    ...         '(?P<lpbranchurl>www)?(?P<clbug>vvv)?'),
+    ...     six.ensure_str('fish'))
     >>> sorted(matchobj.groupdict().items())
     [('bug', None),
      ('clbug', None),
@@ -517,7 +518,9 @@ an AssertionError.
 When we have a URL, the URL is made into a link.  A quote is added to the
 url to demonstrate quoting in the HTML attribute.
 
-    >>> matchobj = re.match('(?P<bug>xxx)?(?P<url>y"y)?', 'y"y')
+    >>> matchobj = re.match(
+    ...     six.ensure_str('(?P<bug>xxx)?(?P<url>y"y)?'),
+    ...     six.ensure_str('y"y'))
     >>> sorted(matchobj.groupdict().items())
     [('bug', None), ('url', 'y"y')]
     >>> FormattersAPI._linkify_substitution(matchobj)
@@ -527,7 +530,8 @@ When we have a bug reference, the 'bug' group is used as the text of the link,
 and the 'bugnum' is used to look up the bug.
 
     >>> matchobj = re.match(
-    ...     '(?P<bug>xxxx)?(?P<bugnum>2)?(?P<url>yyy)?', 'xxxx2')
+    ...     six.ensure_str('(?P<bug>xxxx)?(?P<bugnum>2)?(?P<url>yyy)?'),
+    ...     six.ensure_str('xxxx2'))
     >>> sorted(matchobj.groupdict().items())
     [('bug', 'xxxx'), ('bugnum', '2'), ('url', None)]
     >>> FormattersAPI._linkify_substitution(matchobj)
@@ -537,7 +541,8 @@ When the bugnum doesn't match any bug, we still get a link, but get a message
 in the link's title.
 
     >>> matchobj = re.match(
-    ...     '(?P<bug>xxxx)?(?P<bugnum>2000)?(?P<url>yyy)?', 'xxxx2000')
+    ...     six.ensure_str('(?P<bug>xxxx)?(?P<bugnum>2000)?(?P<url>yyy)?'),
+    ...     six.ensure_str('xxxx2000'))
     >>> sorted(matchobj.groupdict().items())
     [('bug', 'xxxx'), ('bugnum', '2000'), ('url', None)]
     >>> FormattersAPI._linkify_substitution(matchobj)
diff --git a/lib/lp/app/doc/launchpadform.txt b/lib/lp/app/doc/launchpadform.txt
index 6af879c..95bca7e 100644
--- a/lib/lp/app/doc/launchpadform.txt
+++ b/lib/lp/app/doc/launchpadform.txt
@@ -221,7 +221,7 @@ setFieldError() method (for errors specific to a field):
   ...             self.addError('your password may not be the same '
   ...                           'as your name')
   ...         if data.get('password') == 'password':
-  ...             self.setFieldError('password',
+  ...             self.setFieldError(six.ensure_str('password'),
   ...                                'your password must not be "password"')
 
   >>> context = FormTest()
diff --git a/lib/lp/app/doc/launchpadview.txt b/lib/lp/app/doc/launchpadview.txt
index b5617b8..07942aa 100644
--- a/lib/lp/app/doc/launchpadview.txt
+++ b/lib/lp/app/doc/launchpadview.txt
@@ -62,7 +62,7 @@ an IStructuredString implementation.
     >>> print view.info_message
     None
 
-    >>> view.error_message = 'A simple string.'
+    >>> view.error_message = six.ensure_str('A simple string.')
     Traceback (most recent call last):
     ...
     ValueError: <type 'str'> is not a valid value for error_message,
@@ -70,7 +70,7 @@ an IStructuredString implementation.
     >>> print view.error_message
     None
 
-    >>> view.info_message = 'A simple string.'
+    >>> view.info_message = six.ensure_str('A simple string.')
     Traceback (most recent call last):
     ...
     ValueError: <type 'str'> is not a valid value for info_message,
diff --git a/lib/lp/app/doc/loginstatus-pages.txt b/lib/lp/app/doc/loginstatus-pages.txt
index 16fceaf..a07e314 100644
--- a/lib/lp/app/doc/loginstatus-pages.txt
+++ b/lib/lp/app/doc/loginstatus-pages.txt
@@ -27,8 +27,8 @@ Generic request without query args.
     False
     >>> status.login_shown
     True
-    >>> status.login_url
-    'http://localhost/foo/bar/+login'
+    >>> print(status.login_url)
+    http://localhost/foo/bar/+login
 
 Virtual hosted request with a trailing slash.
 
@@ -37,8 +37,8 @@ Virtual hosted request with a trailing slash.
     ...     '/++vh++https:staging.example.com:433/++/foo/bar/', '')
     >>> context = object()
     >>> status = LoginStatus(context, request)
-    >>> status.login_url
-    'https://staging.example.com/foo/bar/+login'
+    >>> print(status.login_url)
+    https://staging.example.com/foo/bar/+login
 
 Virtual hosted request with no trailing slash.
 
@@ -47,8 +47,8 @@ Virtual hosted request with no trailing slash.
     ...     '/++vh++https:staging.example.com:433/++/foo/bar', '')
     >>> context = object()
     >>> status = LoginStatus(context, request)
-    >>> status.login_url
-    'https://staging.example.com/foo/bar/+login'
+    >>> print(status.login_url)
+    https://staging.example.com/foo/bar/+login
 
 Generic request with trailing slash and query parameters.
 
@@ -56,8 +56,8 @@ Generic request with trailing slash and query parameters.
     ...     'http://localhost', '/foo/bar/', 'x=1&y=2')
     >>> context = object()
     >>> status = LoginStatus(context, request)
-    >>> status.login_url
-    'http://localhost/foo/bar/+login?x=1&y=2'
+    >>> print(status.login_url)
+    http://localhost/foo/bar/+login?x=1&y=2
 
 The login page.
 
@@ -80,8 +80,8 @@ The logout page.
     False
     >>> status.login_shown
     True
-    >>> status.login_url
-    'http://localhost/+login'
+    >>> print(status.login_url)
+    http://localhost/+login
 
 The +openid-callback page.
 
@@ -93,8 +93,8 @@ The +openid-callback page.
     False
     >>> status.login_shown
     True
-    >>> status.login_url
-    'http://localhost/+login'
+    >>> print(status.login_url)
+    http://localhost/+login
 
 Logging in.
 
diff --git a/lib/lp/app/doc/menus.txt b/lib/lp/app/doc/menus.txt
index e982b25..e761f1b 100644
--- a/lib/lp/app/doc/menus.txt
+++ b/lib/lp/app/doc/menus.txt
@@ -33,7 +33,7 @@ later, implementations having facets and menus will be defined.
 
     >>> import sys
     >>> import types
-    >>> cookingexample = types.ModuleType('cookingexample')
+    >>> cookingexample = types.ModuleType(six.ensure_str('cookingexample'))
     >>> sys.modules['lp.app.cookingexample'] = cookingexample
 
     >>> cookingexample.ICookbook = ICookbook
diff --git a/lib/lp/app/doc/tales.txt b/lib/lp/app/doc/tales.txt
index cc7606e..682360a 100644
--- a/lib/lp/app/doc/tales.txt
+++ b/lib/lp/app/doc/tales.txt
@@ -266,8 +266,8 @@ fmt:rfc822utcdatetime.
 
 To truncate a long string, use fmt:shorten:
 
-    >>> test_tales('foo/fmt:shorten/8', foo='abcdefghij')
-    'abcde...'
+    >>> print(test_tales('foo/fmt:shorten/8', foo='abcdefghij'))
+    abcde...
 
 To ellipsize the middle of a string. use fmt:ellipsize and pass the max
 length.
@@ -1080,35 +1080,35 @@ the start of the string is not a letter. If any invalid characters are
 stripped out, to ensure the id is unique, a base64 encoding is appended to the
 id.
 
-    >>> test_tales('foo/fmt:css-id', foo='beta2-milestone')
-    'beta2-milestone'
+    >>> print(test_tales('foo/fmt:css-id', foo='beta2-milestone'))
+    beta2-milestone
 
-    >>> test_tales('foo/fmt:css-id', foo='user name')
-    'user-name-dXNlciBuYW1l'
+    >>> print(test_tales('foo/fmt:css-id', foo='user name'))
+    user-name-dXNlciBuYW1l
 
-    >>> test_tales('foo/fmt:css-id', foo='1.0.1_series')
-    'j1-0-1_series'
+    >>> print(test_tales('foo/fmt:css-id', foo='1.0.1_series'))
+    j1-0-1_series
 
 An optional prefix for the if can be added to the path. It too will be
 escaped.
 
-    >>> test_tales('foo/fmt:css-id/series-', foo='1.0.1_series')
-    'series-1-0-1_series'
+    >>> print(test_tales('foo/fmt:css-id/series-', foo='1.0.1_series'))
+    series-1-0-1_series
 
-    >>> test_tales('foo/fmt:css-id/series_', foo='1.0.1_series')
-    'series_1-0-1_series'
+    >>> print(test_tales('foo/fmt:css-id/series_', foo='1.0.1_series'))
+    series_1-0-1_series
 
-    >>> test_tales('foo/fmt:css-id/0series-', foo='1.0.1_series')
-    'j0series-1-0-1_series'
+    >>> print(test_tales('foo/fmt:css-id/0series-', foo='1.0.1_series'))
+    j0series-1-0-1_series
 
 Zope fields are rendered with a period, and we need to ensure there is a way
 to retain the periods in the css id even though we would prefer not to.
 
-    >>> test_tales('foo/fmt:zope-css-id', foo='field.bug.target')
-    'field.bug.target'
+    >>> print(test_tales('foo/fmt:zope-css-id', foo='field.bug.target'))
+    field.bug.target
 
-    >>> test_tales('foo/fmt:zope-css-id', foo='field.gtk+_package')
-    'field.gtk-_package-ZmllbGQuZ3RrK19wYWNrYWdl'
+    >>> print(test_tales('foo/fmt:zope-css-id', foo='field.gtk+_package'))
+    field.gtk-_package-ZmllbGQuZ3RrK19wYWNrYWdl
 
 The fmt: namespace to get strings (obfuscation)
 -----------------------------------------------
@@ -1119,32 +1119,36 @@ unauthenticated users, the email address can be hidden. The address is
 replaced with the message '<email address hidden>'.
 
     >>> login(ANONYMOUS)
-    >>> test_tales('foo/fmt:obfuscate-email', foo='name.surname@xxxxxxxxxxx')
-    '<email address hidden>'
+    >>> print(test_tales(
+    ...     'foo/fmt:obfuscate-email', foo='name.surname@xxxxxxxxxxx'))
+    <email address hidden>
 
-    >>> test_tales('foo/fmt:obfuscate-email', foo='name@xxxxxxxxxxxxxxxxxxx')
-    '<email address hidden>'
+    >>> print(test_tales(
+    ...     'foo/fmt:obfuscate-email', foo='name@xxxxxxxxxxxxxxxxxxx'))
+    <email address hidden>
 
-    >>> test_tales('foo/fmt:obfuscate-email', foo='name+sub@xxxxxxxxxx')
-    '<email address hidden>'
+    >>> print(test_tales(
+    ...     'foo/fmt:obfuscate-email', foo='name+sub@xxxxxxxxxx'))
+    <email address hidden>
 
-    >>> test_tales('foo/fmt:obfuscate-email',
-    ...     foo='long_name@xxxxxxxxxxxxxxxxxxxxxxxx')
-    '<email address hidden>'
+    >>> print(test_tales('foo/fmt:obfuscate-email',
+    ...     foo='long_name@xxxxxxxxxxxxxxxxxxxxxxxx'))
+    <email address hidden>
 
-    >>> test_tales('foo/fmt:obfuscate-email',
-    ...     foo='"long/name="@organization.org')
-    '"<email address hidden>'
+    >>> print(test_tales('foo/fmt:obfuscate-email',
+    ...     foo='"long/name="@organization.org'))
+    "<email address hidden>
 
-    >>> test_tales('foo/fmt:obfuscate-email',
-    ...     foo='long-name@building.museum')
-    '<email address hidden>'
+    >>> print(test_tales('foo/fmt:obfuscate-email',
+    ...     foo='long-name@building.museum'))
+    <email address hidden>
 
-    >>> test_tales('foo/fmt:obfuscate-email', foo='foo@xxxxxxxxxxxxxxxx')
-    '<email address hidden>'
+    >>> print(test_tales(
+    ...     'foo/fmt:obfuscate-email', foo='foo@xxxxxxxxxxxxxxxx'))
+    <email address hidden>
 
-    >>> test_tales('foo/fmt:obfuscate-email', foo='<foo@xxxxxxx>')
-    '<email address hidden>'
+    >>> print(test_tales('foo/fmt:obfuscate-email', foo='<foo@xxxxxxx>'))
+    <email address hidden>
 
     >>> print test_tales('foo/fmt:obfuscate-email/fmt:text-to-html',
     ...     foo=signature)
@@ -1153,41 +1157,43 @@ replaced with the message '<email address hidden>'.
     &lt;email address hidden&gt;<br />
     Guilty of stealing everything I am.</p>
 
-    >>> test_tales('foo/fmt:obfuscate-email',
-    ...     foo='mailto:long-name@xxxxxxxxxxxxxxxx')
-    'mailto:<email address hidden>'
+    >>> print(test_tales('foo/fmt:obfuscate-email',
+    ...     foo='mailto:long-name@xxxxxxxxxxxxxxxx'))
+    mailto:<email address hidden>
 
-    >>> test_tales('foo/fmt:obfuscate-email',
-    ...     foo='http://person:password@xxxxxxxx')
-    'http://person:<email address hidden>'
+    >>> print(test_tales('foo/fmt:obfuscate-email',
+    ...     foo='http://person:password@xxxxxxxx'))
+    http://person:<email address hidden>
 
-    >>> test_tales('foo/fmt:obfuscate-email', foo='name @ host.school.edu')
-    'name @ host.school.edu'
+    >>> print(test_tales(
+    ...     'foo/fmt:obfuscate-email', foo='name @ host.school.edu'))
+    name @ host.school.edu
 
-    >>> test_tales('foo/fmt:obfuscate-email', foo='person@host')
-    'person@host'
+    >>> print(test_tales('foo/fmt:obfuscate-email', foo='person@host'))
+    person@host
 
-    >>> test_tales('foo/fmt:obfuscate-email', foo='(head, tail)=@array')
-    '(head, tail)=@array'
+    >>> print(test_tales(
+    ...     'foo/fmt:obfuscate-email', foo='(head, tail)=@array'))
+    (head, tail)=@array
 
-    >>> test_tales('foo/fmt:obfuscate-email', foo='@staticmethod')
-    '@staticmethod'
+    >>> print(test_tales('foo/fmt:obfuscate-email', foo='@staticmethod'))
+    @staticmethod
 
-    >>> test_tales('foo/fmt:obfuscate-email', foo='element/@attribute')
-    'element/@attribute'
+    >>> print(test_tales('foo/fmt:obfuscate-email', foo='element/@attribute'))
+    element/@attribute
 
     >>> bad_address = (
     ...     "medicalwei@sara:~$ Spinning................................"
     ...     "...........................................................not")
-    >>> test_tales('foo/fmt:obfuscate-email', foo=bad_address)
-    'medicalwei@sara:~$ ...'
+    >>> print(test_tales('foo/fmt:obfuscate-email', foo=bad_address))
+    medicalwei@sara:~$ ...
 
 However, if the user is authenticated, the email address is not
 obfuscated.
 
     >>> login('no-priv@xxxxxxxxxxxxx')
-    >>> test_tales('foo/fmt:obfuscate-email', foo='user@xxxxxxxx')
-    'user@xxxxxxxx'
+    >>> print(test_tales('foo/fmt:obfuscate-email', foo='user@xxxxxxxx'))
+    user@xxxxxxxx
 
 
 Linkification of email addresses
@@ -1220,8 +1226,8 @@ Team addresses are linkified with a team icon:
 
 Unknown email addresses are not altered in any way:
 
-    >>> test_tales('foo/fmt:linkify-email', foo='nobody@xxxxxxxxxxx')
-    'nobody@xxxxxxxxxxx'
+    >>> print(test_tales('foo/fmt:linkify-email', foo='nobody@xxxxxxxxxxx'))
+    nobody@xxxxxxxxxxx
 
 Users who specify that their email addresses must be hidden also do not
 get linkified.  test@xxxxxxxxxxxxx is hidden:
@@ -1231,8 +1237,8 @@ get linkified.  test@xxxxxxxxxxxxx is hidden:
     >>> discreet_user.hide_email_addresses
     True
 
-    >>> test_tales('foo/fmt:linkify-email', foo='test@xxxxxxxxxxxxx')
-    'test@xxxxxxxxxxxxx'
+    >>> print(test_tales('foo/fmt:linkify-email', foo='test@xxxxxxxxxxxxx'))
+    test@xxxxxxxxxxxxx
 
 
 Test the 'fmt:' namespace where the context is a dict.
diff --git a/lib/lp/app/doc/textformatting.txt b/lib/lp/app/doc/textformatting.txt
index cda72a0..3d15a5b 100644
--- a/lib/lp/app/doc/textformatting.txt
+++ b/lib/lp/app/doc/textformatting.txt
@@ -230,14 +230,14 @@ The line endings are normalized to \n, so if we get a text with
 dos-style line endings, we get the following result:
 
     >>> mailwrapper = MailWrapper(width=56)
-    >>> dos_style_comment = (
+    >>> dos_style_comment = six.ensure_str(
     ...     "This paragraph is longer than 56 characters, so it should"
     ...     " be wrapped even though the paragraphs are separated with"
     ...     " dos-style line endings."
     ...     "\r\n\r\n"
     ...     "Here's the second paragraph.")
     >>> wrapped_text = mailwrapper.format(dos_style_comment)
-    >>> wrapped_text.split('\n')
+    >>> wrapped_text.split(six.ensure_str('\n'))
     ['This paragraph is longer than 56 characters, so it',
      'should be wrapped even though the paragraphs are',
      'separated with dos-style line endings.',
diff --git a/lib/lp/app/tests/test_doc.py b/lib/lp/app/tests/test_doc.py
index 495499b..3e93aee 100644
--- a/lib/lp/app/tests/test_doc.py
+++ b/lib/lp/app/tests/test_doc.py
@@ -16,6 +16,7 @@ from lp.testing.pages import (
     )
 from lp.testing.systemdocs import (
     LayeredDocFileSuite,
+    setGlobs,
     setUp,
     tearDown,
     )
@@ -42,7 +43,7 @@ special = {
         layer=LaunchpadFunctionalLayer,
         ),
     'menus.txt': LayeredDocFileSuite(
-        '../doc/menus.txt', layer=None,
+        '../doc/menus.txt', setUp=setGlobs, layer=None,
         ),
     'stories/launchpad-search(Bing)': PageTestSuite(
         '../stories/launchpad-search/',
diff --git a/lib/lp/app/validators/name.py b/lib/lp/app/validators/name.py
index 307795b..5b2575f 100644
--- a/lib/lp/app/validators/name.py
+++ b/lib/lp/app/validators/name.py
@@ -28,10 +28,10 @@ def sanitize_name(name):
     The characters not allowed in Launchpad names are described by
     invalid_name_pattern.
 
-    >>> sanitize_name('foo_bar')
-    'foobar'
-    >>> sanitize_name('baz bar $fd')
-    'bazbarfd'
+    >>> print(sanitize_name('foo_bar'))
+    foobar
+    >>> print(sanitize_name('baz bar $fd'))
+    bazbarfd
     """
     return invalid_name_pattern.sub('', name)
 
diff --git a/lib/lp/app/validators/username.py b/lib/lp/app/validators/username.py
index cce4d4b..acfe874 100644
--- a/lib/lp/app/validators/username.py
+++ b/lib/lp/app/validators/username.py
@@ -27,12 +27,12 @@ def sanitize_username(username):
     The characters not allowed in Launchpad usernames are described by
     `username_invalid_pattern`.
 
-    >>> sanitize_username('foo_bar')
-    'foobar'
-    >>> sanitize_username('foo.bar+baz')
-    'foobarbaz'
-    >>> sanitize_username('-#foo -$fd?.0+-')
-    'foo-fd0'
+    >>> print(sanitize_username('foo_bar'))
+    foobar
+    >>> print(sanitize_username('foo.bar+baz'))
+    foobarbaz
+    >>> print(sanitize_username('-#foo -$fd?.0+-'))
+    foo-fd0
 
     """
     return username_invalid_pattern.sub('', username)
diff --git a/lib/lp/app/widgets/doc/announcement-date-widget.txt b/lib/lp/app/widgets/doc/announcement-date-widget.txt
index becbaad..d65b4f2 100644
--- a/lib/lp/app/widgets/doc/announcement-date-widget.txt
+++ b/lib/lp/app/widgets/doc/announcement-date-widget.txt
@@ -8,7 +8,7 @@ future, or to manually publish it later.
     >>> from lp.testing.pages import extract_text
     >>> from lp.services.webapp.servers import LaunchpadTestRequest
     >>> from lp.app.widgets.announcementdate import AnnouncementDateWidget
-    >>> field = Field(__name__='foo', title=u'Foo')
+    >>> field = Field(__name__=six.ensure_str('foo'), title=u'Foo')
     >>> widget = AnnouncementDateWidget(field, LaunchpadTestRequest())
     >>> print extract_text(widget())
     Publish this announcement:
diff --git a/lib/lp/app/widgets/doc/project-scope-widget.txt b/lib/lp/app/widgets/doc/project-scope-widget.txt
index e497797..0a87803 100644
--- a/lib/lp/app/widgets/doc/project-scope-widget.txt
+++ b/lib/lp/app/widgets/doc/project-scope-widget.txt
@@ -14,7 +14,8 @@ selected.
 
     >>> empty_request = LaunchpadTestRequest()
     >>> scope_field = Choice(
-    ...     __name__='scope', vocabulary='ProjectGroup', required=False)
+    ...     __name__=six.ensure_str('scope'), vocabulary='ProjectGroup',
+    ...     required=False)
     >>> scope_field = scope_field.bind(object())
     >>> widget = ProjectScopeWidget(
     ...     scope_field, scope_field.vocabulary, empty_request)
diff --git a/lib/lp/app/widgets/doc/stripped-text-widget.txt b/lib/lp/app/widgets/doc/stripped-text-widget.txt
index 26f3b06..fe9ed47 100644
--- a/lib/lp/app/widgets/doc/stripped-text-widget.txt
+++ b/lib/lp/app/widgets/doc/stripped-text-widget.txt
@@ -15,8 +15,8 @@ set value.
     >>> thing = Thing('abc')
 
     >>> non_required_field.set(thing, '   egf   ')
-    >>> non_required_field.get(thing)
-    'egf'
+    >>> print(non_required_field.get(thing))  # doctest: -NORMALIZE_WHITESPACE
+    egf
 
 None is an accepted field value.
 
@@ -57,7 +57,7 @@ provided.
   True
 
   >>> required_field = StrippedTextLine(
-  ...     __name__='field', title=u'Title',  required=True)
+  ...     __name__=six.ensure_str('field'), title=u'Title', required=True)
   >>> request = LaunchpadTestRequest(form={'field.field':'    \n    '})
   >>> widget = StrippedTextWidget(required_field, request)
   >>> widget.getInputValue()
diff --git a/lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt b/lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt
index 7d1aade..48ef775 100644
--- a/lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt
+++ b/lib/lp/bugs/stories/bugs/xx-incomplete-bugs.txt
@@ -261,8 +261,6 @@ Since no new comments have been added after we changed the status to
 Incomplete, we can now find that bug searching for Incomplete (without
 response) bugs.
 
-    >>> import six
-
     >>> user_browser.open(
     ...     'http://bugs.launchpad.test/jokosher/+bugs?advanced=1')
     >>> user_browser.getControl(name='field.status:list').value = (
diff --git a/lib/lp/registry/stories/teammembership/xx-teammembership.txt b/lib/lp/registry/stories/teammembership/xx-teammembership.txt
index 06208ac..873a275 100644
--- a/lib/lp/registry/stories/teammembership/xx-teammembership.txt
+++ b/lib/lp/registry/stories/teammembership/xx-teammembership.txt
@@ -224,8 +224,6 @@ On a team's +members page we can see all active members of that team, as
 well as the former members and the ones which proposed themselves or that
 have been invited.
 
-    >>> import six
-
     >>> def print_members(contents, type):
     ...     table = find_tag_by_id(contents, type)
     ...     for link in table.findAll('a'):
diff --git a/lib/lp/testing/pages.py b/lib/lp/testing/pages.py
index e552b4b..6fddbc1 100644
--- a/lib/lp/testing/pages.py
+++ b/lib/lp/testing/pages.py
@@ -936,6 +936,7 @@ def setUpGlobs(test, future=False):
     test.globs['print_tag_with_id'] = print_tag_with_id
     test.globs['PageTestLayer'] = PageTestLayer
     test.globs['stop'] = stop
+    test.globs['six'] = six
 
     if future:
         import __future__
diff --git a/lib/lp/testing/systemdocs.py b/lib/lp/testing/systemdocs.py
index d609bc8..3fb72bf 100644
--- a/lib/lp/testing/systemdocs.py
+++ b/lib/lp/testing/systemdocs.py
@@ -24,6 +24,7 @@ import pdb
 import pprint
 import sys
 
+import six
 import transaction
 from zope.component import getUtility
 from zope.testing.loggingsupport import Handler
@@ -234,6 +235,7 @@ def setGlobs(test, future=False):
     test.globs['launchpadlib_for'] = launchpadlib_for
     test.globs['launchpadlib_credentials_for'] = launchpadlib_credentials_for
     test.globs['oauth_access_token_for'] = oauth_access_token_for
+    test.globs['six'] = six
 
     if future:
         import __future__