← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:py3-tag-decode-contents into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:py3-tag-decode-contents into launchpad:master.

Commit message:
Replace Tag.renderContents with Tag.decode_contents

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

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

renderContents was the Beautiful Soup 3 spelling of encode_contents, which returns bytes; we always prefer to get text instead, so use decode_contents.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:py3-tag-decode-contents into launchpad:master.
diff --git a/lib/lp/answers/stories/answer-contact-report.txt b/lib/lp/answers/stories/answer-contact-report.txt
index 003c42e..9900bd2 100644
--- a/lib/lp/answers/stories/answer-contact-report.txt
+++ b/lib/lp/answers/stories/answer-contact-report.txt
@@ -14,7 +14,7 @@ Since No Privileges Person is not an answer contact, the report states
 that.
 
     >>> content = find_main_content(anon_browser.contents)
-    >>> print(content.find('p').renderContents())
+    >>> print(content.find('p').decode_contents())
     No Privileges Person is not an answer contact for any project.
 
 But when the person is an answer contact, the page displays the project
diff --git a/lib/lp/answers/stories/distribution-package-answer-contact.txt b/lib/lp/answers/stories/distribution-package-answer-contact.txt
index b9d9488..45a8afb 100644
--- a/lib/lp/answers/stories/distribution-package-answer-contact.txt
+++ b/lib/lp/answers/stories/distribution-package-answer-contact.txt
@@ -88,5 +88,5 @@ Again a confirmation message is displayed.
 
     >>> browser.getControl('Continue').click()
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     You have been removed as an answer contact for evolution in Ubuntu.
diff --git a/lib/lp/answers/stories/faq-browse-and-search.txt b/lib/lp/answers/stories/faq-browse-and-search.txt
index 4dd94b5..8826620 100644
--- a/lib/lp/answers/stories/faq-browse-and-search.txt
+++ b/lib/lp/answers/stories/faq-browse-and-search.txt
@@ -137,7 +137,7 @@ Following the link will show the questions results:
 
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Installation of Java Runtime Environment for Mozilla
 
 On the question page, there is also a link to consult the FAQs matching
diff --git a/lib/lp/answers/stories/project-add-question.txt b/lib/lp/answers/stories/project-add-question.txt
index cb4a320..ecd8271 100644
--- a/lib/lp/answers/stories/project-add-question.txt
+++ b/lib/lp/answers/stories/project-add-question.txt
@@ -78,7 +78,7 @@ that they submitted:
     >>> similar_questions = find_tag_by_id(
     ...     user_browser.contents, 'similar-questions')
     >>> for row in similar_questions.findAll('li'):
-    ...     print(row.a.renderContents())
+    ...     print(row.a.decode_contents())
     2: Problem showing the SVG demo on W3C site
 
 No Privileged Person can still change the product for which they're asking
@@ -98,7 +98,7 @@ An error message in the page informs the user that the summary is
 missing:
 
     >>> soup = find_main_content(user_browser.contents)
-    >>> print(soup.find('div', 'message').renderContents())
+    >>> print(soup.find('div', 'message').decode_contents())
     You must enter a summary of your problem.
 
 The product Thunderbird that they selected on the previous screen is still
@@ -117,7 +117,7 @@ Thunderbird. Since there are no similar questions against Thunderbird,
 an appropriate message is displayed to inform them of this:
 
     >>> soup = find_main_content(user_browser.contents)
-    >>> print(soup.find('p').renderContents())
+    >>> print(soup.find('p').decode_contents())
     There are no existing FAQs or questions similar to the summary you
     entered.
 
@@ -177,7 +177,7 @@ speaks Japanese, so we will use him.
     >>> daf_browser.getControl('Continue').click()
     >>> content = find_main_content(daf_browser.contents)
     >>> for message in content.findAll('div', 'informational message'):
-    ...      print(message.renderContents())
+    ...      print(message.decode_contents())
     You have been added as an answer contact for Mozilla Thunderbird.
 
 And we add Japanese to No Privileges Person's preferred languages. We
@@ -193,7 +193,7 @@ match. This condition demonstrates the supported language behaviour.
     >>> user_browser.getControl('Japanese').selected = True
     >>> user_browser.getControl('Save').click()
     >>> soup = find_main_content(user_browser.contents)
-    >>> print(soup.find('div', 'informational message').renderContents())
+    >>> print(soup.find('div', 'informational message').decode_contents())
     Added Japanese to your preferred languages.
 
 So if No Privileges Person were to visit the Ask a Question page for
diff --git a/lib/lp/answers/stories/question-add-in-other-languages.txt b/lib/lp/answers/stories/question-add-in-other-languages.txt
index 61c02c2..55c7538 100644
--- a/lib/lp/answers/stories/question-add-in-other-languages.txt
+++ b/lib/lp/answers/stories/question-add-in-other-languages.txt
@@ -50,12 +50,12 @@ understood by any member of the support community.
     already finds nothing.
 
     #>>> for row in similar_questions.findAll('tr', 'noted'):
-    #...     row.find('a').renderContents()
+    #...     row.find('a').decode_contents()
     #'Installation of Java Runtime Environment for Mozilla'
     #'Problema al recompilar kernel con soporte smp (doble-n\xc3\xbacleo)'
 
     >>> for tag in find_tags_by_class(browser.contents, 'warning message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     <strong>Portuguese (Brazil) (pt_BR)</strong> doesn't seem to be
     a language spoken by any answer contacts in this community. If you
     go ahead and ask a question in that language, no answer
@@ -129,7 +129,7 @@ Welsh, we'll have to warn them.
     'http://launchpad.test/ubuntu/+addquestion'
 
     >>> for tag in find_tags_by_class(browser.contents, 'warning message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     <strong>Welsh (cy)</strong> doesn't seem to be
     a language spoken by any answer contacts in this community. If you
     go ahead and ask a question in that language, no answer
@@ -147,7 +147,7 @@ display the warning again.
     >>> browser.getControl('Post Question').click()
 
     >>> for tag in find_tags_by_class(browser.contents, 'warning message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     <strong>Japanese (ja)</strong> doesn't seem to be
     a language spoken by any answer contacts in this community. If you
     go ahead and ask a question in that language, no answer
diff --git a/lib/lp/answers/stories/question-answer-contact.txt b/lib/lp/answers/stories/question-answer-contact.txt
index 4d63275..cdaa6a5 100644
--- a/lib/lp/answers/stories/question-answer-contact.txt
+++ b/lib/lp/answers/stories/question-answer-contact.txt
@@ -83,7 +83,7 @@ from the answer contact list.
 A confirmation message is displayed:
 
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Landscape Developers has been removed as an answer contact for Ubuntu.
 
 
diff --git a/lib/lp/answers/stories/question-browse-and-search.txt b/lib/lp/answers/stories/question-browse-and-search.txt
index 8ce1c90..6d587ee 100644
--- a/lib/lp/answers/stories/question-browse-and-search.txt
+++ b/lib/lp/answers/stories/question-browse-and-search.txt
@@ -34,7 +34,7 @@ He discovers that there are no questions on the Kubuntu Answers page:
     >>> print(browser.title)
     Questions : Kubuntu
 
-    >>> print(find_main_content(browser.contents).find('p').renderContents())
+    >>> print(find_main_content(browser.contents).find('p').decode_contents())
     There are no questions for Kubuntu with the requested statuses.
 
 For projects that don't have products, the Answers facet is disabled.
@@ -58,7 +58,7 @@ He sees a listing of the current questions posted on Ubuntu:
 
     >>> soup = find_main_content(browser.contents)
     >>> for question in soup.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Continue playing after shutdown
     Play DVDs in Totem
     mailto: problem in webpage
@@ -74,7 +74,7 @@ results. There, he finds only one other question:
     Questions : Ubuntu
     >>> soup = find_main_content(browser.contents)
     >>> for question in soup.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Installation failed
 
 This is the last results page, so the next and last links are greyed
@@ -179,7 +179,7 @@ is shown search results instead of the question.
     >>> print(browser.title)
     Questions matching "question 8"
 
-    >>> print(find_main_content(browser.contents).find('p').renderContents())
+    >>> print(find_main_content(browser.contents).find('p').decode_contents())
     There are no questions matching "question 8" with the requested statuses.
 
 
@@ -225,7 +225,7 @@ This time, the search returns one item.
 
     >>> soup = find_main_content(browser.contents)
     >>> for question in soup.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Firefox is slow and consumes too much RAM
 
 He clicks on the link to read the question description.
@@ -241,7 +241,7 @@ error is displayed when the user forgets to select a status.
     >>> browser.getControl(name='field.status').displayValue = []
     >>> browser.getControl('Search', index=0).click()
     >>> messages = find_tags_by_class(browser.contents, 'message')
-    >>> print(messages[0].renderContents())
+    >>> print(messages[0].decode_contents())
     You must choose at least one status.
 
 
@@ -300,7 +300,7 @@ recent questions on Mozilla Firefox.
     Questions : Mozilla Firefox
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Firefox loses focus and gets stuck
     Problem showing the SVG demo on W3C site
     Firefox cannot render Bank Site
@@ -320,7 +320,7 @@ problems:
     >>> browser.getControl('Search', index=0).click()
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Problem showing the SVG demo on W3C site
 
 
@@ -340,7 +340,7 @@ questions.)
     ['Answered', 'Solved']
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Play DVDs in Totem
     mailto: problem in webpage
     Installation of Java Runtime Environment for Mozilla
@@ -385,7 +385,7 @@ They need to login to access that page:
     >>> questions = find_tag_by_id(
     ...     sample_person_browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     mailto: problem in webpage
     Installation of Java Runtime Environment for Mozilla
 
@@ -407,7 +407,7 @@ The exact question they were searching for is displayed!
     >>> questions = find_tag_by_id(
     ...     sample_person_browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     mailto: problem in webpage
 
 If the user didn't make any questions on the product, a message
@@ -425,7 +425,7 @@ informing them of this fact is displayed.
     ...     'http://launchpad.test/gnomebaker/+questions')
     >>> sample_person_browser.getLink('My questions').click()
     >>> print(find_main_content(
-    ...     sample_person_browser.contents).find('p').renderContents())
+    ...     sample_person_browser.contents).find('p').decode_contents())
     You didn't ask any questions about gnomebaker.
 
 
@@ -453,7 +453,7 @@ They need to login to access that page:
     >>> questions = find_tag_by_id(
     ...     sample_person_browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Play DVDs in Totem
     Installation of Java Runtime Environment for Mozilla
 
@@ -477,7 +477,7 @@ informing them of this fact is displayed.
     ...    'http://launchpad.test/products/gnomebaker/+questions')
     >>> sample_person_browser.getLink('Need attention').click()
     >>> print(find_main_content(
-    ...     sample_person_browser.contents).find('p').renderContents())
+    ...     sample_person_browser.contents).find('p').decode_contents())
     No questions need your attention for gnomebaker.
 
 
@@ -498,7 +498,7 @@ commented on.
 
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Continue playing after shutdown
     Play DVDs in Totem
     mailto: problem in webpage
@@ -514,7 +514,7 @@ The listing contains a 'In' column that shows the context where the
 questions was made.
 
     >>> for question in questions.findAll('td', 'question-target'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Ubuntu
     Ubuntu
     mozilla-firefox in Ubuntu
@@ -538,7 +538,7 @@ questions to a particular status:
     >>> browser.getControl('Search', index=0).click()
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Firefox is slow and consumes too much RAM
     mailto: problem in webpage
 
@@ -554,7 +554,7 @@ assigned.
     >>> browser.getLink('Assigned').click()
     >>> print(browser.title)
     Questions for Foo Bar : Questions : Foo Bar
-    >>> print(find_main_content(browser.contents).find('p').renderContents())
+    >>> print(find_main_content(browser.contents).find('p').decode_contents())
     No questions assigned to Foo Bar found with the requested statuses.
 
 
@@ -568,7 +568,7 @@ answerer.
     Questions for Foo Bar : Questions : Foo Bar
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     mailto: problem in webpage
 
 
@@ -582,7 +582,7 @@ questions commented on by the person.
     Questions for Foo Bar : Questions : Foo Bar
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Continue playing after shutdown
     Play DVDs in Totem
     mailto: problem in webpage
@@ -600,7 +600,7 @@ asked by the person.
     Questions for Foo Bar : Questions : Foo Bar
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Slow system
     Firefox loses focus and gets stuck
 
@@ -615,7 +615,7 @@ the attention of that person.
     Questions for Foo Bar : Questions : Foo Bar
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Continue playing after shutdown
     Slow system
 
@@ -630,7 +630,7 @@ visiting the 'Subscribed' link in the 'Answers' facet.
     Questions for Foo Bar : Questions : Foo Bar
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Slow system
 
 
@@ -651,9 +651,9 @@ there is an 'In' column displaying where the questions were filed.
     ...     questions = find_tag_by_id(contents, 'question-listing')
     ...     for question in questions.tbody.findAll('tr'):
     ...         question_title = question.find(
-    ...             'td', 'questionTITLE').find('a').renderContents()
+    ...             'td', 'questionTITLE').find('a').decode_contents()
     ...         question_target = question.find(
-    ...             'td', 'question-target').find('a').renderContents()
+    ...             'td', 'question-target').find('a').decode_contents()
     ...         print(question_title, question_target)
     >>> print_questions_with_target(browser.contents)
     Newly installed plug-in doesn't seem to be used Mozilla Firefox
@@ -668,7 +668,7 @@ That listing is searchable:
 
     >>> questions = find_tag_by_id(browser.contents, 'question-listing')
     >>> for question in questions.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Problem showing the SVG demo on W3C site
 
 The same standard reports than on regular QuestionTarget are available:
@@ -729,7 +729,7 @@ When no results are found, a message informs the user of this fact:
     >>> browser.getControl('Search', index=0).click()
 
     >>> print(find_main_content(
-    ...     browser.contents).find('p').renderContents())
+    ...     browser.contents).find('p').decode_contents())
     There are no questions matching "firefox" with the requested statuses.
 
 Clicking the 'Search' button without entering any search text will
@@ -760,7 +760,7 @@ They must enter the project's name in the text field:
     >>> anon_browser.getControl('Find Answers').click()
 
     >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     Please enter a project name
 
 Entering an invalid project also displays an error message:
@@ -769,7 +769,7 @@ Entering an invalid project also displays an error message:
     >>> anon_browser.getControl('Find Answers').click()
 
     >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There is no project named 'invalid' registered in Launchpad
 
 If the browser supports javascript, there is a 'Choose' link available
diff --git a/lib/lp/answers/stories/question-edit.txt b/lib/lp/answers/stories/question-edit.txt
index 545c128..3f7a2bc 100644
--- a/lib/lp/answers/stories/question-edit.txt
+++ b/lib/lp/answers/stories/question-edit.txt
@@ -38,11 +38,11 @@ When we post the form, we should be redirected back to the question page.
 And viewing that page should show the updated information.
 
     >>> soup = find_main_content(test_browser.contents)
-    >>> print(soup.find('div', 'report').renderContents().strip())
+    >>> print(soup.find('div', 'report').decode_contents().strip())
     <p>Hi! I'm trying to learn about SVG but I can't get it to
     work at all in firefox. Maybe there is a plugin? Help! Thanks.
     Mark</p>
-    >>> print(soup.find('h1').renderContents())
+    >>> print(soup.find('h1').decode_contents())
     Problem showing the SVG demo on W3C web site
 
 You can even modify the title and description of 'Answered' and
diff --git a/lib/lp/answers/stories/question-obfuscation.txt b/lib/lp/answers/stories/question-obfuscation.txt
index 927268a..6588a40 100644
--- a/lib/lp/answers/stories/question-obfuscation.txt
+++ b/lib/lp/answers/stories/question-obfuscation.txt
@@ -36,10 +36,10 @@ in the question's description.
 
     >>> user_browser.getLink('mailto: problem in webpage').click()
     >>> description = find_main_content(user_browser.contents).p
-    >>> description.renderContents()
-    'I am not able to open my email client if i click on a
-     <a href="mailto:user@xxxxxxxxxx"; ...>mailto:...user@xxxxxxxxxxxx</a>
-     link ...'
+    >>> print(description.decode_contents())
+    I am not able to open my email client if i click on a
+    <a href="mailto:user@xxxxxxxxxx"; ...>mailto:...user@xxxxxxxxxxxx</a>
+    link ...
 
 No Privileges Person can see email addresses in the FAQ's
 Related question's portlet.
@@ -133,9 +133,9 @@ They cannot see the address reading the question either.
     False
 
     >>> description = find_main_content(anon_browser.contents).p
-    >>> description.renderContents()
-    'I am not able to open my email client if i click on a
-    mailto:&lt;email address hidden&gt; link ...'
+    >>> print(description.decode_contents())
+    I am not able to open my email client if i click on a
+    mailto:&lt;email address hidden&gt; link ...
 
 Anonymous users cannot see the email addresses in the Related
 questions portlet on a FAQ page.
diff --git a/lib/lp/answers/stories/question-overview.txt b/lib/lp/answers/stories/question-overview.txt
index f9150ad..0a22246 100644
--- a/lib/lp/answers/stories/question-overview.txt
+++ b/lib/lp/answers/stories/question-overview.txt
@@ -32,7 +32,7 @@ available from the 'Answers' facet.
     >>> browser.getLink('Answers').click()
 
     >>> soup = find_main_content(browser.contents)
-    >>> print(soup.find('h1').renderContents())
+    >>> print(soup.find('h1').decode_contents())
     Questions for Mozilla Firefox
 
     >>> browser.getLink('Firefox loses focus and gets stuck').url
@@ -61,7 +61,7 @@ question page.
     >>> print(browser.title)
     Question #2 : ...
 
-    >>> print(find_main_content(browser.contents).find('h1').renderContents())
+    >>> print(find_main_content(browser.contents).find('h1').decode_contents())
     Problem showing the SVG demo on W3C site
 
 
@@ -197,7 +197,7 @@ involving a person.
     >>> print(browser.title)
     Questions : Foo Bar
 
-    >>> print(find_main_content(browser.contents).find('h1').renderContents())
+    >>> print(find_main_content(browser.contents).find('h1').decode_contents())
     Questions for Foo Bar
 
     >>> browser.getLink('Slow system').url
@@ -222,7 +222,7 @@ the proper context where the question can be found:
     >>> print(browser.url)
     http://answers.launchpad.test/firefox/+question/1
 
-    >>> print(find_main_content(browser.contents).find('h1').renderContents())
+    >>> print(find_main_content(browser.contents).find('h1').decode_contents())
     Firefox cannot render Bank Site
 
 This also works on the webservice.
diff --git a/lib/lp/answers/stories/question-reject-and-change-status.txt b/lib/lp/answers/stories/question-reject-and-change-status.txt
index 3a47d02..9b2c22b 100644
--- a/lib/lp/answers/stories/question-reject-and-change-status.txt
+++ b/lib/lp/answers/stories/question-reject-and-change-status.txt
@@ -31,7 +31,7 @@ To reject the question, the user clicks on the 'Reject Question' link.
 They need to enter a message explaining the rejection:
 
     >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There is 1 error.
     You must provide an explanation message.
 
@@ -51,7 +51,7 @@ will reject the question.
 Once the question is rejected, a confirmation message is shown;
 
     >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     You have rejected this question.
 
 its status is changed to 'Invalid';
@@ -66,7 +66,7 @@ its status is changed to 'Invalid';
 and the rejection message is added to the question board.
 
     >>> content = find_main_content(admin_browser.contents)
-    >>> print(content.findAll('div', 'boardCommentBody')[-1].renderContents())
+    >>> print(content.findAll('div', 'boardCommentBody')[-1].decode_contents())
     <p>Rejecting because it's a duplicate of <a...>bug #1</a>.</p>
 
 The call to help with this problem is not displayed.
@@ -81,7 +81,7 @@ stating that the question is already rejected:
     >>> print(admin_browser.url)
     http://answers.launchpad.test/firefox/+question/2
     >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     The question is already rejected.
 
 = Changing the Question Status =
@@ -124,7 +124,7 @@ status change:
 
     >>> admin_browser.getControl('Change Status').click()
     >>> for error in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(error.renderContents())
+    ...     print(error.decode_contents())
     There are 2 errors.
     You didn't change the status.
     You must provide an explanation message.
@@ -141,7 +141,7 @@ select back the 'Open' status and provide an appropriate message:
 Once the operation is completed, a confirmation message is displayed;
 
     >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     Question status updated.
 
 its status is updated;
@@ -152,6 +152,6 @@ its status is updated;
 and the explanation message is added to the question discussion:
 
     >>> content = find_main_content(admin_browser.contents)
-    >>> print(content.findAll('div', 'boardCommentBody')[-1].renderContents())
+    >>> print(content.findAll('div', 'boardCommentBody')[-1].decode_contents())
     <p>Setting status back to 'Open'. Questions similar to a
     bug report should be linked to it, not rejected.</p>
diff --git a/lib/lp/answers/stories/question-search-multiple-languages.txt b/lib/lp/answers/stories/question-search-multiple-languages.txt
index e500547..034c130 100644
--- a/lib/lp/answers/stories/question-search-multiple-languages.txt
+++ b/lib/lp/answers/stories/question-search-multiple-languages.txt
@@ -21,7 +21,7 @@ information.
     >>> anon_browser.open('http://launchpad.test/distros/ubuntu/+questions')
     >>> soup = find_main_content(anon_browser.contents)
     >>> for question in soup.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Continue playing after shutdown
     Play DVDs in Totem
     mailto: problem in webpage
@@ -32,7 +32,7 @@ information.
     >>> anon_browser.getLink('Next').click()
     >>> soup = find_main_content(anon_browser.contents)
     >>> for question in soup.findAll('td', 'questionTITLE'):
-    ...     print(question.find('a').renderContents())
+    ...     print(question.find('a').decode_contents())
     Installation failed
 
 The questions match the languages inferred from GeoIP (South Africa in
@@ -51,12 +51,12 @@ the anonymous user will see a Spanish question.
     >>> anon_browser.getControl('Search', index=0).click()
     >>> table = find_tag_by_id(anon_browser.contents, 'question-listing')
     >>> for question in table.findAll('td', 'questionTITLE'):
-    ...     question.find('a').renderContents()
-    'Problema al recompilar kernel con soporte smp (doble-n\xc3\xbacleo)'
-    'Continue playing after shutdown'
-    'Play DVDs in Totem'
-    'mailto: problem in webpage'
-    'Installation of Java Runtime Environment for Mozilla'
+    ...     print(question.find('a').decode_contents())
+    Problema al recompilar kernel con soporte smp (doble-núcleo)
+    Continue playing after shutdown
+    Play DVDs in Totem
+    mailto: problem in webpage
+    Installation of Java Runtime Environment for Mozilla
 
 While the user might recognize the first question above is in Spanish,
 browsers and search engine robots need help. Each row in the list
@@ -77,10 +77,10 @@ Arabic and is written from right-to-left.
     >>> anon_browser.getLink('Next').click()
     >>> table = find_tag_by_id(anon_browser.contents, 'question-listing')
     >>> for question in table.findAll('td', 'questionTITLE'):
-    ...     question.find('a').renderContents()
-    'Slow system'
-    'Installation failed'
-    '\xd8\xb9\xd9\x83\xd8\xb3 ...
+    ...     print(question.find('a').decode_contents())
+    Slow system
+    Installation failed
+    عكس ...
 
     >>> for question in table.findAll('tr', lang=True):
     ...     print('lang="%s" dir="%s"' % (question['lang'], question['dir']))
@@ -109,7 +109,7 @@ language controls.
     LookupError: name u'field.language...
 
     >>> content = find_main_content(anon_browser.contents).find('p')
-    >>> print(content.renderContents())
+    >>> print(content.decode_contents())
     There are no questions for Kubuntu with the requested statuses.
 
 When the project has questions in only one language, and that language
@@ -138,20 +138,20 @@ then questions with those language will be displayed:
 
     >>> soup = find_main_content(anon_browser.contents)
     >>> for question in soup.findAll('td', 'questionTITLE'):
-    ...     question.find('a').renderContents()
-    'Problema al recompilar kernel con soporte smp (doble-n\xc3\xbacleo)'
-    'Continue playing after shutdown'
-    'Play DVDs in Totem'
-    'mailto: problem in webpage'
-    'Installation of Java Runtime Environment for Mozilla'
+    ...     print(question.find('a').decode_contents())
+    Problema al recompilar kernel con soporte smp (doble-núcleo)
+    Continue playing after shutdown
+    Play DVDs in Totem
+    mailto: problem in webpage
+    Installation of Java Runtime Environment for Mozilla
 
     # Since we have more than 5 results, some of them are in the second batch.
     >>> anon_browser.getLink('Next').click()
     >>> soup = find_main_content(anon_browser.contents)
     >>> for question in soup.findAll('td', 'questionTITLE'):
-    ...     question.find('a').renderContents()
-    'Slow system'
-    'Installation failed'
+    ...     print(question.find('a').decode_contents())
+    Slow system
+    Installation failed
 
 
 == Authenticated searching ==
@@ -213,8 +213,8 @@ the search results.
     >>> browser.getControl('Search', index=0).click()
     >>> content = find_main_content(browser.contents)
     >>> for question in content.findAll('td', 'questionTITLE'):
-    ...     question.find('a').renderContents()
-    'Problema al recompilar kernel con soporte smp (doble-n\xc3\xbacleo)'
+    ...     print(question.find('a').decode_contents())
+    Problema al recompilar kernel con soporte smp (doble-núcleo)
 
 Some users, translators in particular, speak an English variant.
 English variants are considered to be English in the Answers,
@@ -253,12 +253,12 @@ English, and can use it to locate English questions.
     >>> daf_browser.getControl('Search', index=0).click()
     >>> content = find_main_content(daf_browser.contents)
     >>> for question in content.findAll('td', 'questionTITLE'):
-    ...     question.find('a').renderContents()
-    'Continue playing after shutdown'
-    'Play DVDs in Totem'
-    'mailto: problem in webpage'
-    'Installation of Java Runtime Environment for Mozilla'
-    'Slow system'
+    ...     print(question.find('a').decode_contents())
+    Continue playing after shutdown
+    Play DVDs in Totem
+    mailto: problem in webpage
+    Installation of Java Runtime Environment for Mozilla
+    Slow system
 
 
 == Questions by language ==
@@ -302,8 +302,8 @@ the preceding page.
 
     >>> content = find_main_content(user_browser.contents)
     >>> for question in content.findAll('td', 'questionTITLE'):
-    ...     question.find('a').renderContents()
-    'Problemas de Impress\xc3\xa3o no Firefox'
+    ...     print(question.find('a').decode_contents())
+    Problemas de Impressão no Firefox
 
 The page in all other respects behaves like a question search page.
 
@@ -331,8 +331,8 @@ project. They can see the question on the second page of "My questions"...
     dir: rtl
 
     >>> for question in questions.findAll('td', {'class': 'questionTITLE'}):
-    ...     question.find('a').renderContents()
-    '\xd8\xb9\xd9\x83\xd8\xb3 \xd8\xa7\xd9\x84\xd8\xaa\xd8\xba...'
+    ...     print(question.find('a').decode_contents())
+    عكس التغ...
 
 ...even though they have not set Arabic as one of their preferred languages.
 
diff --git a/lib/lp/answers/stories/question-workflow.txt b/lib/lp/answers/stories/question-workflow.txt
index 6fd675d..08de35a 100755
--- a/lib/lp/answers/stories/question-workflow.txt
+++ b/lib/lp/answers/stories/question-workflow.txt
@@ -58,7 +58,7 @@ on the 'Add Information Request' button.
     ...     'http://launchpad.test/firefox/+question/2')
     >>> content = find_tag_by_id(
     ...     support_browser.contents, 'can-you-help-with-this-problem')
-    >>> print(content.h2.renderContents())
+    >>> print(content.h2.decode_contents())
     Can you help with this problem?
 
     >>> print(extract_text(
@@ -86,7 +86,7 @@ you an error.
 
     >>> support_browser.getControl('Add Information Request').click()
     >>> soup = find_main_content(support_browser.contents)
-    >>> print(soup.find('div', 'message').renderContents())
+    >>> print(soup.find('div', 'message').decode_contents())
     Please enter a message.
 
 
@@ -219,7 +219,7 @@ The confirmed answer is also highlighted.
     <img ... src="/@@/favourite-yes" ... title="Marked as best answer"/>
 
     >>> print(soup.find(
-    ...     'div', 'boardCommentBody highlighted').renderContents())
+    ...     'div', 'boardCommentBody highlighted').decode_contents())
     <p>New version of the firefox package are available with SVG support
     enabled. You can use apt-get or adept to upgrade.</p>
 
@@ -383,7 +383,7 @@ It lists all the actions performed through workflow on the question:
     >>> soup = find_main_content(anon_browser.contents)
     >>> action_listing = soup.find('table', 'listing')
     >>> for header in action_listing.findAll('th'):
-    ...     print(header.renderContents())
+    ...     print(header.decode_contents())
     When
     Who
     Action
@@ -392,8 +392,8 @@ It lists all the actions performed through workflow on the question:
     >>> for row in action_listing.find('tbody').findAll('tr'):
     ...     cells = row.findAll('td')
     ...     who = extract_text(cells[1].find('a'))
-    ...     action = cells[2].renderContents()
-    ...     new_status = cells[3].renderContents()
+    ...     action = cells[2].decode_contents()
+    ...     new_status = cells[3].decode_contents()
     ...     print(who.lstrip('&nbsp;'), action, new_status)
     No Privileges Person Request for more information Needs information
     No Privileges Person Comment Needs information
diff --git a/lib/lp/answers/stories/questions-index.txt b/lib/lp/answers/stories/questions-index.txt
index 450d119..3285409 100644
--- a/lib/lp/answers/stories/questions-index.txt
+++ b/lib/lp/answers/stories/questions-index.txt
@@ -24,25 +24,25 @@ It shows the 5 latest questions asked:
 
     >>> latest_questions_asked = find_tag_by_id(
     ...     anon_browser.contents, 'latest-questions-asked')
-    >>> print(latest_questions_asked.find('h2').renderContents())
+    >>> print(latest_questions_asked.find('h2').decode_contents())
     Latest questions asked
     >>> for row in latest_questions_asked.findAll('li'):
-    ...     row.find('a').renderContents()
-    '...: Problemas de Impress\xc3\xa3o no Firefox'
-    '...: Problema al recompilar kernel con soporte smp (doble-n\xc3\xbacleo)'
-    '...: Continue playing after shutdown'
-    '...: Installation failed'
-    '...: Firefox loses focus and gets stuck'
+    ...     print(row.find('a').decode_contents())
+    13: Problemas de Impressão no Firefox
+    12: Problema al recompilar kernel con soporte smp (doble-núcleo)
+    11: Continue playing after shutdown
+    5: Installation failed
+    4: Firefox loses focus and gets stuck
 
 As well as the 5 latest questions solved:
 
     >>> latest_questions_solved = find_tag_by_id(
     ...     anon_browser.contents, 'latest-questions-solved')
-    >>> print(latest_questions_solved.find('h2').renderContents())
+    >>> print(latest_questions_solved.find('h2').decode_contents())
     Latest questions solved
     >>> for row in latest_questions_solved.findAll('li'):
-    ...     row.find('a').renderContents()
-    '...: mailto: problem in webpage'
+    ...     print(row.find('a').decode_contents())
+    9: mailto: problem in webpage
 
 The application footer also contains a sample of stats for the application:
 
diff --git a/lib/lp/answers/stories/this-is-a-faq.txt b/lib/lp/answers/stories/this-is-a-faq.txt
index 3627a74..ff6e5d9 100644
--- a/lib/lp/answers/stories/this-is-a-faq.txt
+++ b/lib/lp/answers/stories/this-is-a-faq.txt
@@ -60,7 +60,7 @@ relevant' option is selected.
     ...         else:
     ...             radio = '( )'
     ...         if button['value']:
-    ...             link = button.findNext('a').renderContents()
+    ...             link = button.findNext('a').decode_contents()
     ...         else:
     ...             link = ''
     ...         print(radio, label, link)
diff --git a/lib/lp/app/browser/tests/test_stringformatter.py b/lib/lp/app/browser/tests/test_stringformatter.py
index 49cd0af..9b0caf3 100644
--- a/lib/lp/app/browser/tests/test_stringformatter.py
+++ b/lib/lp/app/browser/tests/test_stringformatter.py
@@ -514,7 +514,7 @@ class TestDiffFormatter(TestCase):
         line_numbers = find_tags_by_class(html, 'line-no')
         self.assertEqual(
             ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'],
-            [tag.renderContents() for tag in line_numbers])
+            [tag.decode_contents() for tag in line_numbers])
         text = find_tags_by_class(html, 'text')
         self.assertEqual(
             [['diff-file', 'text'],
@@ -549,7 +549,7 @@ class TestDiffFormatter(TestCase):
         line_numbers = find_tags_by_class(html, 'line-no')
         self.assertEqual(
             ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
-            [tag.renderContents() for tag in line_numbers])
+            [tag.decode_contents() for tag in line_numbers])
         text = find_tags_by_class(html, 'text')
         self.assertEqual(
             [['diff-file', 'text'],
@@ -618,12 +618,12 @@ class TestSideBySideDiffFormatter(TestCase):
         line_numbers = find_tags_by_class(html, 'line-no')
         self.assertEqual(
             ['1', '2', '3', '4', '5', '7', '8', '9', '11', '12', '13'],
-            [tag.renderContents() for tag in line_numbers])
+            [tag.decode_contents() for tag in line_numbers])
         ss_line_numbers = find_tags_by_class(html, 'ss-line-no')
         self.assertEqual(
             ['2435', '2439', '2436', '2440', '', '2441', '2437', '2442',
              '2438', '2443'],
-            [tag.renderContents() for tag in ss_line_numbers])
+            [tag.decode_contents() for tag in ss_line_numbers])
         text = find_tags_by_class(html, 'text')
         self.assertEqual(
             [['diff-file', 'text'],
@@ -665,12 +665,12 @@ class TestSideBySideDiffFormatter(TestCase):
         line_numbers = find_tags_by_class(html, 'line-no')
         self.assertEqual(
             ['1', '2', '3', '4', '5', '6', '8', '9', '10', '12'],
-            [tag.renderContents() for tag in line_numbers])
+            [tag.decode_contents() for tag in line_numbers])
         ss_line_numbers = find_tags_by_class(html, 'ss-line-no')
         self.assertEqual(
             ['2435', '2439', '2436', '2440', '', '2441', '2437', '2442',
              '2438', '2443'],
-            [tag.renderContents() for tag in ss_line_numbers])
+            [tag.decode_contents() for tag in ss_line_numbers])
         text = find_tags_by_class(html, 'text')
         self.assertEqual(
             [['diff-file', 'text'],
diff --git a/lib/lp/app/stories/launchpad-root/front-pages.txt b/lib/lp/app/stories/launchpad-root/front-pages.txt
index 50eebe2..772451a 100644
--- a/lib/lp/app/stories/launchpad-root/front-pages.txt
+++ b/lib/lp/app/stories/launchpad-root/front-pages.txt
@@ -32,7 +32,7 @@ It also includes a search form...
 ...and lists of featured projects and marketing material.
 
     >>> featured = find_tag_by_id(browser.contents, 'homepage-featured')
-    >>> print(extract_text(featured.renderContents()))
+    >>> print(extract_text(featured.decode_contents()))
     Featured projects
     ...
     Ubuntu
diff --git a/lib/lp/blueprints/stories/blueprints/xx-creation.txt b/lib/lp/blueprints/stories/blueprints/xx-creation.txt
index 8eeb212..9d91aa3 100644
--- a/lib/lp/blueprints/stories/blueprints/xx-creation.txt
+++ b/lib/lp/blueprints/stories/blueprints/xx-creation.txt
@@ -638,7 +638,7 @@ an error:
     >>> user_browser.url
     'http://blueprints.launchpad.test/ubuntu/+addspec'
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...      print(message.renderContents())
+    ...      print(message.decode_contents())
     There is 1 error...already in use by another blueprint...
 
 Attempting to register a duplicate blueprint from a non-target context
@@ -656,7 +656,7 @@ produces the same error:
     >>> print(user_browser.url)
     http://blueprints.launchpad.test/sprints/rome/+addspec
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...      print(message.renderContents())
+    ...      print(message.decode_contents())
     There is 1 error...already in use by another blueprint...
 
 
@@ -674,7 +674,7 @@ Blueprint names must conform to a set pattern:
     >>> user_browser.url
     'http://blueprints.launchpad.test/ubuntu/+addspec'
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...      print(message.renderContents())
+    ...      print(message.decode_contents())
     There is 1 error...Invalid name...
 
 However, some invalid names can be transformed into valid names. When it is
@@ -709,7 +709,7 @@ blueprint:
     >>> user_browser.url
     'http://blueprints.launchpad.test/ubuntu/+addspec'
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...      print(message.renderContents())
+    ...      print(message.decode_contents())
     There is 1 error...is already registered by...
     ...Network Magic: Auto Network Detection...
 
diff --git a/lib/lp/blueprints/stories/sprints/xx-sprints.txt b/lib/lp/blueprints/stories/sprints/xx-sprints.txt
index 2b26b63..bd0e5c9 100644
--- a/lib/lp/blueprints/stories/sprints/xx-sprints.txt
+++ b/lib/lp/blueprints/stories/sprints/xx-sprints.txt
@@ -71,7 +71,7 @@ First we'll test the name field validator.
     >>> user_browser.getControl('Add Sprint').click()
 
     >>> for tag in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     <BLANKLINE>
     Invalid name 'ltsp_on_steroids!'. Names must be at least two characte...
@@ -83,7 +83,7 @@ nice error message
     >>> user_browser.getControl('Add Sprint').click()
 
     >>> for tag in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     <BLANKLINE>
     ubz is already in use by another sprint.
@@ -99,7 +99,7 @@ a error message.
     >>> user_browser.getControl('Add Sprint').click()
 
     >>> for tag in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     <BLANKLINE>
     This event can't start after it ends
@@ -213,7 +213,7 @@ should receive a nice error message.
     >>> browser.getControl('Change').click()
 
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     This event can't start after it ends
 
@@ -266,7 +266,7 @@ We should be able to see the spec assignment table of a sprint:
 
     >>> mainarea = find_main_content(anon_browser.contents)
     >>> for header in mainarea.findAll('th'):
-    ...     print(header.renderContents())
+    ...     print(header.decode_contents())
     Priority
     Name
     Definition
@@ -330,7 +330,7 @@ date up to one day after the sprint ends.
     http://launchpad.test/sprints/ubz/+attend
 
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     Please pick a date after 2006-01-10 08:30
 
@@ -344,7 +344,7 @@ An attendance that starts after the end of the sprint is also an error:
     http://launchpad.test/sprints/ubz/+attend
 
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There are 2 errors.
     Please pick a date before 2006-02-12 17:00
     Please pick a date before 2006-02-13 17:00
@@ -360,7 +360,7 @@ error:
     http://launchpad.test/sprints/ubz/+attend
 
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There are 2 errors.
     Please pick a date after 2006-01-09 08:30
     Please pick a date after 2006-01-10 08:30
diff --git a/lib/lp/blueprints/stories/standalone/xx-retargeting.txt b/lib/lp/blueprints/stories/standalone/xx-retargeting.txt
index 523cd5d..3f52054 100644
--- a/lib/lp/blueprints/stories/standalone/xx-retargeting.txt
+++ b/lib/lp/blueprints/stories/standalone/xx-retargeting.txt
@@ -59,7 +59,7 @@ We stay on the same page and get an error message printed out:
   'http://blueprints.launchpad.test/firefox/+spec/svg-support/+retarget'
 
   >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
-  ...     print(tag.renderContents())
+  ...     print(tag.decode_contents())
   There is 1 error.
   <BLANKLINE>
   There is no project with the name 'foo bar'. Please check that name and try again.
diff --git a/lib/lp/bugs/browser/tests/test_bugtask.py b/lib/lp/bugs/browser/tests/test_bugtask.py
index cb98e74..075bf72 100644
--- a/lib/lp/bugs/browser/tests/test_bugtask.py
+++ b/lib/lp/bugs/browser/tests/test_bugtask.py
@@ -871,7 +871,7 @@ class TestBugTasksTableView(TestCaseWithFactory):
         # Check the result.
         soup = BeautifulSoup(content)
         tag = soup.find('label', attrs={'for': "foo.assignee.assigned_to"})
-        tag_text = tag.renderContents().strip()
+        tag_text = tag.decode_contents().strip()
         self.assertEqual(assignee.unique_displayname, tag_text)
 
 
diff --git a/lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt b/lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt
index 0e3f41d..797e5ad 100644
--- a/lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt
+++ b/lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt
@@ -506,7 +506,7 @@ But we're happy, so we add the bug watch.
     >>> bug_watches = find_portlet(
     ...     user_browser.contents, 'Remote bug watches')
     >>> for li in bug_watches('li'):
-    ...     print(li.findAll('a')[0].renderContents())
+    ...     print(li.findAll('a')[0].decode_contents())
     gnome-bugzilla #42
 
 It's possible to supply an HTTPS URL, even though the bug tracker's base
@@ -555,7 +555,7 @@ tracker type it is), an error message is displayed.
     http://bugs.launchpad.test/firefox/+bug/4/+choose-affected-product
 
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There is 1 error.
     Launchpad does not recognize the bug tracker at this URL.
 
@@ -597,7 +597,7 @@ registered automatically.
     >>> bug_watches = find_portlet(
     ...     user_browser.contents, 'Remote bug watches')
     >>> for li in bug_watches('li'):
-    ...     print(li.findAll('a')[0].renderContents())
+    ...     print(li.findAll('a')[0].decode_contents())
     gnome-bugzilla #42
     gnome-bugzilla #84
     auto-new.trac #42
diff --git a/lib/lp/bugs/stories/bug-also-affects/xx-duplicate-bugwatches.txt b/lib/lp/bugs/stories/bug-also-affects/xx-duplicate-bugwatches.txt
index cde8e2e..3210e65 100644
--- a/lib/lp/bugs/stories/bug-also-affects/xx-duplicate-bugwatches.txt
+++ b/lib/lp/bugs/stories/bug-also-affects/xx-duplicate-bugwatches.txt
@@ -21,7 +21,7 @@ Now we can see the added bug watch in the bug watch portlet.
     >>> bugwatch_portlet = find_portlet(
     ...     user_browser.contents, 'Remote bug watches')
     >>> for li_tag in bugwatch_portlet.findAll('li'):
-    ...     print(li_tag.findAll('a')[0].renderContents())
+    ...     print(li_tag.findAll('a')[0].decode_contents())
     debbugs #42
 
 If we add another bug watch, pointing to the same URL, the previous one
diff --git a/lib/lp/bugs/stories/bug-release-management/xx-bug-release-management.txt b/lib/lp/bugs/stories/bug-release-management/xx-bug-release-management.txt
index 3c93bc9..936b823 100644
--- a/lib/lp/bugs/stories/bug-release-management/xx-bug-release-management.txt
+++ b/lib/lp/bugs/stories/bug-release-management/xx-bug-release-management.txt
@@ -29,7 +29,7 @@ Approving a nomination displays a feedback message.
 
     >>> feedback_msg = find_tags_by_class(
     ...     admin_browser.contents, "informational message")[0]
-    >>> print(feedback_msg.renderContents())
+    >>> print(feedback_msg.decode_contents())
     Approved nomination for Mozilla Firefox 1.0
 
 After a productseries task has been created, it's editable.
@@ -58,7 +58,7 @@ Declining a nomination displays a feedback message.
 
     >>> feedback_msg = find_tags_by_class(
     ...     admin_browser.contents, "informational message")[0]
-    >>> print(feedback_msg.renderContents())
+    >>> print(feedback_msg.decode_contents())
     Declined nomination for Ubuntu Hoary
 
 Nominate a bug to a distribution release
@@ -114,7 +114,7 @@ raised.
 
     >>> for tag in find_tags_by_class(nominater_other_browser.contents,
     ...     'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     This bug has already been nominated for these series: Aqua
 
@@ -178,7 +178,7 @@ accidentally nominates the bug for Beta a second time, an error is raised.
 
     >>> for tag in find_tags_by_class(nominater_other_browser.contents,
     ...     'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     This bug has already been nominated for these series: Beta
 
diff --git a/lib/lp/bugs/stories/bug-tags/xx-searching-for-tags.txt b/lib/lp/bugs/stories/bug-tags/xx-searching-for-tags.txt
index f28c4a4..c51e388 100644
--- a/lib/lp/bugs/stories/bug-tags/xx-searching-for-tags.txt
+++ b/lib/lp/bugs/stories/bug-tags/xx-searching-for-tags.txt
@@ -32,7 +32,7 @@ If an invalid tag name is entered, an error message will be displayed.
     >>> anon_browser.getControl('Search', index=0).click()
 
     >>> for tag in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     '!!invalid!!' isn't a valid tag name. Tags must start with a letter
     or number and be lowercase. The characters "+", "-" and "." are also
     allowed after the first character.
diff --git a/lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-listings-page.txt b/lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-listings-page.txt
index b7a633c..ef8b671 100644
--- a/lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-listings-page.txt
+++ b/lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-listings-page.txt
@@ -8,7 +8,7 @@ using Javascript after page load.
     http://launchpad.test/ubuntu/+bugtarget-portlet-tags-content
     >>> tags_portlet = find_tags_by_class(anon_browser.contents, 'data-list')[0]
     >>> for a_tag in tags_portlet('a'):
-    ...     print(a_tag.renderContents())
+    ...     print(a_tag.decode_contents())
     crash
     pebcak
     dataloss
diff --git a/lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-page.txt b/lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-page.txt
index fbc98a7..dc4ae96 100644
--- a/lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-page.txt
+++ b/lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-page.txt
@@ -19,7 +19,7 @@ If we enter an invalid tag name, we'll get an error.
     >>> user_browser.getControl('Change').click()
 
     >>> for tag in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     '!!invalid!!' isn't a valid tag name. Tags must start with a letter
     or number and be lowercase. The characters "+", "-" and "." are also
diff --git a/lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt b/lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt
index 7126637..7cef7dd 100644
--- a/lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt
+++ b/lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt
@@ -36,7 +36,7 @@ We can check that the attachment is there
 
   >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
   >>> for li_tag in attachments.findAll('li', 'download-attachment'):
-  ...   print(li_tag.a.renderContents())
+  ...   print(li_tag.a.decode_contents())
   Some information
 
   >>> link = user_browser.getLink('Some information')
@@ -69,7 +69,7 @@ also not necessary to enter a comment in order to add an attachment.
 
   >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
   >>> for li_tag in attachments.findAll('li', 'download-attachment'):
-  ...   print(li_tag.a.renderContents())
+  ...   print(li_tag.a.decode_contents())
   Some information
   bar.txt
 
@@ -146,7 +146,7 @@ listed as an ordinary attachment.
   'http://bugs.launchpad.test/firefox/+bug/1'
   >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
   >>> for li_tag in attachments.findAll('li', 'download-attachment'):
-  ...   print(li_tag.a.renderContents())
+  ...   print(li_tag.a.decode_contents())
   Some information
   bar.txt
   More data
@@ -203,7 +203,7 @@ Now we are redirected to the main bug page...
 
   >>> patches = find_portlet(user_browser.contents, 'Patches')
   >>> for li_tag in patches.findAll('li', 'download-attachment'):
-  ...   print(li_tag.a.renderContents())
+  ...   print(li_tag.a.decode_contents())
   A fix for this bug.
   A better icon for foo
 
@@ -331,7 +331,7 @@ Now we are redirected to the main bug page...
 
     >>> patches = find_portlet(user_browser.contents, 'Patches')
     >>> for li_tag in patches.findAll('li', 'download-attachment'):
-    ...   print(li_tag.a.renderContents())
+    ...   print(li_tag.a.decode_contents())
     Another title
     A fix for this bug.
     A better icon for foo
@@ -343,7 +343,7 @@ Now we are redirected to the main bug page...
 
     >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
     >>> for li_tag in attachments.findAll('li', 'download-attachment'):
-    ...   print(li_tag.a.renderContents())
+    ...   print(li_tag.a.decode_contents())
     bar.txt
     More data
 
diff --git a/lib/lp/bugs/stories/bugattachments/xx-delete-bug-attachment.txt b/lib/lp/bugs/stories/bugattachments/xx-delete-bug-attachment.txt
index 3a45f61..459a548 100644
--- a/lib/lp/bugs/stories/bugattachments/xx-delete-bug-attachment.txt
+++ b/lib/lp/bugs/stories/bugattachments/xx-delete-bug-attachment.txt
@@ -18,7 +18,7 @@ The attachment is now visible on the bug page.
     >>> attachment_portlet = find_portlet(
     ...     user_browser.contents, 'Bug attachments')
     >>> for li in attachment_portlet.findAll('li', 'download-attachment'):
-    ...     print(li.a.renderContents())
+    ...     print(li.a.decode_contents())
     Great deal
 
 There will also be a comment with a link to the attachment in its body.
@@ -68,7 +68,7 @@ arent' any other attachments, the portlet won't show up at all.
     >>> user_browser.url
     'http://.../+bug/2'
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     Attachment
     "<a href="http://bugs.launchpad.test/...+files/foo.txt";>Great deal</a>"
     has been deleted.
diff --git a/lib/lp/bugs/stories/bugs/xx-add-comment-bugtask-edit.txt b/lib/lp/bugs/stories/bugs/xx-add-comment-bugtask-edit.txt
index da9cc1a..cb230ec 100644
--- a/lib/lp/bugs/stories/bugs/xx-add-comment-bugtask-edit.txt
+++ b/lib/lp/bugs/stories/bugs/xx-add-comment-bugtask-edit.txt
@@ -20,5 +20,5 @@ The user was returned to the bug page, and the comment was added.
 
     >>> main_content = find_main_content(user_browser.contents)
     >>> last_comment = main_content('div', 'boardCommentBody')[-1]
-    >>> print(last_comment.div.renderContents())
+    >>> print(last_comment.div.decode_contents())
     <p>A comment with no change to the bug task.</p>
diff --git a/lib/lp/bugs/stories/bugs/xx-add-comment-distribution-no-current-release.txt b/lib/lp/bugs/stories/bugs/xx-add-comment-distribution-no-current-release.txt
index 21f26b1..f28a028 100644
--- a/lib/lp/bugs/stories/bugs/xx-add-comment-distribution-no-current-release.txt
+++ b/lib/lp/bugs/stories/bugs/xx-add-comment-distribution-no-current-release.txt
@@ -16,5 +16,5 @@ it's still possible to add comments to the bug.
 
     >>> for comment_div in find_tags_by_class(
     ...     user_browser.contents, 'boardCommentBody'):
-    ...     print(comment_div.div.renderContents())
+    ...     print(comment_div.div.decode_contents())
     <p>A new comment.</p>
diff --git a/lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt b/lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt
index 4f6594f..c0f4af0 100644
--- a/lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt
+++ b/lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt
@@ -17,8 +17,9 @@ description in the bug page.
 
     >>> description = find_tag_by_id(
     ...     user_browser.contents, 'edit-description')
-    >>> description.renderContents()
-    '...<p>Shirtpkdf user@xxxxxxxxxx lkjd hlkjfds...'
+    >>> print(description.decode_contents())
+    <BLANKLINE>
+    ...<p>Shirtpkdf user@xxxxxxxxxx lkjd hlkjfds...
 
 An anonymous cannot see the email address anywhere in the page.
 
@@ -33,5 +34,6 @@ An anonymous cannot see the email address anywhere in the page.
 
     >>> description = find_tag_by_id(
     ...     anon_browser.contents, 'edit-description')
-    >>> description.renderContents()
-    '...<p>Shirtpkdf &lt;email address hidden&gt; lkjd hlkjfds...'
+    >>> print(description.decode_contents())
+    <BLANKLINE>
+    ...<p>Shirtpkdf &lt;email address hidden&gt; lkjd hlkjfds...
diff --git a/lib/lp/bugs/stories/bugs/xx-duplicate-of-private-bug.txt b/lib/lp/bugs/stories/bugs/xx-duplicate-of-private-bug.txt
index f714d61..68bf232 100644
--- a/lib/lp/bugs/stories/bugs/xx-duplicate-of-private-bug.txt
+++ b/lib/lp/bugs/stories/bugs/xx-duplicate-of-private-bug.txt
@@ -33,10 +33,10 @@ duplicate bug page:
 
     >>> def print_messages(browser):
     ...     for tag in find_tags_by_class(browser.contents, 'message'):
-    ...         print(tag.renderContents())
+    ...         print(tag.decode_contents())
 
     >>> print(find_tag_by_id(
-    ...     admin_browser.contents, 'duplicate-of').renderContents())
+    ...     admin_browser.contents, 'duplicate-of').decode_contents())
     bug #8
 
 But when accessing it as an unprivileged user the title of the private
diff --git a/lib/lp/bugs/stories/bugs/xx-front-page-bug-lists.txt b/lib/lp/bugs/stories/bugs/xx-front-page-bug-lists.txt
index 9627c61..f783fa9 100644
--- a/lib/lp/bugs/stories/bugs/xx-front-page-bug-lists.txt
+++ b/lib/lp/bugs/stories/bugs/xx-front-page-bug-lists.txt
@@ -47,8 +47,8 @@ to the bug target and to the bug reporter's page.
 
     >>> def print_bugs_links(bug_row):
     ...     print("%s: %s" % (
-    ...         bug_row.span.a.renderContents().strip(),
-    ...         bug_row.a.renderContents()))
+    ...         bug_row.span.a.decode_contents().strip(),
+    ...         bug_row.a.decode_contents()))
     ...     print(bug_row.a['href'])
     ...     print(bug_row.span.a['href'])
     >>> for li in reported_bugs('li'):
diff --git a/lib/lp/bugs/stories/bugs/xx-front-page-search.txt b/lib/lp/bugs/stories/bugs/xx-front-page-search.txt
index 612f0c2..c7914a7 100644
--- a/lib/lp/bugs/stories/bugs/xx-front-page-search.txt
+++ b/lib/lp/bugs/stories/bugs/xx-front-page-search.txt
@@ -86,7 +86,7 @@ name is specified, an error message will be displayed.
     'test bug'
 
     >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     Please enter a project name
 
 An error message will be displayed also if the project isn't registered
@@ -102,7 +102,7 @@ in the Launchpad.
     >>> anon_browser.getControl(name='field.searchtext').value
     'test bug'
     >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There is no project named 'invalid' registered in Launchpad
 
 If the user doesn't know what name to write, they can use the 'Choose'
diff --git a/lib/lp/bugs/stories/bugtask-searches/xx-advanced-people-filters.txt b/lib/lp/bugs/stories/bugtask-searches/xx-advanced-people-filters.txt
index ef280d3..fa1d699 100644
--- a/lib/lp/bugs/stories/bugtask-searches/xx-advanced-people-filters.txt
+++ b/lib/lp/bugs/stories/bugtask-searches/xx-advanced-people-filters.txt
@@ -119,7 +119,7 @@ displayed.
     >>> anon_browser.getControl('Commenter').value = 'non-existent'
     >>> anon_browser.getControl('Search', index=0).click()
     >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There's no person with the name or email address 'non-existent'.
 
 Entering an existing person shows all bugs that person has commented on
@@ -181,7 +181,7 @@ displayed:
     >>> anon_browser.getControl('Subscriber').value = 'non-existent'
     >>> anon_browser.getControl('Search', index=0).click()
     >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There's no person with the name or email address 'non-existent'.
 
 Entering an existing person shows all bugs for packages or products that
diff --git a/lib/lp/bugs/stories/bugwatches/xx-bugwatch-errors.txt b/lib/lp/bugs/stories/bugwatches/xx-bugwatch-errors.txt
index 1a1d93a..ba403de 100644
--- a/lib/lp/bugs/stories/bugwatches/xx-bugwatch-errors.txt
+++ b/lib/lp/bugs/stories/bugwatches/xx-bugwatch-errors.txt
@@ -55,7 +55,7 @@ for that error message.
     >>> user_browser.open('http://bugs.launchpad.test/thunderbird/+bug/12')
     >>> for tag in find_tags_by_class(user_browser.contents,
     ...     'error message'):
-    ...     print(extract_text(tag.renderContents()))
+    ...     print(extract_text(tag.decode_contents()))
     The Mozilla.org Bug Tracker bug #900 appears not to exist. Check
     that the bug number is correct. (what does this mean?)
 
@@ -86,7 +86,7 @@ We can observe this for each of the BugWatchActivityStatus failure values:
     ...     user_browser.open('http://bugs.launchpad.test/thunderbird/+bug/12')
     ...     for tag in find_tags_by_class(user_browser.contents,
     ...         'error message'):
-    ...         print(extract_text(tag.renderContents()))
+    ...         print(extract_text(tag.decode_contents()))
     Launchpad couldn't import bug #900 from The Mozilla.org Bug
     Tracker...
     The Mozilla.org Bug Tracker bug #900 appears not to exist. Check
diff --git a/lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt b/lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt
index 6e9e46e..9518d01 100644
--- a/lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt
+++ b/lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt
@@ -47,7 +47,7 @@ The +edit page for a watch also displays some details about the watch.
 
     >>> for data_tag in find_tags_by_class(
     ...     admin_browser.contents, 'bugwatch-data'):
-    ...     print(extract_text(data_tag.renderContents()))
+    ...     print(extract_text(data_tag.decode_contents()))
     Tracker: The Mozilla.org Bug Tracker
     Remote bug ID: 1000
     Last status: None recorded
@@ -77,7 +77,7 @@ Next check column.
     >>> admin_browser.open('http://bugs.launchpad.test/bugs/1/+watch/2')
     >>> data_tag = find_tag_by_id(
     ...     admin_browser.contents, 'bugwatch-next_check')
-    >>> print(extract_text(data_tag.renderContents()))
+    >>> print(extract_text(data_tag.decode_contents()))
     Next check: 2010-04-08...
 
 
@@ -198,7 +198,7 @@ will appear on the page.
 
     >>> data_tag = find_tag_by_id(
     ...     user_browser.contents, 'bugwatch-next_check')
-    >>> print(extract_text(data_tag.renderContents()))
+    >>> print(extract_text(data_tag.decode_contents()))
     Next check: Not yet scheduled
 
 Clicking the Update Now button will schedule it to be checked
@@ -217,7 +217,7 @@ been scheduled.
     >>> user_browser.open(watch_url)
     >>> data_tag = find_tag_by_id(
     ...     user_browser.contents, 'bugwatch-next_check')
-    >>> print(extract_text(data_tag.renderContents()))
+    >>> print(extract_text(data_tag.decode_contents()))
     Next check: 2...
 
 The button will no longer be shown on the page.
@@ -313,7 +313,7 @@ Clicking the button will reset the watch completely.
 
     >>> data_tag = find_tag_by_id(
     ...     user_browser.contents, 'bugwatch-lastchecked')
-    >>> print(extract_text(data_tag.renderContents()))
+    >>> print(extract_text(data_tag.decode_contents()))
     Checked:
 
 Should a non-admin, non-Launchpad-developer user visit the page, the
diff --git a/lib/lp/bugs/stories/cve/cve-linking.txt b/lib/lp/bugs/stories/cve/cve-linking.txt
index a2bc6f0..cfda439 100644
--- a/lib/lp/bugs/stories/cve/cve-linking.txt
+++ b/lib/lp/bugs/stories/cve/cve-linking.txt
@@ -49,7 +49,7 @@ It is also possible to link a bug using its nickname. For example, bug
 An error message is displayed when user enters a non-existent nickname:
 
     >>> for tag in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     <BLANKLINE>
     Not a valid bug number or nickname.
@@ -75,7 +75,7 @@ forms. This also provides us with a means to test implicitly that bug
     >>> admin_browser.open('http://launchpad.test/bugs/cve/1999-8979')
     >>> for form in find_main_content(
     ...     admin_browser.contents).findAll('form'):
-    ...     print(form.renderContents())
+    ...     print(form.decode_contents())
 
 Similarly, there should be no links allowing the user to mark the bug as
 affecting another product or distribution.
diff --git a/lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-tools.txt b/lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-tools.txt
index 705f57f..1647a69 100644
--- a/lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-tools.txt
+++ b/lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-tools.txt
@@ -29,7 +29,7 @@ After the file has been uploaded, the tool is given a token it can use
 to give the data to the +filebug page.
 
     >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     Your ticket is "..."
 
 To avoid having the tool from parsing the HTML page, the token is
@@ -75,7 +75,7 @@ At first, the user will be shown a message telling them that the extra
 data is being processed.
 
     >>> for message in find_tags_by_class(contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     Please wait while bug data is processed. This page will refresh
     every 10 seconds until processing is complete.
 
@@ -94,7 +94,7 @@ A notification will be shown to inform the user that additional
 information will be added to the bug automatically.
 
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     Extra debug information will be added to the bug report automatically.
 
 After the user fills in the summary and click on the button, we'll still
@@ -114,7 +114,7 @@ Even if the form has errors the token will be present in the URL.
     >>> user_browser.getControl('Submit Bug Report').click()
     >>> for error in find_tags_by_class(
     ...     user_browser.contents, 'message error'):
-    ...     print(error.renderContents())
+    ...     print(error.decode_contents())
     There is 1 error.
 
     >>> user_browser.url == filebug_url
@@ -134,7 +134,7 @@ Two attachments were added.
     >>> attachment_portlet = find_portlet(
     ...     user_browser.contents, 'Bug attachments')
     >>> for li in attachment_portlet('li', 'download-attachment'):
-    ...     print(li.a.renderContents())
+    ...     print(li.a.decode_contents())
     attachment1
     Attachment description.
 
diff --git a/lib/lp/bugs/stories/guided-filebug/xx-distro-guided-filebug-tags.txt b/lib/lp/bugs/stories/guided-filebug/xx-distro-guided-filebug-tags.txt
index e964869..48b4589 100644
--- a/lib/lp/bugs/stories/guided-filebug/xx-distro-guided-filebug-tags.txt
+++ b/lib/lp/bugs/stories/guided-filebug/xx-distro-guided-filebug-tags.txt
@@ -16,7 +16,7 @@ that deal with new package request with a certain tag.
 On the next page, possible duplicates are displayed as ususal. No
 candidates were found for this summary, though.
 
-    >>> print(find_main_content(user_browser.contents).renderContents())
+    >>> print(find_main_content(user_browser.contents).decode_contents())
     <...
     No similar bug reports were found...
 
diff --git a/lib/lp/bugs/stories/guided-filebug/xx-distro-sourcepackage-guided-filebug.txt b/lib/lp/bugs/stories/guided-filebug/xx-distro-sourcepackage-guided-filebug.txt
index 7f1fdf0..f580be0 100644
--- a/lib/lp/bugs/stories/guided-filebug/xx-distro-sourcepackage-guided-filebug.txt
+++ b/lib/lp/bugs/stories/guided-filebug/xx-distro-sourcepackage-guided-filebug.txt
@@ -18,7 +18,7 @@ similar matching bugs.
     >>> similar_bugs_table is None
     True
 
-    >>> print(find_main_content(user_browser.contents).renderContents())
+    >>> print(find_main_content(user_browser.contents).decode_contents())
     <...
     No similar bug reports were found...
 
diff --git a/lib/lp/bugs/stories/guided-filebug/xx-filebug-attachments.txt b/lib/lp/bugs/stories/guided-filebug/xx-filebug-attachments.txt
index 04ec80b..28be58c 100644
--- a/lib/lp/bugs/stories/guided-filebug/xx-filebug-attachments.txt
+++ b/lib/lp/bugs/stories/guided-filebug/xx-filebug-attachments.txt
@@ -43,7 +43,7 @@ No Privileges Person can see the attachment in the attachments portlet.
 
     >>> attachments = find_portlet(user_browser.contents, 'Bug attachments')
     >>> for li_tag in attachments.findAll('li', 'download-attachment'):
-    ...     print(li_tag.a.renderContents())
+    ...     print(li_tag.a.decode_contents())
     A description of the attachment
 
     >>> user_browser.getLink('A description of the attachment').url
diff --git a/lib/lp/bugs/stories/guided-filebug/xx-product-guided-filebug.txt b/lib/lp/bugs/stories/guided-filebug/xx-product-guided-filebug.txt
index 2ef98dc..f7ca514 100644
--- a/lib/lp/bugs/stories/guided-filebug/xx-product-guided-filebug.txt
+++ b/lib/lp/bugs/stories/guided-filebug/xx-product-guided-filebug.txt
@@ -20,11 +20,11 @@ If no title is entered, the user is asked to supply one.
     >>> top_portlet = first_tag_by_class(
     ...     user_browser.contents, 'top-portlet')
     >>> for message in top_portlet.findAll(attrs={'class': 'error message'}):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There is 1 error.
     >>> for message in top_portlet.findAll(
     ...         lambda node: node.attrs.get('class') == ['message']):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     Required input is missing.
 
 The user fills in some keywords, and clicks a button to search existing
@@ -62,12 +62,12 @@ will be displayed as well.
     >>> print(user_browser.url)
     http://bugs.launchpad.test/firefox/+filebug
 
-    >>> print(find_main_content(user_browser.contents).renderContents())
+    >>> print(find_main_content(user_browser.contents).decode_contents())
     <...
     No similar bug reports were found...
 
     >>> for message in find_tags_by_class(user_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There is 1 error.
     Required input is missing.
 
diff --git a/lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt b/lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt
index 0c8760d..a830c05 100644
--- a/lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt
+++ b/lib/lp/bugs/stories/guided-filebug/xx-project-guided-filebug.txt
@@ -26,7 +26,7 @@ product's +filebug page to search for duplicates.
 
     >>> user_browser.url
     'http://bugs...?field.title=Evolution+crashes&field.tags='
-    >>> print(find_main_content(user_browser.contents).renderContents())
+    >>> print(find_main_content(user_browser.contents).decode_contents())
     <...
     <input class="button" id="field.actions.search"
     name="field.actions.search" type="submit" value="Continue"/> ...
@@ -60,6 +60,6 @@ the page explains the case.
 
     >>> for message in find_tags_by_class(user_browser.contents,
     ...     'informational message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There are no projects registered for Test Group that either use Launchpad
     to track bugs or allow new bugs to be filed.
diff --git a/lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt b/lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt
index 0c064d2..de639aa 100644
--- a/lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt
+++ b/lib/lp/bugs/stories/structural-subscriptions/xx-bug-subscriptions.txt
@@ -104,7 +104,7 @@ to subscribe other people.
 No Privileges Person is now subscribed...
 
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     No Privileges Person will now receive an email each time someone reports
     or changes a public bug in "mozilla-firefox in Ubuntu".
 
diff --git a/lib/lp/code/browser/tests/test_branch.py b/lib/lp/code/browser/tests/test_branch.py
index 7a58689..cb2ea66 100644
--- a/lib/lp/code/browser/tests/test_branch.py
+++ b/lib/lp/code/browser/tests/test_branch.py
@@ -1290,9 +1290,10 @@ class TestBranchPrivacyPortlet(TestCaseWithFactory):
         information_type = soup.find('strong')
         description = soup.find('div', id='information-type-description')
         self.assertEqual(
-            InformationType.USERDATA.title, information_type.renderContents())
+            InformationType.USERDATA.title, information_type.decode_contents())
         self.assertTextMatchesExpressionIgnoreWhitespace(
-            InformationType.USERDATA.description, description.renderContents())
+            InformationType.USERDATA.description,
+            description.decode_contents())
         self.assertIsNotNone(
             soup.find('a', id='privacy-link', attrs={'href': edit_url}))
 
diff --git a/lib/lp/code/browser/tests/test_gitrepository.py b/lib/lp/code/browser/tests/test_gitrepository.py
index 262d038..f6b3224 100644
--- a/lib/lp/code/browser/tests/test_gitrepository.py
+++ b/lib/lp/code/browser/tests/test_gitrepository.py
@@ -203,7 +203,7 @@ class TestGitRepositoryView(BrowserTestCase):
         browser = self.getViewBrowser(repository, no_login=True)
         directions = find_tag_by_id(browser.contents, "push-directions")
         login_person(self.user)
-        self.assertThat(directions.renderContents(), DocTestMatches(dedent("""
+        self.assertThat(directions.decode_contents(), DocTestMatches(dedent("""
             Only <a
             href="http://launchpad.test/~{owner.name}";>{owner.display_name}</a>
             can upload to this repository. If you are {owner.display_name}
@@ -219,7 +219,7 @@ class TestGitRepositoryView(BrowserTestCase):
         browser = self.getViewBrowser(repository, no_login=True)
         directions = find_tag_by_id(browser.contents, "push-directions")
         login_person(self.user)
-        self.assertThat(directions.renderContents(), DocTestMatches(dedent("""
+        self.assertThat(directions.decode_contents(), DocTestMatches(dedent("""
             Members of <a
             href="http://launchpad.test/~{owner.name}";>{owner.display_name}</a>
             can upload to this repository. <a href="+login">Log in</a> for
@@ -251,7 +251,7 @@ class TestGitRepositoryView(BrowserTestCase):
         browser = self.getViewBrowser(repository)
         directions = find_tag_by_id(browser.contents, "ssh-key-directions")
         login_person(self.user)
-        self.assertThat(directions.renderContents(), DocTestMatches(dedent("""
+        self.assertThat(directions.decode_contents(), DocTestMatches(dedent("""
             To authenticate with the Launchpad Git hosting service, you need
             to <a href="http://launchpad.test/~{user.name}/+editsshkeys";>
             register an SSH key</a>.
diff --git a/lib/lp/code/browser/tests/test_gitsubscription.py b/lib/lp/code/browser/tests/test_gitsubscription.py
index 2997c93..a887c38 100644
--- a/lib/lp/code/browser/tests/test_gitsubscription.py
+++ b/lib/lp/code/browser/tests/test_gitsubscription.py
@@ -87,7 +87,7 @@ class TestGitSubscriptionAddView(BrowserTestCase):
         subscriptions = find_tags_by_class(
             contents, 'repository-subscribers')[0]
         for subscriber in subscriptions.findAll('div'):
-            yield extract_text(subscriber.renderContents())
+            yield extract_text(subscriber.decode_contents())
 
     def _getInformationalMessage(self, contents):
         message = find_tags_by_class(contents, 'informational message')
diff --git a/lib/lp/code/browser/tests/test_sourcepackagerecipe.py b/lib/lp/code/browser/tests/test_sourcepackagerecipe.py
index 6352f30..94de042 100644
--- a/lib/lp/code/browser/tests/test_sourcepackagerecipe.py
+++ b/lib/lp/code/browser/tests/test_sourcepackagerecipe.py
@@ -140,10 +140,10 @@ class BzrMixin:
                 self.assertEqual(2, len(branch_links))
                 package_branches_info.append(
                     '%s%s' % (root_url, branch_links[0]['href']))
-                package_branches_info.append(branch_links[0].renderContents())
+                package_branches_info.append(branch_links[0].decode_contents())
                 package_branches_info.append(
                     '%s%s' % (root_url, branch_links[1]['href']))
-                package_branches_info.append(branch_links[1].renderContents())
+                package_branches_info.append(branch_links[1].decode_contents())
             expected_branch_info = []
             for branch_info in related_package_branch_info:
                 branch = branch_info[0]
@@ -170,9 +170,9 @@ class BzrMixin:
                 self.assertEqual(2, len(branch_links))
                 series_branches_info.append(
                     '%s%s' % (root_url, branch_links[0]['href']))
-                series_branches_info.append(branch_links[0].renderContents())
+                series_branches_info.append(branch_links[0].decode_contents())
                 series_branches_info.append(branch_links[1]['href'])
-                series_branches_info.append(branch_links[1].renderContents())
+                series_branches_info.append(branch_links[1].decode_contents())
             expected_branch_info = []
             for branch_info in related_series_branch_info:
                 branch = branch_info[0]
diff --git a/lib/lp/code/stories/branches/xx-branch-index.txt b/lib/lp/code/stories/branches/xx-branch-index.txt
index 9801d52..2ff7c1c 100644
--- a/lib/lp/code/stories/branches/xx-branch-index.txt
+++ b/lib/lp/code/stories/branches/xx-branch-index.txt
@@ -103,13 +103,13 @@ and author.
     ...     'http://code.launchpad.test/~name12/+branch/+junk/junk.dev')
     >>> commit_messages = find_tags_by_class(
     ...         browser.contents, 'revision-comment')
-    >>> print(commit_messages[0].p.renderContents())
+    >>> print(commit_messages[0].p.decode_contents())
     fix bug in bar
 
 When a commit message refers to a bug using the form "bug <bugnumber>",
 a link to that bug is created.
 
-    >>> print(commit_messages[3].p.renderContents())
+    >>> print(commit_messages[3].p.decode_contents())
     fix <a ...>bug 1</a>
 
 This link can be followed to the bug's details page.
diff --git a/lib/lp/code/stories/branches/xx-branch-listings.txt b/lib/lp/code/stories/branches/xx-branch-listings.txt
index fed83f7..9790507 100644
--- a/lib/lp/code/stories/branches/xx-branch-listings.txt
+++ b/lib/lp/code/stories/branches/xx-branch-listings.txt
@@ -23,7 +23,7 @@ branches in the listings.
     >>> browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
     >>> browser.open('http://code.launchpad.test/~name12')
     >>> links = find_tag_by_id(browser.contents, 'branch-batch-links')
-    >>> print(links.renderContents())
+    >>> print(links.decode_contents())
     <BLANKLINE>
     ...1...→...6...of 10 results...
 
@@ -50,7 +50,7 @@ and are really just branch metadata without the revisions behind them.
 
     >>> browser.getLink('Next').click()
     >>> links = find_tag_by_id(browser.contents, 'branch-batch-links')
-    >>> print(links.renderContents())
+    >>> print(links.decode_contents())
     <BLANKLINE>
     ...7...→...10...of 10 results...
 
@@ -112,7 +112,7 @@ button.
 Now all types of branches should be shown.
 
     >>> links = find_tag_by_id(browser.contents, 'branch-batch-links')
-    >>> print(links.renderContents())
+    >>> print(links.decode_contents())
     <BLANKLINE>
     ...1...→...6...of 12 results...
 
@@ -128,7 +128,7 @@ Now all types of branches should be shown.
 
     >>> browser.getLink('Next').click()
     >>> links = find_tag_by_id(browser.contents, 'branch-batch-links')
-    >>> print(links.renderContents())
+    >>> print(links.decode_contents())
     <BLANKLINE>
     ...7...→...12...of 12 results...
 
@@ -177,7 +177,7 @@ helpful message supplied.
     >>> browser.getControl(name='field.lifecycle').displayValue
     ['Mature']
     >>> message = find_tag_by_id(browser.contents, 'no-branch-message')
-    >>> print(message.renderContents())
+    >>> print(message.decode_contents())
     There are branches related to Launchpad Developers...
 
 Personal branch listings shouldn't show an option for sorting by "most
diff --git a/lib/lp/code/stories/branches/xx-branch-tag-cloud.txt b/lib/lp/code/stories/branches/xx-branch-tag-cloud.txt
index ff953cf..aed92a3 100644
--- a/lib/lp/code/stories/branches/xx-branch-tag-cloud.txt
+++ b/lib/lp/code/stories/branches/xx-branch-tag-cloud.txt
@@ -25,6 +25,6 @@ the link is shown.
 
     >>> tags = find_tag_by_id(anon_browser.contents, 'project-tags')
     >>> for anchor in tags.findAll('a'):
-    ...     print(anchor.renderContents(), ' '.join(anchor['class']))
+    ...     print(anchor.decode_contents(), ' '.join(anchor['class']))
     linux cloud-size-largest cloud-medium
     wibble cloud-size-smallest cloud-dark
diff --git a/lib/lp/code/stories/branches/xx-private-branch-listings.txt b/lib/lp/code/stories/branches/xx-private-branch-listings.txt
index e0ee96e..4fa90cc 100644
--- a/lib/lp/code/stories/branches/xx-private-branch-listings.txt
+++ b/lib/lp/code/stories/branches/xx-private-branch-listings.txt
@@ -50,16 +50,17 @@ registered branches.
     ...     browser.open('http://code.launchpad.test')
     ...     branches = find_tag_by_id(browser.contents, 'recently-registered')
     ...     for list_item in branches.ul.findAll('li'):
-    ...         print("%r" % list_item.renderContents())
+    ...         print("%r" % list_item.decode_contents())
 
 When there is no logged in user, only public branches should be visible.
 
     >>> print_recently_registered_branches(anon_browser)
-    '...~mark/+junk/testdoc...'
-    '...~name12/gnome-terminal/scanned...'
-    '...~name12/gnome-terminal/mirrored...'
-    '...~name12/gnome-terminal/pushed...'
-    '...~launchpad/gnome-terminal/launchpad...'
+    <BLANKLINE>
+    ...~mark/+junk/testdoc...
+    ...~name12/gnome-terminal/scanned...
+    ...~name12/gnome-terminal/mirrored...
+    ...~name12/gnome-terminal/pushed...
+    ...~launchpad/gnome-terminal/launchpad...
 
 Logged in users should only be able to see public branches, and private
 branches that they are subscribed to or are the owner of.
@@ -67,11 +68,12 @@ branches that they are subscribed to or are the owner of.
     >>> no_priv_browser = setupBrowser(
     ...     auth='Basic no-priv@xxxxxxxxxxxxx:test')
     >>> print_recently_registered_branches(no_priv_browser)
-    '...~no-priv/landscape/testing-branch...<span...class="sprite private"...'
-    '...~mark/+junk/testdoc...'
-    '...~name12/gnome-terminal/scanned...'
-    '...~name12/gnome-terminal/mirrored...'
-    '...~name12/gnome-terminal/pushed...'
+    <BLANKLINE>
+    ...~no-priv/landscape/testing-branch...<span...class="sprite private"...
+    ...~mark/+junk/testdoc...
+    ...~name12/gnome-terminal/scanned...
+    ...~name12/gnome-terminal/mirrored...
+    ...~name12/gnome-terminal/pushed...
 
 The private branches in the sample data belong to Landscape, and are
 subscribed to by Landscape developers.  Sample Person is a Landscape
@@ -80,20 +82,22 @@ developer.
     >>> landscape_dev_browser = setupBrowser(
     ...     auth='Basic test@xxxxxxxxxxxxx:test')
     >>> print_recently_registered_branches(landscape_dev_browser)
-    '...~name12/landscape/feature-x...<span...class="sprite private"...'
-    '...~landscape-developers/landscape/trunk...<span...class="sprite private"...'
-    '...~mark/+junk/testdoc...'
-    '...~name12/gnome-terminal/scanned...'
-    '...~name12/gnome-terminal/mirrored...'
+    <BLANKLINE>
+    ...~name12/landscape/feature-x...<span...class="sprite private"...
+    ...~landscape-developers/landscape/trunk...<span...class="sprite private"...
+    ...~mark/+junk/testdoc...
+    ...~name12/gnome-terminal/scanned...
+    ...~name12/gnome-terminal/mirrored...
 
 Launchpad administrators are able to see all private branches.
 
     >>> print_recently_registered_branches(admin_browser)
-    '...~no-priv/landscape/testing-branch...<span...class="sprite private"...'
-    '...~name12/landscape/feature-x...<span...class="sprite private"...'
-    '...~landscape-developers/landscape/trunk...<span...class="sprite private"...'
-    '...~mark/+junk/testdoc...'
-    '...~name12/gnome-terminal/scanned...'
+    <BLANKLINE>
+    ...~no-priv/landscape/testing-branch...<span...class="sprite private"...
+    ...~name12/landscape/feature-x...<span...class="sprite private"...
+    ...~landscape-developers/landscape/trunk...<span...class="sprite private"...
+    ...~mark/+junk/testdoc...
+    ...~name12/gnome-terminal/scanned...
 
 
 Landscape code listing page
diff --git a/lib/lp/code/stories/branches/xx-subscribing-branches.txt b/lib/lp/code/stories/branches/xx-subscribing-branches.txt
index 315a00e..c0e69d4 100644
--- a/lib/lp/code/stories/branches/xx-subscribing-branches.txt
+++ b/lib/lp/code/stories/branches/xx-subscribing-branches.txt
@@ -10,7 +10,7 @@ A quick helper function to list the subscribed people.
     ...         print(subscriptions)
     ...         return
     ...     for subscriber in subscriptions.findAll('div'):
-    ...         print(extract_text(subscriber.renderContents()))
+    ...         print(extract_text(subscriber.decode_contents()))
 
 Another to print the informational message.
 
diff --git a/lib/lp/code/stories/branches/xx-upload-directions.txt b/lib/lp/code/stories/branches/xx-upload-directions.txt
index 50cd5cc..bbe8717 100644
--- a/lib/lp/code/stories/branches/xx-upload-directions.txt
+++ b/lib/lp/code/stories/branches/xx-upload-directions.txt
@@ -49,7 +49,7 @@ the branch, and suggest logging in for directions.
     >>> anon_browser.open(branch_page)
     >>> content = anon_browser.contents
     >>> instructions = find_tag_by_id(content, 'upload-directions')
-    >>> print(instructions.renderContents())
+    >>> print(instructions.decode_contents())
     Only
     <a href="http://launchpad.test/~name12";>Sample Person</a>
     can upload to this branch. If you are Sample Person please
@@ -61,7 +61,7 @@ definitive that it is not possible to upload to this branch.
     >>> ddaa_browser.open(branch_page)
     >>> content = ddaa_browser.contents
     >>> instructions = find_tag_by_id(content, 'upload-directions')
-    >>> print(instructions.renderContents())
+    >>> print(instructions.decode_contents())
     <div...
     You cannot upload to this branch. Only
     <a href="http://launchpad.test/~name12";>Sample Person</a>
@@ -95,7 +95,7 @@ The branch page now displays directions and a link to register an SSH key.
     >>> name12_browser.open(branch_page)
     >>> content = name12_browser.contents
     >>> instructions = find_tag_by_id(content, 'ssh-key-directions')
-    >>> print(instructions.renderContents())
+    >>> print(instructions.decode_contents())
     To authenticate with the Launchpad branch upload service, you need to
     <a href="http://launchpad.test/~name12/+editsshkeys";>
     register an SSH key</a>.
@@ -154,7 +154,7 @@ owner is a team.
     >>> anon_browser.open(branch_page)
     >>> content = anon_browser.contents
     >>> instructions = find_tag_by_id(content, 'upload-directions')
-    >>> print(instructions.renderContents())
+    >>> print(instructions.decode_contents())
     Members of <a
     href="http://launchpad.test/~landscape-developers";>Landscape
     Developers</a> can upload to this branch. <a href="+login">Log in</a> for
@@ -166,7 +166,7 @@ a link to the team.
     >>> ddaa_browser.open(branch_page)
     >>> content = ddaa_browser.contents
     >>> instructions = find_tag_by_id(content, 'upload-directions')
-    >>> print(instructions.renderContents())
+    >>> print(instructions.decode_contents())
     <div...
     You cannot upload to this branch. Members of <a
     href="http://launchpad.test/~landscape-developers";>Landscape
diff --git a/lib/lp/code/stories/codeimport/xx-codeimport-machines.txt b/lib/lp/code/stories/codeimport/xx-codeimport-machines.txt
index efe5713..2abaa64 100644
--- a/lib/lp/code/stories/codeimport/xx-codeimport-machines.txt
+++ b/lib/lp/code/stories/codeimport/xx-codeimport-machines.txt
@@ -66,7 +66,7 @@ The heading of the page shows the import machine name, and the machine's
 current status.
 
     >>> def print_heading(browser):
-    ...     print(find_main_content(browser.contents).h1.renderContents())
+    ...     print(find_main_content(browser.contents).h1.decode_contents())
     >>> print_heading(browser)
     apollo: Online
 
@@ -103,7 +103,7 @@ the status of the machine.
     >>> admin_browser.open(browser.url)
 
     >>> print(find_tag_by_id(admin_browser.contents,
-    ...     'update-status').h2.renderContents())
+    ...     'update-status').h2.decode_contents())
     Update machine status
 
     >>> admin_browser.getControl('Reason').value = (
diff --git a/lib/lp/coop/answersbugs/stories/question-buglink.txt b/lib/lp/coop/answersbugs/stories/question-buglink.txt
index 3dfdaec..d21c7a9 100644
--- a/lib/lp/coop/answersbugs/stories/question-buglink.txt
+++ b/lib/lp/coop/answersbugs/stories/question-buglink.txt
@@ -118,7 +118,7 @@ have access to:
     >>> browser.getControl('Bug ID').value = '6'
     >>> browser.getControl('Link').click()
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     print tag.renderContents()
+    ...     print tag.decode_contents()
     There is 1 error.
     You are not allowed to link to private bug #6.
 
diff --git a/lib/lp/coop/answersbugs/stories/question-makebug.txt b/lib/lp/coop/answersbugs/stories/question-makebug.txt
index 6131a64..dfe77cc 100644
--- a/lib/lp/coop/answersbugs/stories/question-makebug.txt
+++ b/lib/lp/coop/answersbugs/stories/question-makebug.txt
@@ -54,7 +54,7 @@ questions' portlet:
 
   >>> portlet = find_portlet(browser.contents, 'Related questions')
   >>> for question in portlet.findAll('li', 'question-row'):
-  ...     print question.renderContents()
+  ...     print question.decode_contents()
   <span class="sprite question">Mozilla Firefox</span>: ...<a href=".../firefox/+question/2">Problem...
 
 A user can't create a bug report when a question has already a bug linked
diff --git a/lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt b/lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt
index c694d2b..23a661f 100644
--- a/lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt
+++ b/lib/lp/registry/stories/distributionmirror/xx-distribution-mirrors.txt
@@ -89,7 +89,7 @@ seen by distro owners, mirror admins of the distro or launchpad admins.
     >>> browser.url
     'http://launchpad.test/ubuntu/+disabledmirrors'
 
-    >>> print(find_tag_by_id(browser.contents, 'maincontent').renderContents())
+    >>> print(find_tag_by_id(browser.contents, 'maincontent').decode_contents())
     <BLANKLINE>
     ...We don't know of any Disabled Mirrors for this distribution...
 
diff --git a/lib/lp/registry/stories/distributionmirror/xx-reassign-distributionmirror.txt b/lib/lp/registry/stories/distributionmirror/xx-reassign-distributionmirror.txt
index 53940e3..28dd8d0 100644
--- a/lib/lp/registry/stories/distributionmirror/xx-reassign-distributionmirror.txt
+++ b/lib/lp/registry/stories/distributionmirror/xx-reassign-distributionmirror.txt
@@ -37,9 +37,9 @@ First we'll enter an unexistent name.
     >>> browser.url
     'http://launchpad.test/ubuntu/+mirror/archive-mirror/+reassign'
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     tag.renderContents()
-    'There is 1 error.'
-    "There's no person/team named 'unexistent-name' in Launchpad."
+    ...     print(tag.decode_contents())
+    There is 1 error.
+    There's no person/team named 'unexistent-name' in Launchpad.
 
 We also try to use the name of an unvalidated account, which can't be used as
 the owner of something.
@@ -52,9 +52,9 @@ the owner of something.
     >>> browser.url
     'http://launchpad.test/ubuntu/+mirror/archive-mirror/+reassign'
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     tag.renderContents()
-    'There is 1 error.'
-    "The person/team named 'matsubara' is not a valid owner for ..."
+    ...     print(tag.decode_contents())
+    There is 1 error.
+    The person/team named 'matsubara' is not a valid owner for ...
 
 Now we try to create a team using a name that is already taken.
 
@@ -64,9 +64,9 @@ Now we try to create a team using a name that is already taken.
     >>> browser.url
     'http://launchpad.test/ubuntu/+mirror/archive-mirror/+reassign'
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     tag.renderContents()
-    'There is 1 error.'
-    "There's already a person/team with the name 'name16' in Launchpad..."
+    ...     print(tag.decode_contents())
+    There is 1 error.
+    There's already a person/team with the name 'name16' in Launchpad...
 
 Okay, let's do it properly now and reassign it to an existing (and validated)
 account.
diff --git a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
index 5b2819a..950706d 100644
--- a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
+++ b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
@@ -454,7 +454,7 @@ Now Sample Person will deactivate their key...
 
     >>> browser.getControl('Deactivate Key').click()
     >>> for tag in find_main_content(browser.contents)('p', 'error message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     No key(s) selected for deactivation.
 
 
@@ -465,7 +465,7 @@ Now they select the checkbox and deactivate it.
     >>> browser.getControl('Deactivate Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Deactivated key(s): 1024D/A419AE861E88BC9E04B9C26FBA2B9389DFD20543
 
 
@@ -486,7 +486,7 @@ Now they'll request their key to be reactivated.
     >>> browser.getControl('Reactivate Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'error message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     No key(s) selected for reactivation.
 
     >>> browser.getControl(
@@ -494,7 +494,7 @@ Now they'll request their key to be reactivated.
     >>> browser.getControl('Reactivate Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     A message has been sent to test@xxxxxxxxxxxxx with instructions to reactivate...
 
 
diff --git a/lib/lp/registry/stories/mailinglists/subscriptions.txt b/lib/lp/registry/stories/mailinglists/subscriptions.txt
index 9cbc162..8d64413 100644
--- a/lib/lp/registry/stories/mailinglists/subscriptions.txt
+++ b/lib/lp/registry/stories/mailinglists/subscriptions.txt
@@ -223,7 +223,7 @@ Now Jdub can apply for team membership and mailing list access.
     'http://launchpad.test/~rosetta-admins'
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Your request to join Rosetta Administrators is awaiting approval.
     Your mailing list subscription is awaiting approval.
 
@@ -347,7 +347,7 @@ has an active mailing list.
     ['Preferred address']
     >>> for tag in find_tags_by_class(
     ...     carlos_browser.contents, 'informational'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Subscriptions updated.
 
     >>> carlos_browser.open('http://launchpad.test/~rosetta-admins')
diff --git a/lib/lp/registry/stories/person/xx-add-sshkey.txt b/lib/lp/registry/stories/person/xx-add-sshkey.txt
index 47e4140..afcb755 100644
--- a/lib/lp/registry/stories/person/xx-add-sshkey.txt
+++ b/lib/lp/registry/stories/person/xx-add-sshkey.txt
@@ -65,21 +65,21 @@ message will be shown.
     >>> browser.getControl(name='sshkey').value = sshkey
     >>> browser.getControl('Import Public Key').click()
     >>> for tag in find_main_content(browser.contents)('p', 'error message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Invalid public key
 
     >>> sshkey = "ssh-rsa foo"
     >>> browser.getControl(name='sshkey').value = sshkey
     >>> browser.getControl('Import Public Key').click()
     >>> for tag in find_main_content(browser.contents)('p', 'error message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Invalid public key
 
     >>> sshkey = "ssh-xsa foo comment"
     >>> browser.getControl(name='sshkey').value = sshkey
     >>> browser.getControl('Import Public Key').click()
     >>> for tag in find_main_content(browser.contents)('p', 'error message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Invalid public key
 
 
@@ -91,7 +91,7 @@ format.
     >>> browser.getControl('Import Public Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     SSH public key added.
 
     >>> sshkey = "ssh-dss AAAAB3NzaC1kc3MAAAEBAObOoy3fScSSQPHE/V6tPGoFzo5y1JRjDLcs8CNcvIHh9L27Qdj6h18AXn6MUCvvSCKm49aHpp1Xe14a6fmEIesjz6VopPWGENaOwRmhH6zfqM6imKUXQ0sq9p0znYb0TMjyRC0/AmqYneUF6FA2mVXygkGAkp/vDRPFQhPwnHpVD9TVPxHBQdHgM3bTo2TT+GoL7kw/s32ZiAH4OPvN5fN7bCkQWoUs/ySfoNbISMdvdtq07Rra2Biwzgjjs0ZcKbMicbDyYCe4gXlqK4wqcDfcwgrdqdG6NM0LUdekarWjnv0pMb6ttUl4U7e7Nf+eGkiTVItlppC8DyrnqC9SKCUAAAAVAOlEYNobJottyObVWQcrU8eAP4T5AAABAQDmJmL4DcQ1GVvw1Pjy57V0WUyGrOVBRVz7BwYBIvMA7xJCCvzd47mYWrWJkjV6O3tw2vG5oZx+BXE+ve8O6jL89CrwqncoUS8WHCojRmuUHTmtCCiRBCH+/68HMCusO3Blk//kQSsaqfIn+8Xa56Vr2SweSUlLgjvb51+89JJ13oDlUvdftW2GZu+grbmojqcoJ1LVAI5n0qsDItsFid46f8XfNzPeksasY9JbY5fKq/xf1KcgXL2F9XwmrDjFCuI4/xkJWNfGwaLKC/cbrJ1xmvPLl1/Hm5kNqgrwpNwHVOwyYSCUqXroU5PnpE9uydHUhjhtU2K2Hj0i7fOyxoxyAAABAQCpXKgd6lpTAEKm7ECY3TbJaTXPkNvAwg/2ud+PrtefHrVFFWrXrblSQhnmnc6ut8G3BsDzCljAIV2v+XcdOo+m8EViLf+Bi+gfbAIz4vdVepwQ2XHWUOTKk90i7Xqg4mUUDRIVw9ioNF0GAHbNlJTK3FWC3gstbCJU2hyV3UzgB95b6zqpUHeyn1RK4VAFYGY9fCIdZNy926HEart6uO/N6cO1ETw5B63kI8fTBjU7HLGgGXRjOv1APAqvKgry3tQD2WYkVJGRyYLjDK9d8nStUpwN5swI1xx2IWAbD+UCsRXAixn8s3mvpBD/jbnWjrzEensBc96jtiAsx2P5oXEd salgado@canario"
@@ -99,7 +99,7 @@ format.
     >>> browser.getControl('Import Public Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     SSH public key added.
 
     >>> sshkey = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJseCUmxVG7D6qh4JmhLp0Du4kScScJ9PtZ0LGHYHaURnRw9tbX1wwURAio8og6dbnT75CQ3TbUE/xJhxI0aFXE= salgado@canario"
@@ -107,7 +107,7 @@ format.
     >>> browser.getControl('Import Public Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     SSH public key added.
 
     >>> sshkey = "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBDUR0E0zCHRHJER6uzjfE/o0HAHFLcq/n8lp0duThpeIPsmo+wr3vHHuAAyOddOgkuQC8Lj8FzHlrOEYgXL6qa7FvpviE9YWUgmqVDa/yJbL/m6Mg8fvSIXlDJKmvOSv6g== salgado@canario"
@@ -115,7 +115,7 @@ format.
     >>> browser.getControl('Import Public Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     SSH public key added.
 
     >>> sshkey = "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAB3rpD+Ozb/kwUOqCZUXSiruAkIx6sNZLJyjJ0zxVTZSannaysCLxMQ/IiVxCd59+U2NaLduMzd93JcYDRlX3M5+AApY+3JjfSPo01Sb17HTLNSYU3RZWx0A3XJxm/YN+x/iuYZ3IziuAKeYMsNsdfHlO4/IWjw4Ruy0enW+QhWaY2qAQ== salgado@canario"
@@ -123,7 +123,7 @@ format.
     >>> browser.getControl('Import Public Key').click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     SSH public key added.
 
 Launchpad administrators are not allowed to poke at other user's ssh keys.
@@ -157,7 +157,7 @@ to edit his keys is on the page.
     >>> browser.getControl('Remove', index=0).click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Key ... removed
 
 If Salgado tries to remove a key that doesn't exist or one that doesn't
@@ -167,7 +167,7 @@ belong to him, it will fail with an error message.
     >>> browser.getControl('Remove', index=0).click()
     >>> soup = find_main_content(browser.contents)
     >>> for tag in soup('p', 'error message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Cannot remove a key that doesn't exist
 
     >>> browser.getControl(name='key', index=0).value = '1'
diff --git a/lib/lp/registry/stories/person/xx-person-editgpgkeys-invalid-key.txt b/lib/lp/registry/stories/person/xx-person-editgpgkeys-invalid-key.txt
index d3f8e52..3f7281a 100644
--- a/lib/lp/registry/stories/person/xx-person-editgpgkeys-invalid-key.txt
+++ b/lib/lp/registry/stories/person/xx-person-editgpgkeys-invalid-key.txt
@@ -38,7 +38,7 @@ Attempts to claim a revoked OpenPGP key fail:
     ...     '84D205F03E1E67096CB54E262BE83793AACCD97C')
     >>> browser.getControl('Import Key').click()
     >>> for tag in find_tags_by_class(browser.contents, 'error message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     <BLANKLINE>
     The key 84D205F03E1E67096CB54E262BE83793AACCD97C cannot be validated
     because it has been publicly revoked.
@@ -54,7 +54,7 @@ Attempts to claim an expired OpenPGP key also fail:
     ...     '0DD64D28E5F41138533495200E3DB4D402F53CC6')
     >>> browser.getControl('Import Key').click()
     >>> for tag in find_tags_by_class(browser.contents, 'error message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     <BLANKLINE>
     The key 0DD64D28E5F41138533495200E3DB4D402F53CC6 cannot be validated
     because it has expired. Change the expiry date (in a terminal, enter
@@ -89,7 +89,7 @@ Try to validate the revoked OpenPGP key:
     ...     'http://launchpad.test/token/%s/+validategpg' % revoked_key_token)
     >>> browser.getControl('Continue').click()
     >>> for tag in find_tags_by_class(browser.contents, 'error message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     The key 84D205F03E1E67096CB54E262BE83793AACCD97C cannot be validated
     because it has been publicly revoked.
@@ -105,7 +105,7 @@ Try to validate the revoked OpenPGP key:
     ...     'http://launchpad.test/token/%s/+validategpg' % expired_key_token)
     >>> browser.getControl('Continue').click()
     >>> for tag in find_tags_by_class(browser.contents, 'error message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     The key 0DD64D28E5F41138533495200E3DB4D402F53CC6 cannot be validated
     because it has expired. Change the expiry date (in a terminal, enter
diff --git a/lib/lp/registry/stories/person/xx-person-home.txt b/lib/lp/registry/stories/person/xx-person-home.txt
index e1de161..8332a01 100644
--- a/lib/lp/registry/stories/person/xx-person-home.txt
+++ b/lib/lp/registry/stories/person/xx-person-home.txt
@@ -188,7 +188,7 @@ most active and also the areas in which they worked on each project.
     >>> anon_browser.open('http://launchpad.test/~name16')
     >>> table = find_tag_by_id(anon_browser.contents, 'contributions')
     >>> for tr in table.findAll('tr'):
-    ...     print(tr.find('th').find('a').renderContents())
+    ...     print(tr.find('th').find('a').decode_contents())
     ...     for td in tr.findAll('td'):
     ...         img = td.find('img')
     ...         if img is not None:
diff --git a/lib/lp/registry/stories/person/xx-person-karma.txt b/lib/lp/registry/stories/person/xx-person-karma.txt
index fe6181c..8c1c741 100644
--- a/lib/lp/registry/stories/person/xx-person-karma.txt
+++ b/lib/lp/registry/stories/person/xx-person-karma.txt
@@ -12,7 +12,7 @@ profile page.
 
     >>> content = find_main_content(anon_browser.contents)
     >>> karma = find_tag_by_id(content, 'karma-total')
-    >>> print(karma.renderContents())
+    >>> print(karma.decode_contents())
     138
 
 The total karma points is also a link to the person's karma summary page.
diff --git a/lib/lp/registry/stories/person/xx-person-rdf.txt b/lib/lp/registry/stories/person/xx-person-rdf.txt
index fa390ab..701bc98 100644
--- a/lib/lp/registry/stories/person/xx-person-rdf.txt
+++ b/lib/lp/registry/stories/person/xx-person-rdf.txt
@@ -10,7 +10,7 @@ We export FOAF RDF metadata from the /~Person.name/+index document.
     >>> anon_browser.open("http://launchpad.test/~name16";)
     >>> strainer = SoupStrainer(['link'], {'type': ['application/rdf+xml']})
     >>> soup = BeautifulSoup(anon_browser.contents, parse_only=strainer)
-    >>> print(soup.renderContents())
+    >>> print(soup.decode_contents())
     <link href="+rdf" rel="meta" title="FOAF" type="application/rdf+xml"/>
 
 
@@ -104,8 +104,8 @@ Note how ascii and non-ascii names are rendered properly:
     >>> strainer = SoupStrainer(['foaf:name'])
     >>> soup = BeautifulSoup(anon_browser.contents, parse_only=strainer)
     >>> for tag in soup:
-    ...   tag.renderContents()
-    'Carlos Perell\xc3\xb3 Mar\xc3\xadn'
+    ...     print(tag.decode_contents())
+    Carlos Perelló Marín
 
 If the team has no active members no <foaf:member> elements will be
 present:
diff --git a/lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt b/lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt
index 259de37..ba88a94 100644
--- a/lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt
+++ b/lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt
@@ -66,13 +66,13 @@ they'll see an informative message. They can then reactivate them..
 
     >>> admin_browser.open('http://launchpad.test/firefox')
     >>> print(find_tag_by_id(
-    ...     admin_browser.contents, 'project-inactive').renderContents())
+    ...     admin_browser.contents, 'project-inactive').decode_contents())
      This project is currently inactive...
     >>> toggleProject('firefox')
 
     >>> admin_browser.open('http://launchpad.test/mozilla')
     >>> print(find_tag_by_id(
-    ...     admin_browser.contents, 'project-inactive').renderContents())
+    ...     admin_browser.contents, 'project-inactive').decode_contents())
      This project is currently inactive...
     >>> toggleProject('mozilla')
 
diff --git a/lib/lp/registry/stories/pillar/xx-pillar-sprints.txt b/lib/lp/registry/stories/pillar/xx-pillar-sprints.txt
index 940c933..6e24e3b 100644
--- a/lib/lp/registry/stories/pillar/xx-pillar-sprints.txt
+++ b/lib/lp/registry/stories/pillar/xx-pillar-sprints.txt
@@ -8,7 +8,7 @@ all events relevant to that pillar.
     ...     maincontent = find_tag_by_id(contents, 'maincontent')
     ...     for link in maincontent.findAll('a'):
     ...         if re.search('/sprints/[a-z0-9]', link['href']) is not None:
-    ...             print(link.renderContents())
+    ...             print(link.decode_contents())
 
     >>> anon_browser.open('http://launchpad.test/firefox/+sprints')
     >>> print_sprints(anon_browser.contents)
diff --git a/lib/lp/registry/stories/product/xx-launchpad-project-search.txt b/lib/lp/registry/stories/product/xx-launchpad-project-search.txt
index d1595e8..1a22992 100644
--- a/lib/lp/registry/stories/product/xx-launchpad-project-search.txt
+++ b/lib/lp/registry/stories/product/xx-launchpad-project-search.txt
@@ -74,7 +74,7 @@ some terms and do another search.
     ''
     >>> project_search.getControl('Search').click()
     >>> empty_search = find_tag_by_id(anon_browser.contents, 'empty-search-string')
-    >>> print(empty_search.renderContents())
+    >>> print(empty_search.decode_contents())
     <BLANKLINE>
     ...Enter one or more words related to the project you want to find.
     <BLANKLINE>
@@ -93,7 +93,7 @@ A similar page is available for only searching project groups.
     >>> tags = find_tags_by_class(
     ...     anon_browser.contents, "informational message")
     >>> for tag in tags:
-    ...   print(tag.renderContents())
+    ...   print(tag.decode_contents())
 
 The search results contains only project-groups.
 
diff --git a/lib/lp/registry/stories/product/xx-product-edit.txt b/lib/lp/registry/stories/product/xx-product-edit.txt
index 1b6a7c6..7ac560b 100644
--- a/lib/lp/registry/stories/product/xx-product-edit.txt
+++ b/lib/lp/registry/stories/product/xx-product-edit.txt
@@ -19,7 +19,7 @@ invalid project.
     >>> browser.getControl('Part of', index=0).value = 'asdasfasd'
     >>> browser.getControl(name='field.actions.change').click()
     >>> for message in find_tags_by_class(browser.contents, 'error'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There is 1 error.
     <BLANKLINE>
     ...
@@ -108,7 +108,7 @@ Now we try to change newproductname2's name to newproductname.
     >>> admin_browser.getControl('Name').value = 'newproductname'
     >>> admin_browser.getControl(name='field.actions.change').click()
     >>> for message in find_tags_by_class(admin_browser.contents, 'error'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There is 1 error.
     <BLANKLINE>
     ...
diff --git a/lib/lp/registry/stories/product/xx-product-reassignment-and-milestones.txt b/lib/lp/registry/stories/product/xx-product-reassignment-and-milestones.txt
index abb9ff1..a59227c 100644
--- a/lib/lp/registry/stories/product/xx-product-reassignment-and-milestones.txt
+++ b/lib/lp/registry/stories/product/xx-product-reassignment-and-milestones.txt
@@ -9,7 +9,7 @@ even if the user was trying to set the milestone value.
     >>> browser.getControl("Save Changes").click()
 
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     The milestone setting was ignored because you reassigned the bug
     to...Evolution...
 
@@ -37,6 +37,6 @@ milestone value, if one was set.
     >>> browser.getControl("Save Changes").click()
 
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     The Mozilla Firefox 1.0 milestone setting has been removed
     because you reassigned the bug to Evolution.
diff --git a/lib/lp/registry/stories/productrelease/xx-productrelease-basics.txt b/lib/lp/registry/stories/productrelease/xx-productrelease-basics.txt
index e33c07c..adf171a 100644
--- a/lib/lp/registry/stories/productrelease/xx-productrelease-basics.txt
+++ b/lib/lp/registry/stories/productrelease/xx-productrelease-basics.txt
@@ -96,8 +96,8 @@ has a productrelease.
     >>> browser.url
     'http://launchpad.test/firefox/trunk/1.0/+edit'
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     tag.renderContents()
-    'A project release already exists for this milestone.'
+    ...     print(tag.decode_contents())
+    A project release already exists for this milestone.
 
 
 == Editing a product release ==
diff --git a/lib/lp/registry/stories/productseries/xx-productseries-add-and-edit.txt b/lib/lp/registry/stories/productseries/xx-productseries-add-and-edit.txt
index 64c43bc..aed8cf7 100644
--- a/lib/lp/registry/stories/productseries/xx-productseries-add-and-edit.txt
+++ b/lib/lp/registry/stories/productseries/xx-productseries-add-and-edit.txt
@@ -26,7 +26,7 @@ But Sample Person will and be able to add a series.
     >>> print(browser.url)
     http://launchpad.test/firefox/+addseries
 
-    >>> print(find_main_content(browser.contents).find('h1').renderContents())
+    >>> print(find_main_content(browser.contents).find('h1').decode_contents())
     Register a new Mozilla Firefox release series
 
 After checking that the page +addseries is there, we try to add a new series.
@@ -110,7 +110,7 @@ should get a nice error message.
     >>> browser.getControl('Register Series').click()
 
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There is 1 error.
     unstable is already in use by another series.
 
diff --git a/lib/lp/registry/stories/productseries/xx-productseries-driver.txt b/lib/lp/registry/stories/productseries/xx-productseries-driver.txt
index 6910665..849946b 100644
--- a/lib/lp/registry/stories/productseries/xx-productseries-driver.txt
+++ b/lib/lp/registry/stories/productseries/xx-productseries-driver.txt
@@ -32,8 +32,8 @@ message explains that the driver changed.
     'http://launchpad.test/firefox/1.0'
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     tag.renderContents()
-    'Successfully changed the release manager to Guilherme Salgado'
+    ...     print(tag.decode_contents())
+    Successfully changed the release manager to Guilherme Salgado
 
 Sample Person and Guilherme Salgado are listed as the drivers of Firefox 1.0.
 
diff --git a/lib/lp/registry/stories/project/xx-project-driver.txt b/lib/lp/registry/stories/project/xx-project-driver.txt
index b6fc7b5..073c10f 100644
--- a/lib/lp/registry/stories/project/xx-project-driver.txt
+++ b/lib/lp/registry/stories/project/xx-project-driver.txt
@@ -18,8 +18,8 @@ message informs them of the driver change.
     'http://launchpad.test/gnome'
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     tag.renderContents()
-    'Successfully changed the driver to Sample Person'
+    ...     print(tag.decode_contents())
+    Successfully changed the driver to Sample Person
 
 Sample Person is listed as the driver of the project.
 
diff --git a/lib/lp/registry/stories/project/xx-project-index.txt b/lib/lp/registry/stories/project/xx-project-index.txt
index 9439d73..4f7da69 100644
--- a/lib/lp/registry/stories/project/xx-project-index.txt
+++ b/lib/lp/registry/stories/project/xx-project-index.txt
@@ -122,8 +122,9 @@ owner of the project group views it:
 
     >>> admin_browser.open('http://launchpad.test/a-test-group')
     >>> for warning in find_tags_by_class(admin_browser.contents, 'warning'):
-    ...     extract_text(warning.renderContents())
-    u'There are no projects registered for\nTest Group...'
+    ...     print(extract_text(warning.decode_contents()))
+    There are no projects registered for
+    Test Group...
 
 A link is included in the warning message which will take the admin user to
 the new product form for the project group.
diff --git a/lib/lp/registry/stories/team-polls/edit-options.txt b/lib/lp/registry/stories/team-polls/edit-options.txt
index 547f384..f62778b 100644
--- a/lib/lp/registry/stories/team-polls/edit-options.txt
+++ b/lib/lp/registry/stories/team-polls/edit-options.txt
@@ -51,7 +51,7 @@ been opened yet, he should be able to edit its options.
     >>> browser.url
     'http://launchpad.test/~ubuntu-team/+poll/not-yet-opened'
     >>> print(
-    ...     find_portlet(browser.contents, 'Voting options').renderContents())
+    ...     find_portlet(browser.contents, 'Voting options').decode_contents())
     <BLANKLINE>
     <h2>Voting options</h2>
     ...
diff --git a/lib/lp/registry/stories/team-polls/vote-poll.txt b/lib/lp/registry/stories/team-polls/vote-poll.txt
index aa9de0b..346ba8a 100644
--- a/lib/lp/registry/stories/team-polls/vote-poll.txt
+++ b/lib/lp/registry/stories/team-polls/vote-poll.txt
@@ -10,7 +10,7 @@ that they must use to see/change their vote afterwards.
     >>> browser.url
     'http://launchpad.test/~ubuntu-team/+poll/never-closes/+vote'
 
-    >>> print(find_tag_by_id(browser.contents, 'your-vote').renderContents())
+    >>> print(find_tag_by_id(browser.contents, 'your-vote').decode_contents())
     <BLANKLINE>
     ...
     <h2>Your current vote</h2>
@@ -26,11 +26,11 @@ that they must use to see/change their vote afterwards.
 
     >>> tags = find_tags_by_class(browser.contents, "informational message")
     >>> for tag in tags:
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Your vote has been recorded. If you want to view or change it later you
     must write down this key: ...
 
-    >>> print(find_tag_by_id(browser.contents, 'your-vote').renderContents())
+    >>> print(find_tag_by_id(browser.contents, 'your-vote').decode_contents())
     <BLANKLINE>
     ...
     <h2>Your current vote</h2>
@@ -44,7 +44,7 @@ Foo Bar will now vote on a poll with public votes.
     >>> browser.url
     'http://launchpad.test/~ubuntu-team/+poll/never-closes4/+vote'
 
-    >>> print(find_tag_by_id(browser.contents, 'your-vote').renderContents())
+    >>> print(find_tag_by_id(browser.contents, 'your-vote').decode_contents())
     <BLANKLINE>
     ...
     <h2>Your current vote</h2>
@@ -60,11 +60,11 @@ Foo Bar will now vote on a poll with public votes.
 
     >>> tags = find_tags_by_class(browser.contents, "informational message")
     >>> for tag in tags:
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Your vote was stored successfully. You can come back to this page at any
     time before this poll closes to view or change your vote, if you want.
 
-    >>> print(find_tag_by_id(browser.contents, 'your-vote').renderContents())
+    >>> print(find_tag_by_id(browser.contents, 'your-vote').decode_contents())
     <BLANKLINE>
     ...
     <h2>Your current vote</h2>
@@ -88,10 +88,10 @@ yet.
 
     >>> contents = team_admin_browser.contents
     >>> for tag in find_tags_by_class(contents, "informational message"):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     You chose not to vote yet.
 
-    >>> print(find_tag_by_id(contents, 'your-vote').renderContents())
+    >>> print(find_tag_by_id(contents, 'your-vote').decode_contents())
     <BLANKLINE>
     ...
     <h2>Your current vote</h2>
@@ -110,7 +110,7 @@ even if they guess the URL for the voting page.
     ...     'http://launchpad.test/~ubuntu-team/+poll/never-closes/+vote')
     >>> for tag in find_tags_by_class(
     ...     non_member_browser.contents, "informational message"):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     You can’t vote in this poll because you’re not a member of Ubuntu Team.
 
 
@@ -121,7 +121,7 @@ It's not possible to vote on closed polls, even if we manually craft the URL.
     >>> team_admin_browser.open(
     ...     'http://launchpad.test/~ubuntu-team/+poll/leader-2004')
     >>> print(find_tag_by_id(
-    ...     team_admin_browser.contents, 'maincontent').renderContents())
+    ...     team_admin_browser.contents, 'maincontent').decode_contents())
     <BLANKLINE>
     ...
     <h2>Voting has closed</h2>
@@ -130,7 +130,7 @@ It's not possible to vote on closed polls, even if we manually craft the URL.
     >>> team_admin_browser.open(
     ...     'http://launchpad.test/~ubuntu-team/+poll/leader-2004/+vote')
     >>> print(find_tag_by_id(
-    ...     team_admin_browser.contents, 'maincontent').renderContents())
+    ...     team_admin_browser.contents, 'maincontent').decode_contents())
     <BLANKLINE>
     ...
     <p class="informational message">
@@ -149,7 +149,7 @@ The same is true for condorcet polls too.
     >>> team_admin_browser.open(
     ...     'http://launchpad.test/~ubuntu-team/+poll/director-2004')
     >>> print(find_tag_by_id(
-    ...     team_admin_browser.contents, 'maincontent').renderContents())
+    ...     team_admin_browser.contents, 'maincontent').decode_contents())
     <BLANKLINE>
     ...
     <h2>Voting has closed</h2>
diff --git a/lib/lp/registry/stories/team/xx-team-membership.txt b/lib/lp/registry/stories/team/xx-team-membership.txt
index 2cebe0c..a2d7fb3 100644
--- a/lib/lp/registry/stories/team/xx-team-membership.txt
+++ b/lib/lp/registry/stories/team/xx-team-membership.txt
@@ -48,7 +48,7 @@ TestBrowser to manually re-enable the input. That's what the
 We get a nice error message
 
     >>> for tag in find_tags_by_class(browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Invalid expiration: Invalid date value
 
 Give up on change, nothing should have changed with Colin:
diff --git a/lib/lp/registry/stories/teammembership/xx-add-member.txt b/lib/lp/registry/stories/teammembership/xx-add-member.txt
index 686af11..80e0ead 100644
--- a/lib/lp/registry/stories/teammembership/xx-add-member.txt
+++ b/lib/lp/registry/stories/teammembership/xx-add-member.txt
@@ -13,7 +13,7 @@ Any administrator of a team can add new members to that team.
 
     >>> for tag in find_tags_by_class(browser.contents,
     ...                               'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Celso Providelo (cprov) has been added as a member of this team.
 
 Let's make sure that 'cprov' is now an Approved member of
@@ -44,7 +44,7 @@ become a member.
 
     >>> for tag in find_tags_by_class(browser.contents,
     ...                               'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Launchpad Developers (launchpad) has been invited to join this team.
 
 As we can see, the launchpad team will not be one of the team's active
@@ -147,7 +147,7 @@ First invite name20 to be a member of ubuntu-team.
 
     >>> for tag in find_tags_by_class(browser.contents,
     ...                               'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Warty Security Team (name20) has been invited to join this team.
 
 Open the invitations page with one admin browser.
@@ -169,7 +169,7 @@ Accept the invitation in the first browser.
 
     >>> for tag in find_tags_by_class(browser.contents,
     ...                               'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     This team is now a member of Ubuntu Team.
 
 Accepting the invitation in the second browser, redirects to the team page
@@ -181,5 +181,5 @@ and a message is displayed.
 
     >>> for tag in find_tags_by_class(second_browser.contents,
     ...                               'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     This invitation has already been processed.
diff --git a/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.txt b/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.txt
index eea7fb6..beb8e96 100644
--- a/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.txt
+++ b/lib/lp/registry/stories/teammembership/xx-member-renewed-membership.txt
@@ -117,7 +117,7 @@ Karl then renews his membership.
     'http://launchpad.test/~karl'
     >>> for tag in find_tags_by_class(
     ...         browser.contents, 'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Membership renewed until ...
 
 Karl can't renew it again, since it's now not set to expire soon.
@@ -169,7 +169,7 @@ now renew the membership.
     'http://launchpad.test/~landscape-developers'
     >>> for tag in find_tags_by_class(
     ...         browser.contents, 'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Membership renewed until ...
 
 If the user double clicks or goes back to a cached version of the page
@@ -184,7 +184,7 @@ provided no information as to whether the membership was renewed.
     'http://launchpad.test/~landscape-developers'
     >>> for tag in find_tags_by_class(
     ...         browser.contents, 'informational message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Membership renewed until ...
 
 When the page is loaded again, there is no form since the membership
diff --git a/lib/lp/registry/stories/teammembership/xx-teammembership.txt b/lib/lp/registry/stories/teammembership/xx-teammembership.txt
index 6080f1b..c6fe1d0 100644
--- a/lib/lp/registry/stories/teammembership/xx-teammembership.txt
+++ b/lib/lp/registry/stories/teammembership/xx-teammembership.txt
@@ -55,7 +55,7 @@ Karl will join the newly created team.
 Since this is an open team, he's automatically approved.
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     You have successfully joined your own team.
 
 Now, the link to join the team is not present anymore and the +join page will
@@ -68,7 +68,7 @@ team.
     LinkNotFoundError
     >>> browser.open('http://launchpad.test/~myemail/+join')
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     You are an active member of this team already.
 
 We have a 'Back' button, though, which just takes the user back to
@@ -99,7 +99,8 @@ approved, though.
     >>> browser.url
     'http://launchpad.test/~myemail/+join'
 
-    >>> print(find_tag_by_id(browser.contents, 'maincontent').renderContents())
+    >>> print(find_tag_by_id(
+    ...     browser.contents, 'maincontent').decode_contents())
     <BLANKLINE>
     ...
     One of this team's administrators will have to approve your membership
@@ -121,7 +122,7 @@ hit the 'Cancel' button, going back to the team's page...
     'http://launchpad.test/~myemail'
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Your request to join your own team is awaiting approval.
 
 Delegated teams also require approval of direct membership.
@@ -142,7 +143,8 @@ Delegated teams also require approval of direct membership.
     >>> browser.url
     'http://launchpad.test/~myemail/+join'
 
-    >>> print(find_tag_by_id(browser.contents, 'maincontent').renderContents())
+    >>> print(find_tag_by_id(
+    ...     browser.contents, 'maincontent').decode_contents())
     <BLANKLINE>
     ...
     One of this team's administrators will have to approve your membership
@@ -154,7 +156,7 @@ Delegated teams also require approval of direct membership.
     'http://launchpad.test/~myemail'
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Your request to join your own team is awaiting approval.
 
 If it was a restricted team, users wouldn't even see a link to join the team.
@@ -185,7 +187,7 @@ message explaining that this is a restricted team.
     'http://launchpad.test/~myemail/+join'
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     your own team is a restricted team.
     Only a team administrator can add new members.
 
@@ -207,12 +209,12 @@ be there at all.
 
     >>> contents = anon_browser.contents
     >>> for link in find_tag_by_id(contents, 'activemembers').findAll('a'):
-    ...     print(link.renderContents())
+    ...     print(link.decode_contents())
     Karl Tilbury
     Sample Person
 
     >>> for link in find_tag_by_id(contents, 'proposedmembers').findAll('a'):
-    ...     print(link.renderContents())
+    ...     print(link.decode_contents())
     Colin Watson
     James Blackwell
 
@@ -227,9 +229,9 @@ have been invited.
     >>> def print_members(contents, type):
     ...     table = find_tag_by_id(contents, type)
     ...     for link in table.findAll('a'):
-    ...         link_contents = six.ensure_text(link.renderContents())
+    ...         link_contents = link.decode_contents()
     ...         if link_contents != 'Edit' and not link.find('img'):
-    ...             print(link_contents.encode('ascii', 'replace'))
+    ...             print(link_contents)
 
     >>> browser.open('http://launchpad.test/~landscape-developers')
     >>> browser.getLink('All members').click()
@@ -264,7 +266,7 @@ without bounds, so they are paginated.
     >>> browser.open('http://launchpad.test/~admins/+members')
     >>> print_members(browser.contents, 'activemembers')
     Andrew Bennetts
-    Carlos Perell? Mar?n
+    Carlos Perelló Marín
     Dafydd Harries
     Daniel Henrique Debonzi
     Daniel Silverstone
@@ -415,5 +417,5 @@ error message:
     >>> browser2.url
     'http://launchpad.test/~myemail/+member/karl/+index'
     >>> for tag in find_tags_by_class(browser2.contents, 'error message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     The membership request for Karl Tilbury has already been processed.
diff --git a/lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt b/lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt
index 12dcb8d..5096ee7 100644
--- a/lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt
+++ b/lib/lp/soyuz/stories/ppa/xx-ppa-packages.txt
@@ -423,7 +423,7 @@ Anyone can see the build status for package in Celso's PPA.
     ...         built_icon = columns[-1].img['src']
     ...         built_text = columns[-1].a
     ...         if built_text is not None:
-    ...             built_text = built_text.renderContents()
+    ...             built_text = built_text.decode_contents()
     ...         print(name, built_icon, built_text)
 
     >>> print_build_status(anon_browser.contents)
diff --git a/lib/lp/testing/pages.py b/lib/lp/testing/pages.py
index 161a241..2efabd0 100644
--- a/lib/lp/testing/pages.py
+++ b/lib/lp/testing/pages.py
@@ -533,8 +533,8 @@ def print_comments(page):
     main_content = find_main_content(page)
     for comment in main_content('div', 'boardCommentBody'):
         for li_tag in comment('li'):
-            print("Attachment: %s" % li_tag.a.renderContents())
-        print(comment.div.renderContents())
+            print("Attachment: %s" % li_tag.a.decode_contents())
+        print(comment.div.decode_contents())
         print("-" * 40)
 
 
diff --git a/lib/lp/translations/stories/distroseries/xx-distroseries-translations.txt b/lib/lp/translations/stories/distroseries/xx-distroseries-translations.txt
index fa97ec4..9421546 100644
--- a/lib/lp/translations/stories/distroseries/xx-distroseries-translations.txt
+++ b/lib/lp/translations/stories/distroseries/xx-distroseries-translations.txt
@@ -84,7 +84,7 @@ the system tells them that they're not allowed to see those pages.
     ...
     HTTPError: HTTP Error 503: Service Unavailable
     >>> main_content = find_main_content(user_browser.contents)
-    >>> print(main_content.findNext('p').renderContents())
+    >>> print(main_content.findNext('p').decode_contents())
     Translations for this release series are not available yet.
 
     >>> user_browser.handleErrors = False
diff --git a/lib/lp/translations/stories/importqueue/xx-entry-error-output.txt b/lib/lp/translations/stories/importqueue/xx-entry-error-output.txt
index e34b789..85cb47e 100644
--- a/lib/lp/translations/stories/importqueue/xx-entry-error-output.txt
+++ b/lib/lp/translations/stories/importqueue/xx-entry-error-output.txt
@@ -41,6 +41,6 @@ The output is properly HTML-escaped, so is safe to display in this way.
     >>> entry.error_output = "<h1>Injection &amp; subterfuge</h1>"
     >>> admin_browser.open(entry_url)
     >>> output_panel = find_error_output(admin_browser)
-    >>> print(output_panel.renderContents())
+    >>> print(output_panel.decode_contents())
     Error output for this entry:
     ...&lt;h1&gt;Injection &amp;amp; subterfuge&lt;/h1&gt;...
diff --git a/lib/lp/translations/stories/importqueue/xx-translation-import-queue-filtering.txt b/lib/lp/translations/stories/importqueue/xx-translation-import-queue-filtering.txt
index 7910e05..9846322 100644
--- a/lib/lp/translations/stories/importqueue/xx-translation-import-queue-filtering.txt
+++ b/lib/lp/translations/stories/importqueue/xx-translation-import-queue-filtering.txt
@@ -286,7 +286,7 @@ Carlos uploads files for Evolution in Ubuntu Hoary.
     ...     BytesIO(b'foo'), 'application/x-po', 'foo.pot')
     >>> admin_browser.getControl('Upload').click()
     >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(extract_text(tag.renderContents()))
+    ...     print(extract_text(tag.decode_contents()))
     Thank you for your upload. It will be automatically reviewed...
 
     # Commit the transaction so librarian stores the uploaded file.
diff --git a/lib/lp/translations/stories/importqueue/xx-translation-import-queue-targets.txt b/lib/lp/translations/stories/importqueue/xx-translation-import-queue-targets.txt
index 6de7820..6efe24a 100644
--- a/lib/lp/translations/stories/importqueue/xx-translation-import-queue-targets.txt
+++ b/lib/lp/translations/stories/importqueue/xx-translation-import-queue-targets.txt
@@ -15,11 +15,11 @@ The import queue is linked from the translations page for distribution.
 There is no content for Ubuntu.
 
     >>> print(find_tag_by_id(
-    ...     user_browser.contents, 'description').renderContents().strip())
+    ...     user_browser.contents, 'description').decode_contents().strip())
     These translation related entries are imported, blocked, deleted or
     waiting to be imported in Launchpad for Ubuntu.
     >>> print(find_tag_by_id(
-    ...     user_browser.contents, 'no-entries').renderContents())
+    ...     user_browser.contents, 'no-entries').decode_contents())
     There are no entries that match this filtering.
     >>> find_tag_by_id(user_browser.contents, 'import-entries-list') is None
     True
@@ -36,11 +36,11 @@ And obviously, given that the ubuntu distribution had no content, Hoary, an
 Ubuntu distro series has also no content.
 
     >>> print(find_tag_by_id(
-    ...     user_browser.contents, 'description').renderContents().strip())
+    ...     user_browser.contents, 'description').decode_contents().strip())
     These translation related entries are imported, blocked, deleted or
     waiting to be imported in Launchpad for Hoary.
     >>> print(find_tag_by_id(
-    ...     user_browser.contents, 'no-entries').renderContents())
+    ...     user_browser.contents, 'no-entries').decode_contents())
     There are no entries that match this filtering.
     >>> find_tag_by_id(user_browser.contents, 'import-entries-list') is None
     True
@@ -56,7 +56,7 @@ The import queue is linked from the translations page for products.
 This time, we do have content for this product:
 
     >>> print(find_tag_by_id(
-    ...     user_browser.contents, 'description').renderContents().strip())
+    ...     user_browser.contents, 'description').decode_contents().strip())
     These translation related entries are imported, blocked, deleted or
     waiting to be imported in Launchpad for Evolution.
     >>> find_tag_by_id(user_browser.contents, 'no-entries') is None
@@ -89,7 +89,7 @@ The import queue is linked from the translations page for product series.
 This time, we do have content for this product:
 
     >>> print(find_tag_by_id(
-    ...     user_browser.contents, 'description').renderContents().strip())
+    ...     user_browser.contents, 'description').decode_contents().strip())
     These translation related entries are imported, blocked, deleted or
     waiting to be imported in Launchpad for trunk.
     >>> find_tag_by_id(user_browser.contents, 'no-entries') is None
@@ -123,7 +123,7 @@ The import queue is linked from the translations page for persons.
 This time, we do have content for this product:
 
     >>> print(find_tag_by_id(
-    ...     user_browser.contents, 'description').renderContents().strip())
+    ...     user_browser.contents, 'description').decode_contents().strip())
     These translation related entries are imported, blocked, deleted or
     waiting to be imported in Launchpad for Foo Bar.
     >>> find_tag_by_id(user_browser.contents, 'no-entries') is None
diff --git a/lib/lp/translations/stories/importqueue/xx-translation-import-queue.txt b/lib/lp/translations/stories/importqueue/xx-translation-import-queue.txt
index 9c15385..f119ac0 100644
--- a/lib/lp/translations/stories/importqueue/xx-translation-import-queue.txt
+++ b/lib/lp/translations/stories/importqueue/xx-translation-import-queue.txt
@@ -31,7 +31,7 @@ translations.
     True
     >>> for tag in find_tags_by_class(ff_owner_browser.contents,
     ...                               'informational message'):
-    ...     print(extract_text(tag.renderContents()))
+    ...     print(extract_text(tag.decode_contents()))
     Thank you for your upload. 2 files from the tarball will be automatically
     reviewed in the next few hours...
 
@@ -119,7 +119,7 @@ Now, we attach a new file to an already existing translation resource.
   >>> print(browser.url)
   http://translations.launchpad.test/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/+upload
   >>> for tag in find_tags_by_class(browser.contents, 'message'):
-  ...     print(tag.renderContents())
+  ...     print(tag.decode_contents())
   Thank you for your upload.  It will be automatically reviewed...
 
 The import queue should have three additional entries with the last upload as
diff --git a/lib/lp/translations/stories/productseries/xx-productseries-export-to-branch.txt b/lib/lp/translations/stories/productseries/xx-productseries-export-to-branch.txt
index 458a1bb..c7e2a65 100644
--- a/lib/lp/translations/stories/productseries/xx-productseries-export-to-branch.txt
+++ b/lib/lp/translations/stories/productseries/xx-productseries-export-to-branch.txt
@@ -35,7 +35,7 @@ branch.
     ...     The browser must be pointing at the settings page.
     ...     """
     ...     tag = find_tag_by_id(browser.contents, 'translations-branch')
-    ...     return tag.renderContents()
+    ...     return tag.decode_contents()
 
 A project owner sets a translations branch from the series' translations
 settings page.
diff --git a/lib/lp/translations/stories/standalone/custom-language-codes.txt b/lib/lp/translations/stories/standalone/custom-language-codes.txt
index 6132599..34f104d 100644
--- a/lib/lp/translations/stories/standalone/custom-language-codes.txt
+++ b/lib/lp/translations/stories/standalone/custom-language-codes.txt
@@ -41,7 +41,7 @@ main translations page.
 
     >>> owner_browser.open(product_page)
     >>> tag = find_custom_language_codes_link(owner_browser)
-    >>> print(extract_text(tag.renderContents()))
+    >>> print(extract_text(tag.decode_contents()))
     If necessary, you may
     define custom language codes
     for this project.
@@ -50,7 +50,7 @@ Translation admins also have access to this link.
 
     >>> rosetta_admin_browser.open(product_page)
     >>> tag = find_custom_language_codes_link(rosetta_admin_browser)
-    >>> print(extract_text(tag.renderContents()))
+    >>> print(extract_text(tag.decode_contents()))
     If necessary, you may
     define custom language codes
     for this project.
@@ -69,7 +69,7 @@ Other users don't see this link.
 Initially the page shows no custom language codes for the project.
 
     >>> tag = find_tag_by_id(owner_browser.contents, 'empty')
-    >>> print(extract_text(tag.renderContents()))
+    >>> print(extract_text(tag.decode_contents()))
     No custom language codes have been defined.
 
 There is a link to add a custom language code.
@@ -88,7 +88,7 @@ code is now shown.
     True
 
     >>> tag = find_tag_by_id(owner_browser.contents, 'nonempty')
-    >>> print(extract_text(tag.renderContents()))
+    >>> print(extract_text(tag.decode_contents()))
     Foo uses the following custom language codes:
     Code...     ...maps to language
     no          Norwegian Nynorsk
@@ -130,7 +130,7 @@ This leads back to the overview page.
     True
 
     >>> tag = find_tag_by_id(owner_browser.contents, 'empty')
-    >>> print(extract_text(tag.renderContents()))
+    >>> print(extract_text(tag.decode_contents()))
     No custom language codes have been defined.
 
 
@@ -143,7 +143,7 @@ This can be convenient for debugging.
     >>> user_browser.open(custom_language_codes_page)
 
     >>> tag = find_tag_by_id(user_browser.contents, 'empty')
-    >>> print(extract_text(tag.renderContents()))
+    >>> print(extract_text(tag.decode_contents()))
     No custom language codes have been defined.
 
 However all they get is a read-only version of the page.
@@ -170,7 +170,7 @@ unprivileged user can't remove it.
 
     >>> user_browser.open(custom_language_codes_page)
     >>> tag = find_tag_by_id(user_browser.contents, 'nonempty')
-    >>> print(extract_text(tag.renderContents()))
+    >>> print(extract_text(tag.decode_contents()))
     Foo uses the following custom language codes:
     Code...     ...maps to language
     no          Norwegian Nynorsk
@@ -231,7 +231,7 @@ Of course in this case, the notice about there being no custom language
 codes talks about a package, not a project.
 
     >>> tag = find_custom_language_codes_link(translations_browser)
-    >>> print(extract_text(tag.renderContents()))
+    >>> print(extract_text(tag.decode_contents()))
     If necessary, you may
     define custom language codes
     for this package.
@@ -240,7 +240,7 @@ codes talks about a package, not a project.
     >>> custom_language_codes_page = translations_browser.url
 
     >>> tag = find_tag_by_id(translations_browser.contents, 'empty')
-    >>> print(extract_text(tag.renderContents()))
+    >>> print(extract_text(tag.decode_contents()))
     No custom language codes have been defined.
 
 A translations admin can add a language code.
@@ -255,7 +255,7 @@ A translations admin can add a language code.
 The language code is displayed.
 
     >>> tag = find_tag_by_id(translations_browser.contents, 'nonempty')
-    >>> print(extract_text(tag.renderContents()))
+    >>> print(extract_text(tag.decode_contents()))
     bar in Distro uses the following custom language codes:
     Code...     ...maps to language
     pt-br       Portuguese (Brazil)
@@ -265,14 +265,14 @@ release series of the same distribution.
 
     >>> translations_browser.open(page_in_other_series)
     >>> tag = find_custom_language_codes_link(translations_browser)
-    >>> print(extract_text(tag.renderContents()))
+    >>> print(extract_text(tag.decode_contents()))
     If necessary, you may
     define custom language codes
     for this package.
 
     >>> translations_browser.getLink("define custom language codes").click()
     >>> tag = find_tag_by_id(translations_browser.contents, 'nonempty')
-    >>> print(extract_text(tag.renderContents()))
+    >>> print(extract_text(tag.decode_contents()))
     bar in Distro uses the following custom language codes:
     Code...     ...maps to language
     pt-br       Portuguese (Brazil)
@@ -288,5 +288,5 @@ The new code has a link there...
     >>> translations_browser.getControl("Remove").click()
 
     >>> tag = find_tag_by_id(translations_browser.contents, 'empty')
-    >>> print(extract_text(tag.renderContents()))
+    >>> print(extract_text(tag.decode_contents()))
     No custom language codes have been defined.
diff --git a/lib/lp/translations/stories/standalone/xx-language.txt b/lib/lp/translations/stories/standalone/xx-language.txt
index 53ab6cb..97cb4f0 100644
--- a/lib/lp/translations/stories/standalone/xx-language.txt
+++ b/lib/lp/translations/stories/standalone/xx-language.txt
@@ -48,7 +48,7 @@ the system detects it and warns the user.
     http://translations.launchpad.test/+languages/+add
 
     >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     There is already a language with that code.
 
@@ -113,7 +113,7 @@ about the selected language.
     http://translations.launchpad.test/+languages/es
 
     >>> print(extract_text(find_portlet(browser.contents, 'Plural forms'
-    ...     ).renderContents()))
+    ...     ).decode_contents()))
     Plural forms
     Spanish has 2 plural forms:
     Form 0 for 1.
@@ -166,7 +166,7 @@ form.
 
     >>> browser.open('http://translations.launchpad.test/+languages/ab')
     >>> print(extract_text(find_portlet(browser.contents, 'Plural forms'
-    ...     ).renderContents()))
+    ...     ).decode_contents()))
     Plural forms
     Unfortunately, Launchpad doesn't know the plural
     form information for this language...
@@ -265,7 +265,7 @@ so the user can fix it.
     http://translations.launchpad.test/+languages/es/+admin
 
     >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     There is already a language with that code.
 
diff --git a/lib/lp/translations/stories/standalone/xx-person-editlanguages.txt b/lib/lp/translations/stories/standalone/xx-person-editlanguages.txt
index 9b6e020..022536d 100644
--- a/lib/lp/translations/stories/standalone/xx-person-editlanguages.txt
+++ b/lib/lp/translations/stories/standalone/xx-person-editlanguages.txt
@@ -54,7 +54,7 @@ his two translatable languages. The page also displays two messages
 confirming his changes.
 
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     Added Welsh to your preferred languages.<br/>Removed Spanish from your
     preferred languages.
 
@@ -196,7 +196,7 @@ page will nag you about it.
     ...     """Return the nag message as shown in browser, if present."""
     ...     tag = find_tag_by_id(browser.contents, 'no-languages')
     ...     if tag:
-    ...         return tag.renderContents()
+    ...         return tag.decode_contents()
     ...     else:
     ...         return None
 
diff --git a/lib/lp/translations/stories/standalone/xx-pofile-export.txt b/lib/lp/translations/stories/standalone/xx-pofile-export.txt
index fef4485..7aa1661 100644
--- a/lib/lp/translations/stories/standalone/xx-pofile-export.txt
+++ b/lib/lp/translations/stories/standalone/xx-pofile-export.txt
@@ -37,8 +37,8 @@ If we POST the page, it should add the request to the queue.
     http://translatio.../ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es
 
     >>> for tag in find_tags_by_class(user_browser.contents, 'informational'):
-    ...     tag.renderContents()
-    'Your request has been received. Expect to receive an email shortly.'
+    ...     print(tag.decode_contents())
+    Your request has been received. Expect to receive an email shortly.
 
 Let's be sure that we can request po files without translations as the
 No Privileges Person.
@@ -55,7 +55,7 @@ No Privileges Person.
 
     >>> for tag in find_tags_by_class(
     ...     user_browser.contents, 'informational'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Your request has been received. Expect to receive an email shortly.
 
 If the POFile first has to be created, the requester becomes its owner.
@@ -76,8 +76,8 @@ evolution yet; it will be created at the moment the export is requested.
     http://translations.launchpad.test/evolution/trunk/+pots/evolution-2.2/sv
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     tag.renderContents()
-    'Your request has been received. Expect to receive an email shortly.'
+    ...     print(tag.decode_contents())
+    Your request has been received. Expect to receive an email shortly.
 
     >>> browser.open(
     ...     'http://translations.launchpad.test/evolution/trunk/+pots/'
@@ -101,5 +101,5 @@ https://launchpad.net/rosetta/+bug/1558).
     http://translations.launchpad.test/evolution/trunk/+pots/evolution-2.2/sv
 
     >>> for tag in find_tags_by_class(browser.contents, 'informational'):
-    ...     tag.renderContents()
-    'Your request has been received. Expect to receive an email shortly.'
+    ...     print(tag.decode_contents())
+    Your request has been received. Expect to receive an email shortly.
diff --git a/lib/lp/translations/stories/standalone/xx-pofile-translate-gettext-error-middle-page.txt b/lib/lp/translations/stories/standalone/xx-pofile-translate-gettext-error-middle-page.txt
index 574be01..5e610de 100644
--- a/lib/lp/translations/stories/standalone/xx-pofile-translate-gettext-error-middle-page.txt
+++ b/lib/lp/translations/stories/standalone/xx-pofile-translate-gettext-error-middle-page.txt
@@ -37,7 +37,7 @@ We remain at the same page:
 The valid translation is stored:
 
   >>> print(find_tag_by_id(
-  ...     browser.contents, 'msgset_140_es_translation_0').renderContents())
+  ...     browser.contents, 'msgset_140_es_translation_0').decode_contents())
   Foo
 
 And the error is noted in the page.
diff --git a/lib/lp/translations/stories/standalone/xx-pofile-translate-html-tags-escape.txt b/lib/lp/translations/stories/standalone/xx-pofile-translate-html-tags-escape.txt
index e528f7f..583ceb1 100644
--- a/lib/lp/translations/stories/standalone/xx-pofile-translate-html-tags-escape.txt
+++ b/lib/lp/translations/stories/standalone/xx-pofile-translate-html-tags-escape.txt
@@ -30,6 +30,6 @@ And we can see that we escaped the tags:
 
   >>> text = find_tag_by_id(
   ...     user_browser.contents, 'msgset_67_hr_translation_0')
-  >>> print(extract_text(text.renderContents()))
+  >>> print(extract_text(text.decode_contents()))
   Upotreba:
   %s [opcije] &lt;foo&gt; [&lt;etiketa&gt;]%s%s%s
diff --git a/lib/lp/translations/stories/standalone/xx-pofile-translate-message-filtering.txt b/lib/lp/translations/stories/standalone/xx-pofile-translate-message-filtering.txt
index e2de6c3..4fca5f4 100644
--- a/lib/lp/translations/stories/standalone/xx-pofile-translate-message-filtering.txt
+++ b/lib/lp/translations/stories/standalone/xx-pofile-translate-message-filtering.txt
@@ -35,7 +35,7 @@ messages in the batches of 10 items.
     ...     for msgset_id in sorted(translations.keys()):
     ...         translation = translations[msgset_id]
     ...         print("%d: '%s'" % (
-    ...             msgset_id, translation.renderContents().strip()))
+    ...             msgset_id, translation.decode_contents().strip()))
 
 
 Filters
@@ -358,7 +358,7 @@ conversion specifications the original message has, and is shown an
 error.
 
     >>> print(find_tag_by_id(
-    ...     user_browser.contents, 'msgset_142_singular').renderContents())
+    ...     user_browser.contents, 'msgset_142_singular').decode_contents())
     Migrating ...%s...
 
     >>> user_browser.getControl(
@@ -390,7 +390,7 @@ The exact same batch of messages is shown again, but with the error.
     142: '(no translation yet)'
 
     >>> for tag in find_tags_by_class(user_browser.contents, 'error'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is an error in a translation you provided.
     Please correct it before continuing.
     ...Error in Translation:...
diff --git a/lib/lp/translations/stories/standalone/xx-pofile-translate-private-issues.txt b/lib/lp/translations/stories/standalone/xx-pofile-translate-private-issues.txt
index 2963894..d79450e 100644
--- a/lib/lp/translations/stories/standalone/xx-pofile-translate-private-issues.txt
+++ b/lib/lp/translations/stories/standalone/xx-pofile-translate-private-issues.txt
@@ -10,12 +10,12 @@ if you log in.
 The GNOME standard string for credits is well handled.
 
   >>> msgid = find_tag_by_id(browser.contents, 'msgset_199_singular')
-  >>> print(msgid.renderContents())
+  >>> print(msgid.decode_contents())
   translation-credits
 
   >>> translation = find_tag_by_id(
   ...     browser.contents, 'msgset_199_es_translation_0')
-  >>> print(translation.renderContents())
+  >>> print(translation.decode_contents())
   To prevent privacy issues, this translation is not available to anonymous
   users,<br/> if you want to see it, please, <a href="+login">log in</a>
   first.
@@ -23,13 +23,13 @@ The GNOME standard string for credits is well handled.
 And the same for KDE one.
 
   >>> msgid = find_tag_by_id(browser.contents, 'msgset_200_singular')
-  >>> print(msgid.renderContents())
+  >>> print(msgid.decode_contents())
   _: EMAIL OF TRANSLATORS<img alt="" src="/@@/translation-newline"/><br/>
   Your emails
 
   >>> translation = find_tag_by_id(
   ...     browser.contents, 'msgset_200_es_translation_0')
-  >>> print(translation.renderContents())
+  >>> print(translation.decode_contents())
   To prevent privacy issues, this translation is not available to anonymous
   users,<br/> if you want to see it, please, <a href="+login">log in</a>
   first.
@@ -50,25 +50,25 @@ But, if you are logged in, the system will show you the data.
 The GNOME standard string for credits is now available:
 
   >>> msgid = find_tag_by_id(user_browser.contents, 'msgset_199_singular')
-  >>> print(msgid.renderContents())
+  >>> print(msgid.decode_contents())
   translation-credits
 
   >>> translation = find_tag_by_id(
   ...     user_browser.contents, 'msgset_199_es_translation_0')
-  >>> print(extract_text(translation.renderContents()))
+  >>> print(extract_text(translation.decode_contents()))
   Launchpad Contributions:
   Carlos ... http://translations.launchpad.test/~carlos
 
 And the same for KDE one.
 
   >>> msgid = find_tag_by_id(user_browser.contents, 'msgset_200_singular')
-  >>> print(msgid.renderContents())
+  >>> print(msgid.decode_contents())
   _: EMAIL OF TRANSLATORS<img alt="" src="/@@/translation-newline"/><br/>
   Your emails
 
   >>> translation = find_tag_by_id(
   ...     user_browser.contents, 'msgset_200_es_translation_0')
-  >>> print(translation.renderContents())
+  >>> print(translation.decode_contents())
   ,,carlos@xxxxxxxxxxxxx
 
 Also, suggestions should not appear.
diff --git a/lib/lp/translations/stories/standalone/xx-product-export.txt b/lib/lp/translations/stories/standalone/xx-product-export.txt
index b06a81f..9291933 100644
--- a/lib/lp/translations/stories/standalone/xx-product-export.txt
+++ b/lib/lp/translations/stories/standalone/xx-product-export.txt
@@ -80,7 +80,7 @@ We can't see its placeholder in non-development mode:
     >>> anon_browser.open('http://translations.launchpad.test/evolution/')
     >>> for tag in find_tags_by_class(
     ...     anon_browser.contents, 'menu-link-translationdownload'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
 
     # Reset global configuration...
     >>> config.devmode = True
diff --git a/lib/lp/translations/stories/standalone/xx-product-translations.txt b/lib/lp/translations/stories/standalone/xx-product-translations.txt
index 6692e3a..c7b184d 100644
--- a/lib/lp/translations/stories/standalone/xx-product-translations.txt
+++ b/lib/lp/translations/stories/standalone/xx-product-translations.txt
@@ -172,7 +172,7 @@ The page mentions which product series should be translated.
     ...         browser.contents, 'translation-recommendation')
     ...     if tag is None:
     ...         return None
-    ...     return extract_text(tag.renderContents())
+    ...     return extract_text(tag.decode_contents())
 
     >>> product_url = 'http://translations.launchpad.test/evolution'
 
diff --git a/lib/lp/translations/stories/standalone/xx-products-with-translations.txt b/lib/lp/translations/stories/standalone/xx-products-with-translations.txt
index c04aa46..aad7698 100644
--- a/lib/lp/translations/stories/standalone/xx-products-with-translations.txt
+++ b/lib/lp/translations/stories/standalone/xx-products-with-translations.txt
@@ -24,7 +24,7 @@ Launchpad.
     ...     'http://translations.launchpad.test/'
     ...     'translations/+products-with-translations')
 
-    >>> print(find_main_content(browser.contents).renderContents())
+    >>> print(find_main_content(browser.contents).decode_contents())
     <...>
     ... of 2 results
     ...Evolution...
diff --git a/lib/lp/translations/stories/standalone/xx-template-description-escaping.txt b/lib/lp/translations/stories/standalone/xx-template-description-escaping.txt
index b7daddf..c96ec8d 100644
--- a/lib/lp/translations/stories/standalone/xx-template-description-escaping.txt
+++ b/lib/lp/translations/stories/standalone/xx-template-description-escaping.txt
@@ -24,7 +24,7 @@ IP address, since we'll use that later.
     >>> def find_description(url):
     ...     user_browser.open(url)
     ...     main = find_main_content(user_browser.contents)
-    ...     return re.findall("See[^!]*!", main.renderContents(), re.DOTALL)
+    ...     return re.findall("See[^!]*!", main.decode_contents(), re.DOTALL)
 
 The template's description is linkified, so the URL is clickable.
 
diff --git a/lib/lp/translations/stories/standalone/xx-translation-access-display.txt b/lib/lp/translations/stories/standalone/xx-translation-access-display.txt
index adcc0bd..1f2c0f9 100644
--- a/lib/lp/translations/stories/standalone/xx-translation-access-display.txt
+++ b/lib/lp/translations/stories/standalone/xx-translation-access-display.txt
@@ -10,7 +10,7 @@ translation, and also reminds him that he has full editing privileges.
     ...     if tag is None:
     ...         print('None')
     ...     else:
-    ...         print(tag.renderContents())
+    ...         print(tag.decode_contents())
 
     >>> admin_browser.open(
     ...     'http://translations.launchpad.test/'
@@ -73,7 +73,7 @@ If the two groups are identical, however, it is only listed once.
     ...     'http://translations.launchpad.test/'
     ...     'evolution/trunk/+pots/evolution-2.2/es/+translate')
     >>> managers_tag = find_tag_by_id(
-    ...     admin_browser.contents, 'translation-managers').renderContents()
+    ...     admin_browser.contents, 'translation-managers').decode_contents()
     >>> print(re.search(',\s+and', managers_tag))
     None
 
diff --git a/lib/lp/translations/stories/standalone/xx-translationmessage-translate.txt b/lib/lp/translations/stories/standalone/xx-translationmessage-translate.txt
index e7fb942..34ff287 100644
--- a/lib/lp/translations/stories/standalone/xx-translationmessage-translate.txt
+++ b/lib/lp/translations/stories/standalone/xx-translationmessage-translate.txt
@@ -190,16 +190,16 @@ Check that the message #13 is without translation.
 
 First what we represent in the form when there is no translation:
 
-    >>> print(find_tag_by_id(browser.contents, 'msgset_142').renderContents())
+    >>> print(find_tag_by_id(browser.contents, 'msgset_142').decode_contents())
     13.
     <input name="msgset_142" type="hidden"/>
 
     >>> print(find_tag_by_id(
-    ...     browser.contents, 'msgset_142_singular').renderContents())
+    ...     browser.contents, 'msgset_142_singular').decode_contents())
     Migrating `<code>%s</code>':
 
     >>> print(find_tag_by_id(
-    ...     browser.contents, 'msgset_142_es_translation_0').renderContents())
+    ...     browser.contents, 'msgset_142_es_translation_0').decode_contents())
     (no translation yet)
 
 And also, we don't get anyone as the Last translator because there is no
@@ -244,16 +244,16 @@ Let's submit an invalid value for this message #13.
 
 The message is still without translation:
 
-    >>> print(find_tag_by_id(browser.contents, 'msgset_142').renderContents())
+    >>> print(find_tag_by_id(browser.contents, 'msgset_142').decode_contents())
     13.
     <input name="msgset_142" type="hidden"/>
 
     >>> print(find_tag_by_id(
-    ...     browser.contents, 'msgset_142_singular').renderContents())
+    ...     browser.contents, 'msgset_142_singular').decode_contents())
     Migrating `<code>%s</code>':
 
     >>> print(find_tag_by_id(
-    ...     browser.contents, 'msgset_142_es_translation_0').renderContents())
+    ...     browser.contents, 'msgset_142_es_translation_0').decode_contents())
     (no translation yet)
 
 And now a good submit.
@@ -278,16 +278,16 @@ Now, it has the submitted value.
 
 Check that the message #13 has the new value we submitted.
 
-    >>> print(find_tag_by_id(browser.contents, 'msgset_142').renderContents())
+    >>> print(find_tag_by_id(browser.contents, 'msgset_142').decode_contents())
     13.
     <input name="msgset_142" type="hidden"/>
 
     >>> print(find_tag_by_id(
-    ...     browser.contents, 'msgset_142_singular').renderContents())
+    ...     browser.contents, 'msgset_142_singular').decode_contents())
     Migrating `<code>%s</code>':
 
     >>> print(find_tag_by_id(
-    ...     browser.contents, 'msgset_142_es_translation_0').renderContents())
+    ...     browser.contents, 'msgset_142_es_translation_0').decode_contents())
     foo <code>%s</code>
 
 And now, we get the translator and reviewer, who happen to be the same
@@ -441,17 +441,17 @@ We submit it
 Also, we should still have previous translation:
 
     >>> print(find_tag_by_id(
-    ...     slow_submission.contents, 'msgset_143').renderContents())
+    ...     slow_submission.contents, 'msgset_143').decode_contents())
     14.
     <input name="msgset_143" type="hidden"/>
 
     >>> print(find_tag_by_id(
-    ...     slow_submission.contents, 'msgset_143_singular').renderContents())
+    ...     slow_submission.contents, 'msgset_143_singular').decode_contents())
     The location and hierarchy of the Evolution contact...
 
     >>> print(find_tag_by_id(
     ...     slow_submission.contents,
-    ...     'msgset_143_es_translation_0').renderContents())
+    ...     'msgset_143_es_translation_0').decode_contents())
     blah
 
 But also, the new one should appear in the form.
@@ -460,7 +460,7 @@ But also, the new one should appear in the form.
     >>> elements = find_main_content(slow_submission.contents).findAll(
     ...     True, {'id': re.compile(r'^msgset_143_es_suggestion_\d+_0$')})
     >>> for element in elements:
-    ...     print(element.renderContents())
+    ...     print(element.decode_contents())
     La ubicación ...
     Tenga paciencia ...
     foo!!
diff --git a/lib/lp/translations/stories/standalone/xx-translations-to-complete.txt b/lib/lp/translations/stories/standalone/xx-translations-to-complete.txt
index 055e858..cffdfd3 100644
--- a/lib/lp/translations/stories/standalone/xx-translations-to-complete.txt
+++ b/lib/lp/translations/stories/standalone/xx-translations-to-complete.txt
@@ -38,7 +38,7 @@ The dashboard shows a listing of translations that need Jean's help.
 
     >>> tag = find_tag_by_id(
     ...     jean_browser.contents, 'translations-to-complete-table')
-    >>> print(tag.renderContents())
+    >>> print(tag.decode_contents())
 
 Only Jean sees his personal listing.
 
diff --git a/lib/lp/translations/stories/standalone/xx-translations-to-review.txt b/lib/lp/translations/stories/standalone/xx-translations-to-review.txt
index 09bcb33..c90ebbe 100644
--- a/lib/lp/translations/stories/standalone/xx-translations-to-review.txt
+++ b/lib/lp/translations/stories/standalone/xx-translations-to-review.txt
@@ -55,7 +55,7 @@ Xowxz is a Khmer reviewer.
     ...     if listing:
     ...         count = 0
     ...         for tr in listing.findAll('tr'):
-    ...             tds = [td.renderContents() for td in tr.findAll('td')]
+    ...             tds = [td.decode_contents() for td in tr.findAll('td')]
     ...             print('    '.join(tds))
     ...             count += 1
     ...         print("Listing contains %d translation(s)." % count)
@@ -67,7 +67,7 @@ Xowxz is a Khmer reviewer.
     ...     soup = BeautifulSoup(browser.contents)
     ...     link = soup.find(id="translations-to-review-link")
     ...     if link:
-    ...         print(link.renderContents())
+    ...         print(link.decode_contents())
     ...     else:
     ...         print("No link.")
 
diff --git a/lib/lp/translations/stories/translationgroups/xx-translationgroups.txt b/lib/lp/translations/stories/translationgroups/xx-translationgroups.txt
index f4b2b55..e73332c 100644
--- a/lib/lp/translations/stories/translationgroups/xx-translationgroups.txt
+++ b/lib/lp/translations/stories/translationgroups/xx-translationgroups.txt
@@ -37,7 +37,7 @@ OK, best we try again, with administrator rights!
     >>> admin_browser.open(
     ...     'http://translations.launchpad.test/+groups/+new')
     >>> print(find_main_content(
-    ...     admin_browser.contents).find('h1').renderContents())
+    ...     admin_browser.contents).find('h1').decode_contents())
     Create a new translation group
 
 Translation group names must meet certain conditions.  For example, they
@@ -53,7 +53,7 @@ may not contain any upper-case letters.
     ...     "each specialising in their own language.")
     >>> admin_browser.getControl('Create').click()
     >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There is 1 error.
     Invalid name 'PolYglot'. Names must be at least two characters ...
 
@@ -73,7 +73,7 @@ translation-team.
     http://translations.launchpad.test/+groups/+new
 
     >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There is 1 error.
     There is already a translation group with such name
 
@@ -123,7 +123,7 @@ A Rosetta administrator is also allowed to create groups.
 By default, when a group is created, the creator is its owner.
 
     >>> for t in find_tags_by_class(browser.contents, 'link'):
-    ...     print(t.renderContents())
+    ...     print(t.decode_contents())
     Jordi Mallach
 
 The Rosetta administrator assigns ownership of the group to Sample
@@ -146,7 +146,7 @@ The Rosetta administrator is still able to administer this group:
 But Sample Person is now listed as its owner:
 
     >>> for t in find_tags_by_class(browser.contents, 'link'):
-    ...     print(t.renderContents())
+    ...     print(t.decode_contents())
     Sample Person
 
 That means that Sample Person is allowed to administer "their" group.
@@ -212,7 +212,7 @@ didn't move away from this form.
     http://translations.launchpad.test/+groups/testing-translation-team/+edit
 
     >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     There is 1 error.
     There is already a translation group with this name
 
@@ -224,7 +224,7 @@ Choosing another name should work though.
     http://translations.launchpad.test/+groups/renamed-group
 
     >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
 
 You can also edit the generic translation instructions for the team
 
@@ -577,7 +577,7 @@ The error means we stay on the appoint page:
     'http://translations.launchpad.test/+groups/polyglot/+appoint'
 
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There is 1 error.
     There is already a translator for this language
 
@@ -680,7 +680,7 @@ exist.
 the system detects it and notify the user that is not possible.
 
     >>> for message in find_tags_by_class(browser.contents, 'message'):
-    ...     print(message.renderContents())
+    ...     print(message.decode_contents())
     There is 1 error.
     <a href="http://translations.launchpad.test/~name21";>Hoary Gnome Team</a>
     is already a translator for this language
@@ -732,7 +732,7 @@ should be redirected to the polyglot page.
 And on that page, we should see the removal message.
 
     >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Removed Hoary Gnome Team as the Abkhazian translator for The PolyGlot
     Translation Group.
 
@@ -781,7 +781,7 @@ No Privileges is going to do some translation here.  Right now, message
 number 148 is not translated.
 
     >>> tag = find_tag_by_id(browser.contents, 'msgset_148_cy_translation_0')
-    >>> print(tag.renderContents())
+    >>> print(tag.decode_contents())
     (no translation yet)
 
 After No posts a translation, however, it is.
@@ -803,7 +803,7 @@ added (with some extra html code, but the same content we wanted to add)
     http://.../ubuntu/.../evolution/+pots/evolution-2.2/cy/19/+translate
 
     >>> tag = find_tag_by_id(browser.contents, 'msgset_148_cy_translation_0')
-    >>> print(tag.renderContents())
+    >>> print(tag.decode_contents())
     foo<img alt="" src="/@@/translation-newline"/><br/>
     %i%i%i
 
@@ -842,7 +842,7 @@ suggestions.
     ...     if not labels:
     ...         return None
     ...     else:
-    ...         return labels[0].renderContents()
+    ...         return labels[0].decode_contents()
 
     >>> def get_detail_tag(browser, tag_class):
     ...     """Find tag of given class in translation page."""
@@ -850,13 +850,13 @@ suggestions.
     ...     if not tag:
     ...         return None
     ...     else:
-    ...         return tag.renderContents()
+    ...         return tag.decode_contents()
 
     >>> def print_menu_option(contents, option):
     ...     """Print given navigation menu on given page, if present."""
     ...     found = False
     ...     for item in find_tags_by_class(contents, 'menu-link-%s' % option):
-    ...         print(item.renderContents())
+    ...         print(item.decode_contents())
     ...         found = True
     ...     if not found:
     ...         print("Not found.")
@@ -942,7 +942,7 @@ as well as to upload files.
     ...     if not markers:
     ...         return None
     ...     else:
-    ...         return markers[0].renderContents()
+    ...         return markers[0].decode_contents()
 
     >>> browser.open(
     ...     'http://translations.launchpad.test/'
@@ -990,7 +990,7 @@ has been added.
     >>> print(find_no_translation_marker(browser.contents))
     None
 
-    >>> print(find_main_content(browser.contents).renderContents())
+    >>> print(find_main_content(browser.contents).decode_contents())
     <...evolution minikaart...
 
 First, we verify that netapplet is using Launchpad Translations.
@@ -1101,7 +1101,7 @@ an error message back.
     http://.../ubuntu/hoary/+source/evolution/+pots/evolution-2.2/af/+upload
 
     >>> for tag in find_tags_by_class(admin_browser.contents, 'error'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Ignored your upload because you didn't select a file to upload.
 
 Uploading files with an unkown file format notifies the user that it
@@ -1141,7 +1141,7 @@ cannot be handled.
     http://translations.launchpad.test/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/af/+upload
 
     >>> for tag in find_tags_by_class(admin_browser.contents, 'error'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Ignored your upload because the file you uploaded was not recognised as
     a file that can be imported.
 
@@ -1154,7 +1154,7 @@ With all the correct information, a file can be uploaded.
     http://translations.launchpad.test/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/af/+upload
 
     >>> for tag in find_tags_by_class(admin_browser.contents, 'message'):
-    ...     print(tag.renderContents())
+    ...     print(tag.decode_contents())
     Thank you for your upload.  It will be automatically reviewed...
 
 
diff --git a/lib/lp/translations/stories/translations/xx-translations.txt b/lib/lp/translations/stories/translations/xx-translations.txt
index 928b0b3..3ae2b56 100644
--- a/lib/lp/translations/stories/translations/xx-translations.txt
+++ b/lib/lp/translations/stories/translations/xx-translations.txt
@@ -12,17 +12,17 @@ We are going to change message #21, but first, we see that this messages
 has no translations at all.
 
   >>> print(find_tag_by_id(
-  ...     user_browser.contents, 'msgset_150').renderContents())
+  ...     user_browser.contents, 'msgset_150').decode_contents())
   21.
   <input name="msgset_150" type="hidden"/>
   >>> print(find_tag_by_id(
-  ...     user_browser.contents, 'msgset_150_singular').renderContents())
+  ...     user_browser.contents, 'msgset_150_singular').decode_contents())
   Found <code>%i</code> invalid file.
   >>> print(find_tag_by_id(user_browser.contents,
-  ...                      'msgset_150_es_translation_0').renderContents())
+  ...                      'msgset_150_es_translation_0').decode_contents())
   (no translation yet)
   >>> print(find_tag_by_id(user_browser.contents,
-  ...                      'msgset_150_es_translation_1').renderContents())
+  ...                      'msgset_150_es_translation_1').decode_contents())
   (no translation yet)
 
 We are going to submit now translations for the singular and plural forms.
@@ -72,17 +72,17 @@ they will be empty waiting for new suggestions/translations.
 Also, we can see that the message has no active translation yet:
 
   >>> print(find_tag_by_id(
-  ...     user_browser.contents, 'msgset_150').renderContents())
+  ...     user_browser.contents, 'msgset_150').decode_contents())
   21.
   <input name="msgset_150" type="hidden"/>
   >>> print(find_tag_by_id(
-  ...     user_browser.contents, 'msgset_150_singular').renderContents())
+  ...     user_browser.contents, 'msgset_150_singular').decode_contents())
   Found <code>%i</code> invalid file.
   >>> print(find_tag_by_id(user_browser.contents,
-  ...                      'msgset_150_es_translation_0').renderContents())
+  ...                      'msgset_150_es_translation_0').decode_contents())
   (no translation yet)
   >>> print(find_tag_by_id(user_browser.contents,
-  ...                      'msgset_150_es_translation_1').renderContents())
+  ...                      'msgset_150_es_translation_1').decode_contents())
   (no translation yet)
 
 = Translations for DistroSeries =

Follow ups