← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~jugmac00/launchpad:apply-blackdoc-on-launchpad into launchpad:master

 

Jürgen Gmach has proposed merging ~jugmac00/launchpad:apply-blackdoc-on-launchpad into launchpad:master.

Commit message:
Format docstrings and doctests via `blackdoc`

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jugmac00/launchpad/+git/launchpad/+merge/429709
-- 
The attached diff has been truncated due to its size.
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/launchpad:apply-blackdoc-on-launchpad into launchpad:master.
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 63ab21e..68c414a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -61,7 +61,7 @@ repos:
     -   id: eslint
         args: [--quiet]
 -   repo: https://git.launchpad.net/lp-lint-doctest
-    rev: '0.4'
+    rev: '0.5'
     hooks:
     -   id: lp-lint-doctest
         args: [--allow-option-flag, IGNORE_EXCEPTION_MODULE_IN_PYTHON2]
@@ -80,3 +80,9 @@ repos:
     hooks:
     -   id: woke-from-source
         files: ^doc/.*\.rst$
+-   repo: https://github.com/keewis/blackdoc
+    rev: v0.3.6
+    hooks:
+    -   id: blackdoc
+        args: ["-l", "78"]
+        exclude: ^doc/.*
diff --git a/configs/README.rst b/configs/README.rst
index 8d21996..a044a59 100644
--- a/configs/README.rst
+++ b/configs/README.rst
@@ -73,10 +73,10 @@ The LaunchpadConfig singleton is exposed as config in its module.
 
 The config can be accessed as a dictionary...
 
-    >>> 'launchpad' in config
+    >>> "launchpad" in config
     True
 
-    >>> config['launchpad']['default_batch_size']
+    >>> config["launchpad"]["default_batch_size"]
     5
 
 ...though it is commonly accessed as an object.
@@ -111,17 +111,17 @@ ConfigData. The configuration can be modified and safely restored.
 Tests can call push() with the configuration name and data to update
 the config singleton.
 
-    >>> test_data = ("""
+    >>> test_data = """
     ...     [answertracker]
-    ...     email_domain: answers.launchpad.test""")
-    >>> config.push('test_data', test_data)
+    ...     email_domain: answers.launchpad.test"""
+    >>> config.push("test_data", test_data)
     >>> config.answertracker.email_domain
     'answers.launchpad.test'
 
 And tests can remove the data with pop() when they are done to restore
 the config.
 
-    >>> config.pop('test_data')
+    >>> config.pop("test_data")
     (<canonical.lazr.config.ConfigData ...>,)
     >>> config.answertracker.email_domain
     'answers.launchpad.net'
diff --git a/lib/lp/answers/browser/tests/faq-views.rst b/lib/lp/answers/browser/tests/faq-views.rst
index 6928252..5f09e80 100644
--- a/lib/lp/answers/browser/tests/faq-views.rst
+++ b/lib/lp/answers/browser/tests/faq-views.rst
@@ -3,10 +3,11 @@ Answer Tracker FAQ Pages
 
     >>> from lp.registry.interfaces.product import IProductSet
 
-    >>> firefox = getUtility(IProductSet).getByName('firefox')
+    >>> firefox = getUtility(IProductSet).getByName("firefox")
     >>> ignored = login_person(firefox.owner)
     >>> firefox_faq = firefox.newFAQ(
-    ...     firefox.owner, 'A FAQ', 'FAQ for test purpose')
+    ...     firefox.owner, "A FAQ", "FAQ for test purpose"
+    ... )
 
 
 Latest FAQs portlet
@@ -15,20 +16,21 @@ Latest FAQs portlet
 The latest FAQs portlet allows an `IFAQTarget` to show the latest FAQs.
 It's view provided latest_faqs to get the FAQs to display.
 
-    >>> from lp.testing.pages import (
-    ...     extract_text, find_tag_by_id)
+    >>> from lp.testing.pages import extract_text, find_tag_by_id
 
     >>> view = create_initialized_view(
-    ...     firefox, '+portlet-listfaqs', principal=firefox.owner)
+    ...     firefox, "+portlet-listfaqs", principal=firefox.owner
+    ... )
     >>> for faq in view.latest_faqs:
     ...     print(faq.title)
+    ...
     A FAQ
     What's the keyboard shortcut for [random feature]?
     How do I install plugins (Shockwave, QuickTime, etc.)?
     How do I troubleshoot problems with extensions/themes?
     How do I install Extensions?
 
-    >>> content = find_tag_by_id(view.render(), 'portlet-latest-faqs')
+    >>> content = find_tag_by_id(view.render(), "portlet-latest-faqs")
     >>> print(content.h2)
     <h2>...FAQs for Mozilla Firefox </h2>
 
@@ -38,7 +40,7 @@ It's view provided latest_faqs to get the FAQs to display.
 
 Each FAQ is linked.
 
-    >>> print(content.find('a', {'class': 'sprite faq'}))
+    >>> print(content.find("a", {"class": "sprite faq"}))
     <a class="..." href="http://answers.../firefox/+faq/...";>A FAQ</a>
 
 The portlet has a form to search FAQs. The view provides the action URL so
@@ -47,21 +49,22 @@ that the form works from any page.
     >>> print(view.portlet_action)
     http://answers.launchpad.test/firefox/+faqs
 
-    >>> print(content.form['action'])
+    >>> print(content.form["action"])
     http://answers.launchpad.test/firefox/+faqs
 
 The portlet provides a link to create a FAQ when the user that has append
 permission, such as the project owner.
 
-    >>> print(content.find('a', {'class': 'menu-link-create_faq sprite add'}))
+    >>> print(content.find("a", {"class": "menu-link-create_faq sprite add"}))
     <a class="..." href=".../firefox/+createfaq">Create a new FAQ</a>
 
 Other users do not see the link.
 
-    >>> user = factory.makePerson(name='a-user')
+    >>> user = factory.makePerson(name="a-user")
     >>> ignored = login_person(user)
     >>> view = create_initialized_view(
-    ...     firefox, '+portlet-listfaqs', principal=user)
-    >>> content = find_tag_by_id(view.render(), 'portlet-latest-faqs')
-    >>> print(content.find('a', {'class': 'menu-link-create_faq sprite add'}))
+    ...     firefox, "+portlet-listfaqs", principal=user
+    ... )
+    >>> content = find_tag_by_id(view.render(), "portlet-latest-faqs")
+    >>> print(content.find("a", {"class": "menu-link-create_faq sprite add"}))
     None
diff --git a/lib/lp/answers/browser/tests/question-subscribe_me.rst b/lib/lp/answers/browser/tests/question-subscribe_me.rst
index 6087177..e1bafad 100644
--- a/lib/lp/answers/browser/tests/question-subscribe_me.rst
+++ b/lib/lp/answers/browser/tests/question-subscribe_me.rst
@@ -9,11 +9,12 @@ whatever the action used.
     >>> from lp.services.webapp.interfaces import ILaunchBag
     >>> from lp.registry.interfaces.product import IProductSet
 
-    >>> firefox = getUtility(IProductSet).getByName('firefox')
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> firefox = getUtility(IProductSet).getByName("firefox")
+    >>> login("test@xxxxxxxxxxxxx")
     >>> sample_person = getUtility(ILaunchBag).user
     >>> firefox_question = firefox.newQuestion(
-    ...     sample_person, 'New question', 'A problem.')
+    ...     sample_person, "New question", "A problem."
+    ... )
 
 Empty the subscribers list:
 
@@ -24,61 +25,64 @@ Empty the subscribers list:
 Create a view harness:
 
     >>> workflow_harness = LaunchpadFormHarness(
-    ...     firefox_question, QuestionWorkflowView)
-    >>> form_data = {'field.message': 'A message.',
-    ...              'field.subscribe_me.used': 1,
-    ...              'field.subscribe_me': 'on'}
+    ...     firefox_question, QuestionWorkflowView
+    ... )
+    >>> form_data = {
+    ...     "field.message": "A message.",
+    ...     "field.subscribe_me.used": 1,
+    ...     "field.subscribe_me": "on",
+    ... }
 
 Subscription is possible when requesting for more information:
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> foo_bar = getUtility(ILaunchBag).user
-    >>> workflow_harness.submit('requestinfo', form_data)
+    >>> workflow_harness.submit("requestinfo", form_data)
     >>> firefox_question.isSubscribed(foo_bar)
     True
     >>> firefox_question.unsubscribe(foo_bar, foo_bar)
 
 Subscription is possible when providing more information:
 
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> workflow_harness.submit('giveinfo', form_data)
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> workflow_harness.submit("giveinfo", form_data)
     >>> firefox_question.isSubscribed(sample_person)
     True
     >>> firefox_question.unsubscribe(sample_person, sample_person)
 
 Subscription is possible when providing an answer:
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> workflow_harness.submit('answer', form_data)
+    >>> login("foo.bar@xxxxxxxxxxxxx")
+    >>> workflow_harness.submit("answer", form_data)
     >>> firefox_question.isSubscribed(foo_bar)
     True
     >>> firefox_question.unsubscribe(foo_bar, foo_bar)
 
 As when confirming an answer (although this is probably not that common):
 
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> workflow_harness.submit('confirm', dict(answer_id=-1, **form_data))
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> workflow_harness.submit("confirm", dict(answer_id=-1, **form_data))
     >>> firefox_question.isSubscribed(sample_person)
     True
     >>> firefox_question.unsubscribe(sample_person, sample_person)
 
 It is also possible when reopening the request.
 
-    >>> workflow_harness.submit('reopen', form_data)
+    >>> workflow_harness.submit("reopen", form_data)
     >>> firefox_question.isSubscribed(sample_person)
     True
     >>> firefox_question.unsubscribe(sample_person, sample_person)
 
 Self-Answering the request:
 
-    >>> workflow_harness.submit('selfanswer', form_data)
+    >>> workflow_harness.submit("selfanswer", form_data)
     >>> firefox_question.isSubscribed(sample_person)
     True
     >>> firefox_question.unsubscribe(sample_person, sample_person)
 
 As well as adding a comment:
 
-    >>> workflow_harness.submit('comment', form_data)
+    >>> workflow_harness.submit("comment", form_data)
     >>> firefox_question.isSubscribed(sample_person)
     True
     >>> firefox_question.unsubscribe(sample_person, sample_person)
@@ -86,9 +90,14 @@ As well as adding a comment:
 Make sure that whenever the view actions is modified, this test
 requires update:
 
-    >>> print("\n".join(sorted(
-    ...     action.__name__.split('.')[-1]
-    ...     for action in workflow_harness.view.actions)))
+    >>> print(
+    ...     "\n".join(
+    ...         sorted(
+    ...             action.__name__.split(".")[-1]
+    ...             for action in workflow_harness.view.actions
+    ...         )
+    ...     )
+    ... )
     answer
     comment
     confirm
diff --git a/lib/lp/answers/browser/tests/views.rst b/lib/lp/answers/browser/tests/views.rst
index 831b4c4..70a10a6 100644
--- a/lib/lp/answers/browser/tests/views.rst
+++ b/lib/lp/answers/browser/tests/views.rst
@@ -7,15 +7,15 @@ Several views are used to handle the various operations on a question.
     >>> from lp.services.webapp.servers import LaunchpadTestRequest
     >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> from lp.registry.interfaces.product import IProductSet
-    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
+    >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
     >>> question_three = ubuntu.getQuestion(3)
-    >>> firefox = getUtility(IProductSet).getByName('firefox')
+    >>> firefox = getUtility(IProductSet).getByName("firefox")
     >>> firefox_question = firefox.getQuestion(2)
 
     # The firefox_question doesn't have any subscribers, let's subscribe
     # the owner.
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> firefox_question.subscribe(firefox_question.owner)
     <lp.answers.model.questionsubscription.QuestionSubscription...>
 
@@ -33,22 +33,27 @@ Register an event listener that will print events it receives.
     >>> from lp.testing.fixture import ZopeEventHandlerFixture
 
     >>> def print_modified_event(object, event):
-    ...     print("Received ObjectModifiedEvent: %s" % (
-    ...         ", ".join(sorted(event.edited_fields))))
+    ...     print(
+    ...         "Received ObjectModifiedEvent: %s"
+    ...         % (", ".join(sorted(event.edited_fields)))
+    ...     )
+    ...
     >>> question_event_listener = ZopeEventHandlerFixture(
-    ...     print_modified_event, (IQuestion, IObjectModifiedEvent))
+    ...     print_modified_event, (IQuestion, IObjectModifiedEvent)
+    ... )
     >>> question_event_listener.setUp()
 
-    >>> view = create_initialized_view(question_three, name='+subscribe')
+    >>> view = create_initialized_view(question_three, name="+subscribe")
     >>> print(view.label)
     Subscribe to question
 
     >>> print(view.page_title)
     Subscription
 
-    >>> form = {'subscribe': 'Subscribe'}
+    >>> form = {"subscribe": "Subscribe"}
     >>> view = create_initialized_view(
-    ...     question_three, name='+subscribe', form=form)
+    ...     question_three, name="+subscribe", form=form
+    ... )
     Received ObjectModifiedEvent: subscribers
     >>> question_three.isSubscribed(getUtility(ILaunchBag).user)
     True
@@ -58,29 +63,32 @@ question view page.
 
     >>> for notice in view.request.notifications:
     ...     print(notice.message)
+    ...
     You have subscribed to this question.
 
-    >>> view.request.response.getHeader('Location')
+    >>> view.request.response.getHeader("Location")
     '.../+question/3'
 
 Unsubscription works in a similar manner.
 
-    >>> view = create_initialized_view(question_three, name='+subscribe')
+    >>> view = create_initialized_view(question_three, name="+subscribe")
     >>> print(view.label)
     Unsubscribe from question
 
-    >>> form = {'subscribe': 'Unsubscribe'}
+    >>> form = {"subscribe": "Unsubscribe"}
     >>> view = create_initialized_view(
-    ...     question_three, name='+subscribe', form=form)
+    ...     question_three, name="+subscribe", form=form
+    ... )
     Received ObjectModifiedEvent: subscribers
     >>> question_three.isSubscribed(getUtility(ILaunchBag).user)
     False
 
     >>> for notice in view.request.notifications:
     ...     print(notice.message)
+    ...
     You have unsubscribed from this question.
 
-    >>> view.request.response.getHeader('Location')
+    >>> view.request.response.getHeader("Location")
     '.../+question/3'
 
     >>> question_event_listener.cleanUp()
@@ -99,37 +107,41 @@ the form.
     >>> from lp.answers.browser.question import QuestionWorkflowView
     >>> from lp.testing.deprecated import LaunchpadFormHarness
     >>> workflow_harness = LaunchpadFormHarness(
-    ...     firefox_question, QuestionWorkflowView)
+    ...     firefox_question, QuestionWorkflowView
+    ... )
 
     # Let's define a helper method that will print the names of the
     # available actions.
 
     >>> def printAvailableActionNames(view):
-    ...     names = [action.__name__.split('.')[-1]
-    ...              for action in view.actions
-    ...              if action.available()]
+    ...     names = [
+    ...         action.__name__.split(".")[-1]
+    ...         for action in view.actions
+    ...         if action.available()
+    ...     ]
     ...     for name in sorted(names):
     ...         print(name)
+    ...
 
 Unlogged-in users cannot post any comments on the question:
 
     >>> login(ANONYMOUS)
-    >>> workflow_harness.submit('', {})
+    >>> workflow_harness.submit("", {})
     >>> printAvailableActionNames(workflow_harness.view)
 
 When question is in the OPEN state, the owner can comment, answer their
 own question or provide more information.
 
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> workflow_harness.submit('', {})
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> workflow_harness.submit("", {})
     >>> printAvailableActionNames(workflow_harness.view)
     comment giveinfo selfanswer
 
 But when another user sees the question, they can comment, provide an
 answer or request more information.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
-    >>> workflow_harness.submit('', {})
+    >>> login("no-priv@xxxxxxxxxxxxx")
+    >>> workflow_harness.submit("", {})
     >>> printAvailableActionNames(workflow_harness.view)
     answer comment requestinfo
 
@@ -138,11 +150,15 @@ displayed, the question status is changed to NEEDSINFO and the user is
 redirected back to the question page.
 
     >>> workflow_harness.submit(
-    ...     'requestinfo', {
-    ...         'field.message': 'Can you provide an example of an URL'
-    ...             'displaying the problem?'})
+    ...     "requestinfo",
+    ...     {
+    ...         "field.message": "Can you provide an example of an URL"
+    ...         "displaying the problem?"
+    ...     },
+    ... )
     >>> for notification in workflow_harness.request.response.notifications:
     ...     print(notification.message)
+    ...
     Thanks for your information request.
 
     >>> print(firefox_question.status.name)
@@ -159,8 +175,8 @@ answer or request more information:
 
 And the question owner still has the same possibilities as at first:
 
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> workflow_harness.submit('', {})
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> workflow_harness.submit("", {})
     >>> printAvailableActionNames(workflow_harness.view)
     comment giveinfo selfanswer
 
@@ -168,12 +184,13 @@ If they reply with the requested information, the question is moved back
 to the OPEN state.
 
     >>> form = {
-    ...     'field.message': "The following SVG doesn't display properly:"
-    ...         "\nhttp://www.w3.org/2001/08/rdfweb/rdfweb-chaals-and-dan.svg";
-    ...     }
-    >>> workflow_harness.submit('giveinfo', form)
+    ...     "field.message": "The following SVG doesn't display properly:"
+    ...     "\nhttp://www.w3.org/2001/08/rdfweb/rdfweb-chaals-and-dan.svg";
+    ... }
+    >>> workflow_harness.submit("giveinfo", form)
     >>> for notification in workflow_harness.request.response.notifications:
     ...     print(notification.message)
+    ...
     Thanks for adding more information to your question.
 
     >>> print(firefox_question.status.name)
@@ -184,14 +201,18 @@ to the OPEN state.
 
 The other user can come back and gives an answer:
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> workflow_harness.submit(
-    ...     'answer', {
-    ...         'field.message': "New version of the firefox package are "
-    ...             "available with SVG support enabled. Using apt "
-    ...             "you should be able to upgrade."})
+    ...     "answer",
+    ...     {
+    ...         "field.message": "New version of the firefox package are "
+    ...         "available with SVG support enabled. Using apt "
+    ...         "you should be able to upgrade."
+    ...     },
+    ... )
     >>> for notification in workflow_harness.request.response.notifications:
     ...     print(notification.message)
+    ...
     Thanks for your answer.
 
     >>> print(firefox_question.status.name)
@@ -205,8 +226,8 @@ question owner changes. They can now either comment, confirm the answer,
 answer the problem themselves, or reopen the request because that answer
 isn't working.
 
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> workflow_harness.submit('', {})
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> workflow_harness.submit("", {})
     >>> printAvailableActionNames(workflow_harness.view)
     comment confirm reopen selfanswer
 
@@ -216,10 +237,12 @@ user to enter a confirmation message at that stage.
 
     >>> answer_message_number = len(firefox_question.messages) - 1
     >>> workflow_harness.submit(
-    ...     'confirm', {'answer_id': answer_message_number,
-    ...                 'field.message': ''})
+    ...     "confirm",
+    ...     {"answer_id": answer_message_number, "field.message": ""},
+    ... )
     >>> for notification in workflow_harness.request.response.notifications:
     ...     print(notification.message)
+    ...
     Thanks for your feedback.
 
     >>> print(firefox_question.status.name)
@@ -243,11 +266,15 @@ the question:
 Adding a comment doesn't change the status:
 
     >>> workflow_harness.submit(
-    ...     'comment', {
-    ...         'field.message': "The example now displays "
-    ...         "correctly. Thanks."})
+    ...     "comment",
+    ...     {
+    ...         "field.message": "The example now displays "
+    ...         "correctly. Thanks."
+    ...     },
+    ... )
     >>> for notification in workflow_harness.request.response.notifications:
     ...     print(notification.message)
+    ...
     Thanks for your comment.
 
     >>> workflow_harness.redirectionTarget()
@@ -258,23 +285,27 @@ Adding a comment doesn't change the status:
 
 And the other user can only comment on the question:
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
-    >>> workflow_harness.submit('', {})
+    >>> login("no-priv@xxxxxxxxxxxxx")
+    >>> workflow_harness.submit("", {})
     >>> printAvailableActionNames(workflow_harness.view)
     comment
 
 If the question owner reopens the question, its status is changed back
 to 'OPEN'.
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> workflow_harness.submit(
-    ...     'reopen', {
-    ...         'field.message': "Actually, there are still SVG "
+    ...     "reopen",
+    ...     {
+    ...         "field.message": "Actually, there are still SVG "
     ...         "that do not display correctly. For example, the following "
     ...         "http://people.w3.org/maxf/ChessGML/immortal.svg doesn't "
-    ...         "display correctly."})
+    ...         "display correctly."
+    ...     },
+    ... )
     >>> for notification in workflow_harness.request.response.notifications:
     ...     print(notification.message)
+    ...
     Your question was reopened.
 
     >>> print(firefox_question.status.name)
@@ -288,11 +319,15 @@ to the SOLVED state. The question owner is attributed as the answerer,
 but no answer message is assigned to the answer.
 
     >>> workflow_harness.submit(
-    ...     'selfanswer', {
-    ...         'field.message': "OK, this example requires some "
-    ...         "SVG features that will only be available in Firefox 2.0."})
+    ...     "selfanswer",
+    ...     {
+    ...         "field.message": "OK, this example requires some "
+    ...         "SVG features that will only be available in Firefox 2.0."
+    ...     },
+    ... )
     >>> for notification in workflow_harness.request.response.notifications:
     ...     print(notification.message)
+    ...
     Your question is solved. If a particular message helped you solve the
     problem, use the <em>'This solved my problem'</em> button.
 
@@ -318,8 +353,9 @@ answerer's message is attributed as the answer in this case.
     comment confirm reopen
 
     >>> workflow_harness.submit(
-    ...     'confirm', {'answer_id': answer_message_number,
-    ...                 'field.message': ''})
+    ...     "confirm",
+    ...     {"answer_id": answer_message_number, "field.message": ""},
+    ... )
     >>> print(firefox_question.status.name)
     SOLVED
 
@@ -344,13 +380,16 @@ The QuestionMakeBugView is used to handle the creation of a bug from a
 question. In addition to creating a bug, this operation will also link
 the bug to the question.
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> request = LaunchpadTestRequest(
-    ...     form={'field.actions.create': 'Create',
-    ...           'field.title': 'Bug title',
-    ...           'field.description': 'Bug description.'})
-    >>> request.method = 'POST'
-    >>> makebug = getMultiAdapter((question_three, request), name='+makebug')
+    ...     form={
+    ...         "field.actions.create": "Create",
+    ...         "field.title": "Bug title",
+    ...         "field.description": "Bug description.",
+    ...     }
+    ... )
+    >>> request.method = "POST"
+    >>> makebug = getMultiAdapter((question_three, request), name="+makebug")
     >>> question_three.bugs
     []
 
@@ -371,21 +410,24 @@ the bug to the question.
     >>> message = [n.message for n in request.notifications]
     >>> for m in message:
     ...     print(m)
+    ...
     Thank you! Bug #... created.
 
-    >>> 'Bug #%s created.' % new_bug_id in message[0]
+    >>> "Bug #%s created." % new_bug_id in message[0]
     True
 
 If the question already has bugs linked to it, no new bug can be
 created.
 
     >>> request = LaunchpadTestRequest(
-    ...     form={'field.actions.create': 'create'})
-    >>> request.method = 'POST'
-    >>> makebug = getMultiAdapter((question_three, request), name='+makebug')
+    ...     form={"field.actions.create": "create"}
+    ... )
+    >>> request.method = "POST"
+    >>> makebug = getMultiAdapter((question_three, request), name="+makebug")
     >>> makebug.initialize()
     >>> for n in request.notifications:
     ...     print(n.message)
+    ...
     You cannot create a bug report...
 
 
@@ -405,15 +447,19 @@ QuestionRejectView
 That view is used by administrator and answer contacts to reject a
 question.
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> request = LaunchpadTestRequest(
-    ...     form={'field.actions.reject': 'Reject',
-    ...           'field.message': 'Rejecting for the fun of it.'})
-    >>> request.method = 'POST'
-    >>> view = getMultiAdapter((firefox_question, request), name='+reject')
+    ...     form={
+    ...         "field.actions.reject": "Reject",
+    ...         "field.message": "Rejecting for the fun of it.",
+    ...     }
+    ... )
+    >>> request.method = "POST"
+    >>> view = getMultiAdapter((firefox_question, request), name="+reject")
     >>> view.initialize()
     >>> for notice in request.notifications:
     ...     print(notice.message)
+    ...
     You have rejected this question.
 
     >>> print(firefox_question.status.title)
@@ -427,15 +473,20 @@ QuestionChangeStatusView is used by administrator to change the status
 outside of the comment workflow.
 
     >>> request = LaunchpadTestRequest(
-    ...     form={'field.actions.change-status': 'Change Status',
-    ...           'field.status': 'SOLVED',
-    ...           'field.message': 'Previous rejection was an error.'})
-    >>> request.method = 'POST'
+    ...     form={
+    ...         "field.actions.change-status": "Change Status",
+    ...         "field.status": "SOLVED",
+    ...         "field.message": "Previous rejection was an error.",
+    ...     }
+    ... )
+    >>> request.method = "POST"
     >>> view = getMultiAdapter(
-    ...     (firefox_question, request), name='+change-status')
+    ...     (firefox_question, request), name="+change-status"
+    ... )
     >>> view.initialize()
     >>> for notice in request.notifications:
     ...     print(notice.message)
+    ...
     Question status updated.
 
     >>> print(firefox_question.status.title)
@@ -450,20 +501,23 @@ fields. It can be used to edit the question title and description and
 also its metadata like language, assignee, distribution, source package,
 product and whiteboard.
 
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> request = LaunchpadTestRequest(form={
-    ...     'field.actions.change': 'Continue',
-    ...     'field.title': 'Better Title',
-    ...     'field.language': 'en',
-    ...     'field.description': 'A better description.',
-    ...     'field.target': 'package',
-    ...     'field.target.distribution': 'ubuntu',
-    ...     'field.target.package': 'mozilla-firefox',
-    ...     'field.assignee': 'name16',
-    ...     'field.whiteboard': 'Some note'})
-    >>> request.method = 'POST'
-
-    >>> view = getMultiAdapter((question_three, request), name='+edit')
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> request = LaunchpadTestRequest(
+    ...     form={
+    ...         "field.actions.change": "Continue",
+    ...         "field.title": "Better Title",
+    ...         "field.language": "en",
+    ...         "field.description": "A better description.",
+    ...         "field.target": "package",
+    ...         "field.target.distribution": "ubuntu",
+    ...         "field.target.package": "mozilla-firefox",
+    ...         "field.assignee": "name16",
+    ...         "field.whiteboard": "Some note",
+    ...     }
+    ... )
+    >>> request.method = "POST"
+
+    >>> view = getMultiAdapter((question_three, request), name="+edit")
     >>> view.initialize()
     >>> print(question_three.distribution.name)
     ubuntu
@@ -495,19 +549,22 @@ owner) to change status whiteboard, the values are unchanged.
 If the user has the required permission, the assignee and whiteboard
 fields will be updated:
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> request = LaunchpadTestRequest(form={
-    ...     'field.actions.change': 'Continue',
-    ...     'field.language': 'en',
-    ...     'field.title': 'Better Title',
-    ...     'field.description': 'A better description.',
-    ...     'field.target': 'package',
-    ...     'field.target.distribution': 'ubuntu',
-    ...     'field.target.package': 'mozilla-firefox',
-    ...     'field.assignee': 'name16',
-    ...     'field.whiteboard': 'Some note'})
-    >>> request.method = 'POST'
-    >>> view = getMultiAdapter((question_three, request), name='+edit')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
+    >>> request = LaunchpadTestRequest(
+    ...     form={
+    ...         "field.actions.change": "Continue",
+    ...         "field.language": "en",
+    ...         "field.title": "Better Title",
+    ...         "field.description": "A better description.",
+    ...         "field.target": "package",
+    ...         "field.target.distribution": "ubuntu",
+    ...         "field.target.package": "mozilla-firefox",
+    ...         "field.assignee": "name16",
+    ...         "field.whiteboard": "Some note",
+    ...     }
+    ... )
+    >>> request.method = "POST"
+    >>> view = getMultiAdapter((question_three, request), name="+edit")
     >>> view.initialize()
     >>> print(question_three.title)
     Better Title
@@ -524,26 +581,29 @@ fields will be updated:
 The question language can be set to any language registered with
 Launchpad--it is not restricted to the user's preferred languages.
 
-    >>> view = create_initialized_view(question_three, name='+edit')
-    >>> view.widgets['language'].vocabulary
+    >>> view = create_initialized_view(question_three, name="+edit")
+    >>> view.widgets["language"].vocabulary
     <lp.services.worlddata.vocabularies.LanguageVocabulary ...>
 
 In a similar manner, the sourcepackagename field can only be updated on
 a distribution question:
 
-    >>> request = LaunchpadTestRequest(form={
-    ...     'field.actions.change': 'Continue',
-    ...     'field.language': 'en',
-    ...     'field.title': 'Better Title',
-    ...     'field.description': 'A better description.',
-    ...     'field.target': 'product',
-    ...     'field.target.distribution': '',
-    ...     'field.target.package': 'mozilla-firefox',
-    ...     'field.target.product': 'firefox',
-    ...     'field.assignee': '',
-    ...     'field.whiteboard': ''})
-    >>> request.method = 'POST'
-    >>> view = getMultiAdapter((question_three, request), name='+edit')
+    >>> request = LaunchpadTestRequest(
+    ...     form={
+    ...         "field.actions.change": "Continue",
+    ...         "field.language": "en",
+    ...         "field.title": "Better Title",
+    ...         "field.description": "A better description.",
+    ...         "field.target": "product",
+    ...         "field.target.distribution": "",
+    ...         "field.target.package": "mozilla-firefox",
+    ...         "field.target.product": "firefox",
+    ...         "field.assignee": "",
+    ...         "field.whiteboard": "",
+    ...     }
+    ... )
+    >>> request.method = "POST"
+    >>> view = getMultiAdapter((question_three, request), name="+edit")
     >>> view.initialize()
     >>> view.errors
     []
@@ -585,14 +645,17 @@ South Africa.
 
     >>> login(ANONYMOUS)
     >>> request = LaunchpadTestRequest(
-    ...     HTTP_ACCEPT_LANGUAGE='pt_BR', REMOTE_ADDR='196.36.161.227')
+    ...     HTTP_ACCEPT_LANGUAGE="pt_BR", REMOTE_ADDR="196.36.161.227"
+    ... )
     >>> from lp.answers.browser.question import (
-    ...     QuestionLanguageVocabularyFactory)
-    >>> view = getMultiAdapter((firefox, request), name='+addticket')
+    ...     QuestionLanguageVocabularyFactory,
+    ... )
+    >>> view = getMultiAdapter((firefox, request), name="+addticket")
     >>> vocab = QuestionLanguageVocabularyFactory(view)(None)
     >>> languages = [term.value for term in vocab]
-    >>> for lang in sorted(languages, key=attrgetter('code')):
+    >>> for lang in sorted(languages, key=attrgetter("code")):
     ...     print(lang.code)
+    ...
     af
     en
     pt_BR
@@ -603,15 +666,16 @@ South Africa.
 If the user logs in but didn't configure their preferred languages, the
 same logic is used to find the languages:
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> user = getUtility(ILaunchBag).user
     >>> len(user.languages)
     0
 
     >>> vocab = QuestionLanguageVocabularyFactory(view)(None)
     >>> languages = [term.value for term in vocab]
-    >>> for lang in sorted(languages, key=attrgetter('code')):
+    >>> for lang in sorted(languages, key=attrgetter("code")):
     ...     print(lang.code)
+    ...
     af
     en
     pt_BR
@@ -621,18 +685,20 @@ same logic is used to find the languages:
 
 But if the user configured their preferred languages, only these are used:
 
-    >>> login('carlos@xxxxxxxxxxxxx')
+    >>> login("carlos@xxxxxxxxxxxxx")
     >>> user = getUtility(ILaunchBag).user
-    >>> for lang in sorted(user.languages, key=attrgetter('code')):
+    >>> for lang in sorted(user.languages, key=attrgetter("code")):
     ...     print(lang.code)
+    ...
     ca
     en
     es
 
     >>> vocab = QuestionLanguageVocabularyFactory(view)(None)
     >>> languages = [term.value for term in vocab]
-    >>> for lang in sorted(languages, key=attrgetter('code')):
+    >>> for lang in sorted(languages, key=attrgetter("code")):
     ...     print(lang.code)
+    ...
     ca
     en
     es
@@ -643,10 +709,11 @@ English options).
 
 Daf has en_GB listed among his languages:
 
-    >>> login('daf@xxxxxxxxxxxxx')
+    >>> login("daf@xxxxxxxxxxxxx")
     >>> user = getUtility(ILaunchBag).user
-    >>> for lang in sorted(user.languages, key=attrgetter('code')):
+    >>> for lang in sorted(user.languages, key=attrgetter("code")):
     ...     print(lang.code)
+    ...
     cy
     en_GB
     ja
@@ -656,8 +723,9 @@ variant with English:
 
     >>> vocab = QuestionLanguageVocabularyFactory(view)(None)
     >>> languages = [term.value for term in vocab]
-    >>> for lang in sorted(languages, key=attrgetter('code')):
+    >>> for lang in sorted(languages, key=attrgetter("code")):
     ...     print(lang.code)
+    ...
     cy
     en
     ja
@@ -667,7 +735,7 @@ language in the vocabulary, even if this language would not be selected
 by the previous rules.
 
     >>> from lp.services.worlddata.interfaces.language import ILanguageSet
-    >>> afar = getUtility(ILanguageSet)['aa_DJ']
+    >>> afar = getUtility(ILanguageSet)["aa_DJ"]
     >>> question_three.language = afar
     >>> vocab = QuestionLanguageVocabularyFactory(view)(question_three)
     >>> afar in vocab
@@ -675,7 +743,7 @@ by the previous rules.
 
     # Clean up.
 
-    >>> question_three.language = getUtility(ILanguageSet)['en']
+    >>> question_three.language = getUtility(ILanguageSet)["en"]
 
 
 UserSupportLanguagesMixin
@@ -686,11 +754,13 @@ retrieve the set of languages in which the user is assumed to be
 interested.
 
     >>> from lp.answers.browser.questiontarget import (
-    ...     UserSupportLanguagesMixin)
+    ...     UserSupportLanguagesMixin,
+    ... )
     >>> from lp.services.webapp import LaunchpadView
 
-    >>> class UserSupportLanguagesView(UserSupportLanguagesMixin,
-    ...                                LaunchpadView):
+    >>> class UserSupportLanguagesView(
+    ...     UserSupportLanguagesMixin, LaunchpadView
+    ... ):
     ...     """View to test UserSupportLanguagesMixin."""
 
 The set of languages to use for support is defined in the
@@ -705,7 +775,8 @@ languages configured in the browser, plus other inferred from the GeoIP
 database.
 
     >>> request = LaunchpadTestRequest(
-    ...     HTTP_ACCEPT_LANGUAGE='fr, en_CA', REMOTE_ADDR='196.36.161.227')
+    ...     HTTP_ACCEPT_LANGUAGE="fr, en_CA", REMOTE_ADDR="196.36.161.227"
+    ... )
 
     >>> login(ANONYMOUS)
     >>> view = UserSupportLanguagesView(None, request)
@@ -715,7 +786,8 @@ request), and the languages spoken in South Africa (inferred from the
 GeoIP location of the request).
 
     >>> for language in sorted(
-    ...         view.user_support_languages, key=attrgetter('code')):
+    ...     view.user_support_languages, key=attrgetter("code")
+    ... ):
     ...     print(language.code)
     af
     en
@@ -727,10 +799,11 @@ GeoIP location of the request).
 Same thing if the logged in user didn't have any preferred languages
 set:
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> view = UserSupportLanguagesView(None, request)
     >>> for language in sorted(
-    ...         view.user_support_languages, key=attrgetter('code')):
+    ...     view.user_support_languages, key=attrgetter("code")
+    ... ):
     ...     print(language.code)
     af
     en
@@ -742,10 +815,11 @@ set:
 But when the user has some preferred languages set, these will be used
 instead of the ones inferred from the request:
 
-    >>> login('carlos@xxxxxxxxxxxxx')
+    >>> login("carlos@xxxxxxxxxxxxx")
     >>> view = UserSupportLanguagesView(None, request)
     >>> for language in sorted(
-    ...         view.user_support_languages, key=attrgetter('code')):
+    ...     view.user_support_languages, key=attrgetter("code")
+    ... ):
     ...     print(language.code)
     ca
     en
@@ -754,10 +828,11 @@ instead of the ones inferred from the request:
 English variants included in the user's preferred languages are
 excluded:
 
-    >>> login('daf@xxxxxxxxxxxxx')
+    >>> login("daf@xxxxxxxxxxxxx")
     >>> view = UserSupportLanguagesView(None, request)
     >>> for language in sorted(
-    ...         view.user_support_languages, key=attrgetter('code')):
+    ...     view.user_support_languages, key=attrgetter("code")
+    ... ):
     ...     print(language.code)
     cy
     en
@@ -781,25 +856,27 @@ keeping those searchable.
     ...
     ...     def getDefaultFilter(self):
     ...         return dict(**self.default_filter)
+    ...
 
     >>> search_view_harness = LaunchpadFormHarness(
-    ...     ubuntu, MyCustomSearchQuestionsView)
+    ...     ubuntu, MyCustomSearchQuestionsView
+    ... )
 
 By default, that class provides widgets to search by text and by status.
 
     >>> search_view = search_view_harness.view
-    >>> search_view.widgets.get('search_text') is not None
+    >>> search_view.widgets.get("search_text") is not None
     True
 
-    >>> search_view.widgets.get('language') is not None
+    >>> search_view.widgets.get("language") is not None
     True
 
-    >>> search_view.widgets.get('status') is not None
+    >>> search_view.widgets.get("status") is not None
     True
 
 It also includes a widget to select the sort order.
 
-    >>> search_view.widgets.get('sort') is not None
+    >>> search_view.widgets.get("sort") is not None
     True
 
 The questions matching the search are available by using the
@@ -811,6 +888,7 @@ searchResults() method. The returned results are batched.
 
     >>> for question in questions.batch:
     ...     print(backslashreplace(question.title))
+    ...
     Problema al recompilar kernel con soporte smp (doble-n\xfacleo)
     Continue playing after shutdown
     Play DVDs in Totem
@@ -820,15 +898,20 @@ searchResults() method. The returned results are batched.
 These were the default results when no search is entered. The user can
 tweak the search and filter the results:
 
-    >>> search_view_harness.submit('search', {
-    ...     'field.status': ['SOLVED', 'OPEN'],
-    ...     'field.search_text': 'firefox',
-    ...     'field.language': ['en'],
-    ...     'field.sort': 'by relevancy'})
+    >>> search_view_harness.submit(
+    ...     "search",
+    ...     {
+    ...         "field.status": ["SOLVED", "OPEN"],
+    ...         "field.search_text": "firefox",
+    ...         "field.language": ["en"],
+    ...         "field.sort": "by relevancy",
+    ...     },
+    ... )
     >>> search_view = search_view_harness.view
     >>> questions = search_view.searchResults()
     >>> for question in questions.batch:
     ...     print(question.title, question.status.title)
+    ...
     mailto: problem in webpage Solved
 
 Specific views can provide a default filter by returning the default
@@ -836,9 +919,10 @@ search parameters to use in the getDefaultFilter() method:
 
     >>> from lp.answers.enums import QuestionStatus
     >>> MyCustomSearchQuestionsView.default_filter = {
-    ...     'status': [QuestionStatus.SOLVED, QuestionStatus.INVALID],
-    ...     'language' : search_view.user_support_languages}
-    >>> search_view_harness.submit('', {})
+    ...     "status": [QuestionStatus.SOLVED, QuestionStatus.INVALID],
+    ...     "language": search_view.user_support_languages,
+    ... }
+    >>> search_view_harness.submit("", {})
 
 In this example, only the solved and invalid questions are listed by
 default.
@@ -847,39 +931,47 @@ default.
     >>> questions = search_view.searchResults()
     >>> for question in questions.batch:
     ...     print(question.title)
+    ...
     mailto: problem in webpage
     Better Title
 
 The status widget displays the default criteria used:
 
-    >>> for status in search_view.widgets['status']._getFormValue():
+    >>> for status in search_view.widgets["status"]._getFormValue():
     ...     print(status.title)
+    ...
     Solved
     Invalid
 
 The user selected search parameters will override these default
 criteria.
 
-    >>> search_view_harness.submit('search', {
-    ...     'field.status': ['SOLVED'],
-    ...     'field.search_text': 'firefox',
-    ...     'field.language': ['en'],
-    ...     'field.sort': 'by relevancy'})
+    >>> search_view_harness.submit(
+    ...     "search",
+    ...     {
+    ...         "field.status": ["SOLVED"],
+    ...         "field.search_text": "firefox",
+    ...         "field.language": ["en"],
+    ...         "field.sort": "by relevancy",
+    ...     },
+    ... )
     >>> search_view = search_view_harness.view
     >>> questions = search_view.searchResults()
     >>> for question in questions.batch:
     ...     print(question.title)
+    ...
     mailto: problem in webpage
 
-    >>> for status in search_view.widgets['status']._getFormValue():
+    >>> for status in search_view.widgets["status"]._getFormValue():
     ...     print(status.title)
+    ...
     Solved
 
 The base view computes the page heading and the message displayed when
 no results are found based on the selected search filter:
 
     >>> from zope.i18n import translate
-    >>> search_view_harness.submit('', {})
+    >>> search_view_harness.submit("", {})
     >>> print(translate(search_view_harness.view.page_title))
     Questions for Ubuntu
 
@@ -887,8 +979,9 @@ no results are found based on the selected search filter:
     There are no questions for Ubuntu with the requested statuses.
 
     >>> MyCustomSearchQuestionsView.default_filter = dict(
-    ...     status=[QuestionStatus.OPEN], search_text='Firefox')
-    >>> search_view_harness.submit('', {})
+    ...     status=[QuestionStatus.OPEN], search_text="Firefox"
+    ... )
+    >>> search_view_harness.submit("", {})
     >>> print(translate(search_view_harness.view.page_title))
     Open questions matching "Firefox" for Ubuntu
 
@@ -897,22 +990,30 @@ no results are found based on the selected search filter:
 
 It works also with user submitted values:
 
-    >>> search_view_harness.submit('search', {
-    ...     'field.status': ['EXPIRED'],
-    ...     'field.search_text': '',
-    ...     'field.language': ['en'],
-    ...     'field.sort': 'by relevancy'})
+    >>> search_view_harness.submit(
+    ...     "search",
+    ...     {
+    ...         "field.status": ["EXPIRED"],
+    ...         "field.search_text": "",
+    ...         "field.language": ["en"],
+    ...         "field.sort": "by relevancy",
+    ...     },
+    ... )
     >>> print(translate(search_view_harness.view.page_title))
     Expired questions for Ubuntu
 
     >>> print(translate(search_view_harness.view.empty_listing_message))
     There are no expired questions for Ubuntu.
 
-    >>> search_view_harness.submit('search', {
-    ...     'field.status': ['OPEN', 'ANSWERED'],
-    ...     'field.search_text': 'evolution',
-    ...     'field.language': ['en'],
-    ...     'field.sort': 'by relevancy'})
+    >>> search_view_harness.submit(
+    ...     "search",
+    ...     {
+    ...         "field.status": ["OPEN", "ANSWERED"],
+    ...         "field.search_text": "evolution",
+    ...         "field.language": ["en"],
+    ...         "field.sort": "by relevancy",
+    ...     },
+    ... )
     >>> print(translate(search_view_harness.view.page_title))
     Questions matching "evolution" for Ubuntu
 
@@ -928,20 +1029,21 @@ The SearchQuestionsView has two attributes that control the columns of
 the question listing table. Products display the default columns of
 Summary, Created, Submitter, Assignee, and Status.
 
-    >>> from lp.testing.pages import (
-    ...     extract_text, find_tag_by_id)
+    >>> from lp.testing.pages import extract_text, find_tag_by_id
 
     >>> view = create_initialized_view(
-    ...     firefox, name="+questions", principal=question_three.owner)
+    ...     firefox, name="+questions", principal=question_three.owner
+    ... )
     >>> view.display_sourcepackage_column
     False
 
     >>> view.display_target_column
     False
 
-    >>> table = find_tag_by_id(view.render(), 'question-listing')
-    >>> for row in table.find_all('tr'):
+    >>> table = find_tag_by_id(view.render(), "question-listing")
+    >>> for row in table.find_all("tr"):
     ...     print(extract_text(row))
+    ...
     Summary                Created     Submitter      Assignee  Status
     6 Newly installed...  2005-10-14   Sample Person  —         Answered ...
 
@@ -949,16 +1051,18 @@ Distribution display the "Source Package" column. The name of the source
 package is displayed if it exists.
 
     >>> view = create_initialized_view(
-    ...     ubuntu, name="+questions", principal=question_three.owner)
+    ...     ubuntu, name="+questions", principal=question_three.owner
+    ... )
     >>> view.display_sourcepackage_column
     True
 
     >>> view.display_target_column
     False
 
-    >>> table = find_tag_by_id(view.render(), 'question-listing')
-    >>> for row in table.find_all('tr'):
+    >>> table = find_tag_by_id(view.render(), "question-listing")
+    >>> for row in table.find_all("tr"):
     ...     print(extract_text(row))
+    ...
     Summary  Created     Submitter      Source Package   Assignee  Status ...
     8 ...    2006-07-20  Sample Person  mozilla-firefox  —         Answered
     7 ...    2005-10-14  Foo Bar        —                —         Needs ...
@@ -966,19 +1070,21 @@ package is displayed if it exists.
 ProjectGroups display the "In" column to show the product name.
 
     >>> from lp.registry.interfaces.projectgroup import IProjectGroupSet
-    >>> mozilla = getUtility(IProjectGroupSet).getByName('mozilla')
+    >>> mozilla = getUtility(IProjectGroupSet).getByName("mozilla")
 
     >>> view = create_initialized_view(
-    ...     mozilla, name="+questions", principal=question_three.owner)
+    ...     mozilla, name="+questions", principal=question_three.owner
+    ... )
     >>> view.display_sourcepackage_column
     False
 
     >>> view.display_target_column
     True
 
-    >>> table = find_tag_by_id(view.render(), 'question-listing')
-    >>> for row in table.find_all('tr'):
+    >>> table = find_tag_by_id(view.render(), "question-listing")
+    >>> for row in table.find_all("tr"):
     ...     print(extract_text(row))
+    ...
     Summary  Created     Submitter      In               Assignee  Status
     6 ...    2005-10-14  Sample Person  Mozilla Firefox  —         Answered...
 
@@ -987,18 +1093,21 @@ to the question, or an m-dash if there is no assignee.
 
     >>> question_six = firefox.getQuestion(6)
     >>> question_six.assignee = factory.makePerson(
-    ...     name="bob", displayname="Bob")
+    ...     name="bob", displayname="Bob"
+    ... )
     >>> view = create_initialized_view(
-    ...     firefox, name="+questions", principal=question_three.owner)
+    ...     firefox, name="+questions", principal=question_three.owner
+    ... )
     >>> view.display_sourcepackage_column
     False
 
     >>> view.display_target_column
     False
 
-    >>> table = find_tag_by_id(view.render(), 'question-listing')
-    >>> for row in table.find_all('tr'):
+    >>> table = find_tag_by_id(view.render(), "question-listing")
+    >>> for row in table.find_all("tr"):
     ...     print(extract_text(row))
+    ...
     Summary  Created     Submitter      Assignee  Status
     6 ...    2005-10-14  Sample Person  Bob       Answered
     4 ...    2005-09-05  Foo Bar        —         Open ...
@@ -1016,24 +1125,28 @@ himself or the Ubuntu Team as answer contact for ubuntu:
     >>> list(ubuntu.answer_contacts)
     []
 
-    >>> login('jeff.waugh@xxxxxxxxxxxxxxx')
+    >>> login("jeff.waugh@xxxxxxxxxxxxxxx")
     >>> jeff_waugh = getUtility(ILaunchBag).user
 
     >>> from lp.registry.interfaces.person import IPersonSet
-    >>> ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
+    >>> ubuntu_team = getUtility(IPersonSet).getByName("ubuntu-team")
     >>> jeff_waugh in ubuntu_team.getDirectAdministrators()
     True
 
     >>> request = LaunchpadTestRequest(
-    ...     method='POST', form={
-    ...         'field.actions.update': 'Continue',
-    ...         'field.want_to_be_answer_contact': 'on',
-    ...         'field.answer_contact_teams': 'ubuntu-team'})
+    ...     method="POST",
+    ...     form={
+    ...         "field.actions.update": "Continue",
+    ...         "field.want_to_be_answer_contact": "on",
+    ...         "field.answer_contact_teams": "ubuntu-team",
+    ...     },
+    ... )
     >>> view = getMultiAdapter((ubuntu, request), name="+answer-contact")
     >>> view.initialize()
 
     >>> for person in sorted(
-    ...         ubuntu.direct_answer_contacts, key=attrgetter('displayname')):
+    ...     ubuntu.direct_answer_contacts, key=attrgetter("displayname")
+    ... ):
     ...     print(person.displayname)
     Jeff Waugh
     Ubuntu Team
@@ -1042,6 +1155,7 @@ The view adds notifications about the answer contacts added:
 
     >>> for notification in request.notifications:
     ...     print(notification.message)
+    ...
     <...Your preferred languages... were updated to include ...English (en).
     You have been added as an answer contact for Ubuntu.
     English was added to Ubuntu Team's ...preferred languages...
@@ -1050,20 +1164,24 @@ The view adds notifications about the answer contacts added:
 But Daniel Silverstone is only a regular member of Ubuntu Team, so he
 can only subscribe himself:
 
-    >>> login('daniel.silverstone@xxxxxxxxxxxxx')
+    >>> login("daniel.silverstone@xxxxxxxxxxxxx")
     >>> kinnison = getUtility(ILaunchBag).user
     >>> kinnison in ubuntu_team.getDirectAdministrators()
     False
 
     >>> request = LaunchpadTestRequest(
-    ...     method='POST', form={
-    ...         'field.actions.update': 'Continue',
-    ...         'field.want_to_be_answer_contact': 'on'})
+    ...     method="POST",
+    ...     form={
+    ...         "field.actions.update": "Continue",
+    ...         "field.want_to_be_answer_contact": "on",
+    ...     },
+    ... )
     >>> view = getMultiAdapter((ubuntu, request), name="+answer-contact")
     >>> view.initialize()
 
     >>> for person in sorted(
-    ...         ubuntu.direct_answer_contacts, key=attrgetter('displayname')):
+    ...     ubuntu.direct_answer_contacts, key=attrgetter("displayname")
+    ... ):
     ...     print(person.displayname)
     Daniel Silverstone
     Jeff Waugh
@@ -1071,6 +1189,7 @@ can only subscribe himself:
 
     >>> for notification in request.notifications:
     ...     print(notification.message)
+    ...
     <...Your preferred languages... were updated to include ...English (en).
     You have been added as an answer contact for Ubuntu.
 
@@ -1078,39 +1197,49 @@ The same view is used to remove answer contact registrations. The user
 can only remove their own registration.
 
     >>> request = LaunchpadTestRequest(
-    ...     method='POST', form={
-    ...         'field.actions.update': 'Continue',
-    ...         'field.want_to_be_answer_contact': 'off'})
+    ...     method="POST",
+    ...     form={
+    ...         "field.actions.update": "Continue",
+    ...         "field.want_to_be_answer_contact": "off",
+    ...     },
+    ... )
     >>> view = getMultiAdapter((ubuntu, request), name="+answer-contact")
     >>> view.initialize()
 
     >>> for person in sorted(
-    ...         ubuntu.direct_answer_contacts, key=attrgetter('displayname')):
+    ...     ubuntu.direct_answer_contacts, key=attrgetter("displayname")
+    ... ):
     ...     print(person.displayname)
     Jeff Waugh
     Ubuntu Team
 
     >>> for notification in request.notifications:
     ...     print(notification.message)
+    ...
     You have been removed as an answer contact for Ubuntu.
 
 It can also be used to remove a team registration when the user is a
 team administrator:
 
-    >>> login('jeff.waugh@xxxxxxxxxxxxxxx')
+    >>> login("jeff.waugh@xxxxxxxxxxxxxxx")
     >>> request = LaunchpadTestRequest(
-    ...     method='POST', form={
-    ...         'field.actions.update': 'Continue',
-    ...         'field.want_to_be_answer_contact': 'on',
-    ...         'field.answer_contact_teams-empty_marker': '1'})
+    ...     method="POST",
+    ...     form={
+    ...         "field.actions.update": "Continue",
+    ...         "field.want_to_be_answer_contact": "on",
+    ...         "field.answer_contact_teams-empty_marker": "1",
+    ...     },
+    ... )
     >>> view = getMultiAdapter((ubuntu, request), name="+answer-contact")
     >>> view.initialize()
 
     >>> for person in sorted(
-    ...         ubuntu.direct_answer_contacts, key=attrgetter('displayname')):
+    ...     ubuntu.direct_answer_contacts, key=attrgetter("displayname")
+    ... ):
     ...     print(person.displayname)
     Jeff Waugh
 
     >>> for notification in request.notifications:
     ...     print(notification.message)
+    ...
     Ubuntu Team has been removed as an answer contact for Ubuntu.
diff --git a/lib/lp/answers/doc/expiration.rst b/lib/lp/answers/doc/expiration.rst
index 6fbb0d2..72d5087 100644
--- a/lib/lp/answers/doc/expiration.rst
+++ b/lib/lp/answers/doc/expiration.rst
@@ -23,8 +23,11 @@ somebody are subject to expiration.
     >>> from lp.answers.enums import QuestionStatus
     >>> from lp.answers.model.question import Question
     >>> IStore(Question).find(
-    ...     Question, Question.status.is_in(
-    ...         (QuestionStatus.OPEN, QuestionStatus.NEEDSINFO))).count()
+    ...     Question,
+    ...     Question.status.is_in(
+    ...         (QuestionStatus.OPEN, QuestionStatus.NEEDSINFO)
+    ...     ),
+    ... ).count()
     9
 
     # By default, all open and needs info question should expire. Make
@@ -38,7 +41,9 @@ somebody are subject to expiration.
     ...     Question,
     ...     Or(
     ...         Question.datelastresponse >= interval,
-    ...         Question.datelastquery >= interval)).count()
+    ...         Question.datelastquery >= interval,
+    ...     ),
+    ... ).count()
     0
 
     # We need to massage sample data a little. Since all expiration
@@ -52,7 +57,7 @@ somebody are subject to expiration.
     >>> from lp.services.webapp.interfaces import ILaunchBag
     >>> from lp.answers.interfaces.questioncollection import IQuestionSet
     >>> from lp.registry.interfaces.person import IPersonSet
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> no_priv = getUtility(ILaunchBag).user
 
     >>> questionset = getUtility(IQuestionSet)
@@ -63,21 +68,23 @@ somebody are subject to expiration.
     Needs information
 
     # An open question assigned to somebody.
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> old_assigned_open_question = questionset.get(1)
     >>> old_assigned_open_question.assignee = getUtility(ILaunchBag).user
 
     # This one got an update from its owner recently.
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> recent_open_question = questionset.get(2)
     >>> recent_open_question.giveInfo(
-    ...     'SVG works better now, but is still broken')
+    ...     "SVG works better now, but is still broken"
+    ... )
     <lp.answers.model.questionmessage.QuestionMessage...>
 
     # This one was put in the NEEDSINFO state recently.
     >>> recent_needsinfo_question = questionset.get(4)
     >>> recent_needsinfo_question.requestInfo(
-    ...     no_priv, 'What URL were you visiting?')
+    ...     no_priv, "What URL were you visiting?"
+    ... )
     <lp.answers.model.questionmessage.QuestionMessage...>
 
     # Old open questions.
@@ -85,10 +92,10 @@ somebody are subject to expiration.
 
     # Subscribe a team to that question, and a answer contact,
     # to make sure that DB permissions are correct.
-    >>> admin_team = getUtility(IPersonSet).getByName('admins')
+    >>> admin_team = getUtility(IPersonSet).getByName("admins")
     >>> old_open_question.subscribe(admin_team)
     <lp.answers.model.questionsubscription.QuestionSubscription...>
-    >>> salgado = getUtility(IPersonSet).getByName('salgado')
+    >>> salgado = getUtility(IPersonSet).getByName("salgado")
     >>> old_open_question.target.addAnswerContact(salgado, salgado)
     True
 
@@ -97,10 +104,12 @@ somebody are subject to expiration.
     # the last updates date of the question and remove it from the expiration
     # set.
     >>> from zope.security.proxy import removeSecurityProxy
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> faq = old_open_question.target.newFAQ(
-    ...     salgado, 'Why everyone think this is weird.',
-    ...     "That's an easy one. It's because it is!")
+    ...     salgado,
+    ...     "Why everyone think this is weird.",
+    ...     "That's an easy one. It's because it is!",
+    ... )
     >>> removeSecurityProxy(old_open_question).faq = faq
 
     # A question linked to an non-Invalid bug is not expirable.
@@ -138,9 +147,13 @@ somebody are subject to expiration.
     # Run the script.
     >>> import subprocess
     >>> process = subprocess.Popen(
-    ...     'cronscripts/expire-questions.py', shell=True,
-    ...     stdin=subprocess.PIPE, stdout=subprocess.PIPE,
-    ...     stderr=subprocess.PIPE, universal_newlines=True)
+    ...     "cronscripts/expire-questions.py",
+    ...     shell=True,
+    ...     stdin=subprocess.PIPE,
+    ...     stdout=subprocess.PIPE,
+    ...     stderr=subprocess.PIPE,
+    ...     universal_newlines=True,
+    ... )
     >>> (out, err) = process.communicate()
     >>> print(err)
     INFO    Creating lockfile: /var/lock/launchpad-expire-questions.lock
diff --git a/lib/lp/answers/doc/faq-vocabulary.rst b/lib/lp/answers/doc/faq-vocabulary.rst
index 933eb19..e6a3fd0 100644
--- a/lib/lp/answers/doc/faq-vocabulary.rst
+++ b/lib/lp/answers/doc/faq-vocabulary.rst
@@ -11,8 +11,8 @@ collection. It provides the IHugeVocabulary interface.
     >>> from lp.registry.interfaces.product import IProductSet
 
     >>> vocabulary_registry = getVocabularyRegistry()
-    >>> firefox = getUtility(IProductSet).getByName('firefox')
-    >>> vocabulary = vocabulary_registry.get(firefox, 'FAQ')
+    >>> firefox = getUtility(IProductSet).getByName("firefox")
+    >>> vocabulary = vocabulary_registry.get(firefox, "FAQ")
     >>> verifyObject(IHugeVocabulary, vocabulary)
     True
 
@@ -26,10 +26,11 @@ collections:
     >>> vocabulary_faqs = set(term.value for term in vocabulary)
     >>> for item in firefox_faqs.symmetric_difference(vocabulary_faqs):
     ...     print(item)
+    ...
 
 And it only contains FAQs:
 
-    >>> u'10' in vocabulary
+    >>> "10" in vocabulary
     False
 
 The term's token is the FAQ's id and its title is the FAQ's title:
@@ -43,9 +44,8 @@ The term's token is the FAQ's id and its title is the FAQ's title:
 
 Asking for something which isn't a FAQ of the target raises LookupError:
 
-    >>> from lp.registry.interfaces.distribution import (
-    ...     IDistributionSet)
-    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
+    >>> from lp.registry.interfaces.distribution import IDistributionSet
+    >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
     >>> ubuntu_faq = ubuntu.getFAQ(1)
     >>> vocabulary.getTerm(ubuntu_faq)
     Traceback (most recent call last):
@@ -55,18 +55,18 @@ Asking for something which isn't a FAQ of the target raises LookupError:
 Since IHugeVocabulary extends IVocabularyTokenized, the term can also
 be retrieved by token:
 
-    >>> term = vocabulary.getTermByToken(u'10')
+    >>> term = vocabulary.getTermByToken("10")
     >>> print(term.title)
     How do I install plugins (Shockwave, QuickTime, etc.)?
 
 Trying to retrieve an invalid or non-existent token raises LookupError:
 
-    >>> vocabulary.getTermByToken('not a good token')
+    >>> vocabulary.getTermByToken("not a good token")
     Traceback (most recent call last):
       ...
     LookupError:...
 
-    >>> vocabulary.getTermByToken('1001')
+    >>> vocabulary.getTermByToken("1001")
     Traceback (most recent call last):
       ...
     LookupError:...
@@ -76,12 +76,13 @@ are similar to the query:
 
     >>> from zope.security import proxy
     >>> from lp.services.webapp.vocabulary import CountableIterator
-    >>> terms = vocabulary.searchForTerms('extensions')
+    >>> terms = vocabulary.searchForTerms("extensions")
     >>> proxy.isinstance(terms, CountableIterator)
     True
     >>> terms.count()
     2
     >>> for term in terms:
     ...     print(term.title)
+    ...
     How do I install Extensions?
     How do I troubleshoot problems with extensions/themes?
diff --git a/lib/lp/answers/doc/faq.rst b/lib/lp/answers/doc/faq.rst
index 10be05e..1ead529 100644
--- a/lib/lp/answers/doc/faq.rst
+++ b/lib/lp/answers/doc/faq.rst
@@ -17,7 +17,7 @@ provided by objects that can host FAQs.
     >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> from lp.registry.interfaces.product import IProductSet
 
-    >>> firefox = getUtility(IProductSet).getByName('firefox')
+    >>> firefox = getUtility(IProductSet).getByName("firefox")
 
     # removeSecurityProxy() is needed because not all interface
     # attributes are available to everybody.
@@ -25,7 +25,7 @@ provided by objects that can host FAQs.
     >>> verifyObject(IFAQTarget, removeSecurityProxy(firefox))
     True
 
-    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
+    >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
     >>> verifyObject(IFAQTarget, removeSecurityProxy(ubuntu))
     True
 
@@ -33,7 +33,7 @@ Any user who has 'launchpad.Append' on the project can create a new
 FAQ. (That permission is granted to project's registrant and answer
 contacts.)
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
 
     >>> from lp.services.webapp.interfaces import ILaunchBag
     >>> sample_person = getUtility(ILaunchBag).user
@@ -41,8 +41,10 @@ contacts.)
     Sample Person
 
     >>> firefox_faq = firefox.newFAQ(
-    ...     sample_person, 'How can I see the Fnords?',
-    ...     "Install the Fnords highlighter extension and see the Fnords!")
+    ...     sample_person,
+    ...     "How can I see the Fnords?",
+    ...     "Install the Fnords highlighter extension and see the Fnords!",
+    ... )
 
 (The complete description of IFAQTarget is available in faqtarget.rst)
 
@@ -56,25 +58,27 @@ a suitable IFAQTarget from objects that do not provide it directly.
 It is possible to adapt an IDistributionSourcePackage to IFAQTarget,
 (the distribution is really the appropriate IFAQTarget in this case):
 
-    >>> mozilla_firefox = ubuntu.getSourcePackage('mozilla-firefox')
+    >>> mozilla_firefox = ubuntu.getSourcePackage("mozilla-firefox")
     >>> IFAQTarget.providedBy(mozilla_firefox)
     False
 
     >>> mozilla_firefox_faq_target = IFAQTarget(mozilla_firefox)
     >>> verifyObject(
-    ...     IFAQTarget, removeSecurityProxy(mozilla_firefox_faq_target))
+    ...     IFAQTarget, removeSecurityProxy(mozilla_firefox_faq_target)
+    ... )
     True
 
 Likewise, it is possible to adapt an ISourcePackage to IFAQTarget.
 
-    >>> hoary = ubuntu.getSeries('hoary')
-    >>> hoary_mozilla_firefox = hoary.getSourcePackage('mozilla-firefox')
+    >>> hoary = ubuntu.getSeries("hoary")
+    >>> hoary_mozilla_firefox = hoary.getSourcePackage("mozilla-firefox")
     >>> IFAQTarget.providedBy(hoary_mozilla_firefox)
     False
 
     >>> hoary_firefox_faq_target = IFAQTarget(hoary_mozilla_firefox)
     >>> verifyObject(
-    ...     IFAQTarget, removeSecurityProxy(hoary_firefox_faq_target))
+    ...     IFAQTarget, removeSecurityProxy(hoary_firefox_faq_target)
+    ... )
     True
 
 It is also possible to adapt an IQuestion into an IFAQTarget. This
@@ -104,7 +108,7 @@ for FAQs. It is provided by product, distribution, and projects.
 
     >>> from lp.answers.interfaces.faqcollection import IFAQCollection
     >>> from lp.registry.interfaces.projectgroup import IProjectGroupSet
-    >>> gnome = getUtility(IProjectGroupSet).getByName('gnome')
+    >>> gnome = getUtility(IProjectGroupSet).getByName("gnome")
     >>> verifyObject(IFAQCollection, gnome)
     True
 
@@ -155,8 +159,9 @@ Initially, the last_updated_by and date_last_updated are not set.
 When the FAQ is modified, the attributes are automatically updated.
 
     >>> from lp.services.webapp.snapshot import notify_modified
-    >>> with notify_modified(firefox_faq, ['keywords'], user=sample_person):
-    ...     firefox_faq.keywords = 'extension'
+    >>> with notify_modified(firefox_faq, ["keywords"], user=sample_person):
+    ...     firefox_faq.keywords = "extension"
+    ...
 
     >>> print(firefox_faq.last_updated_by.displayname)
     Sample Person
@@ -173,16 +178,16 @@ Only the project owners or answer contacts can edit an IFAQ.
     >>> from lp.services.webapp.authorization import check_permission
 
     >>> login(ANONYMOUS)
-    >>> check_permission('launchpad.Edit', firefox_faq)
+    >>> check_permission("launchpad.Edit", firefox_faq)
     False
 
 So Sample Person (the project owner) has edit permission:
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> print(firefox.owner.displayname)
     Sample Person
 
-    >>> check_permission('launchpad.Edit', firefox_faq)
+    >>> check_permission("launchpad.Edit", firefox_faq)
     True
 
 Answer contacts can also edit FAQs:
@@ -191,13 +196,13 @@ Answer contacts can also edit FAQs:
 
     >>> from lp.services.worlddata.interfaces.language import ILanguageSet
     >>> no_priv = getUtility(ILaunchBag).user
-    >>> no_priv.addLanguage(getUtility(ILanguageSet)['en'])
+    >>> no_priv.addLanguage(getUtility(ILanguageSet)["en"])
     >>> firefox.addAnswerContact(no_priv, no_priv)
     True
 
     >>> from lp.services.webapp.authorization import clear_cache
     >>> clear_cache()
-    >>> check_permission('launchpad.Edit', firefox_faq)
+    >>> check_permission("launchpad.Edit", firefox_faq)
     True
 
 
@@ -225,10 +230,11 @@ It can retrieve any FAQ by id using the getFAQ() method.
 The searchFAQs() method can be used to find FAQs by keywords or owner.
 
     >>> from lp.registry.interfaces.person import IPersonSet
-    >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@xxxxxxxxxxxxx')
+    >>> foo_bar = getUtility(IPersonSet).getByEmail("foo.bar@xxxxxxxxxxxxx")
     >>> for faq in faqset.searchFAQs(
-    ...     search_text=u'java OR flash', owner=foo_bar):
-    ...     print('%s (%s)' % (faq.title, faq.target.displayname))
+    ...     search_text="java OR flash", owner=foo_bar
+    ... ):
+    ...     print("%s (%s)" % (faq.title, faq.target.displayname))
     How do I install plugins (Shockwave, QuickTime, etc.)? (Mozilla Firefox)
     How can I play MP3/Divx/DVDs/Quicktime/Realmedia files
         or view Flash/Java web pages (Ubuntu)
@@ -245,15 +251,16 @@ posting the answer, the FAQ containing the answer and a comment that
 will be added to the question explaining the FAQ link.
 
     >>> fnord_question = firefox.newQuestion(
-    ...     sample_person, 'Are there Fnords on the web?',
-    ...     'Do Fnords also exists on the web?')
+    ...     sample_person,
+    ...     "Are there Fnords on the web?",
+    ...     "Do Fnords also exists on the web?",
+    ... )
 
 Any user can link an existing FAQ to a question.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> no_priv = getUtility(ILaunchBag).user
-    >>> message = fnord_question.linkFAQ(
-    ...     no_priv, firefox_faq, 'See the FAQ.')
+    >>> message = fnord_question.linkFAQ(no_priv, firefox_faq, "See the FAQ.")
 
 Once the FAQ is linked, the question is considered 'answered':
 
@@ -275,14 +282,17 @@ answered by the FAQ:
 
     >>> for question in firefox_faq.related_questions:
     ...     print(question.title)
+    ...
     Are there Fnords on the web?
 
 A FAQ can be linked to multiple question:
 
     >>> other_question = firefox.getQuestion(4)
     >>> message = other_question.linkFAQ(
-    ...     no_priv, firefox_faq,
-    ...     'If you lose focus and gets stuck it must be the fnords!')
+    ...     no_priv,
+    ...     firefox_faq,
+    ...     "If you lose focus and gets stuck it must be the fnords!",
+    ... )
 
     >>> print(other_question.faq.title)
     How can I see the Fnords?
@@ -292,6 +302,7 @@ A FAQ can be linked to multiple question:
 
     >>> for question in firefox_faq.related_questions:
     ...     print(question.title)
+    ...
     Firefox loses focus and gets stuck
     Are there Fnords on the web?
 
@@ -299,7 +310,8 @@ The FAQ link can be changed or removed by using the linkFAQ() method
 again:
 
     >>> message = other_question.linkFAQ(
-    ...     no_priv, None, "This has nothing to do with Fnords.")
+    ...     no_priv, None, "This has nothing to do with Fnords."
+    ... )
     >>> print(other_question.faq)
     None
 
@@ -307,6 +319,7 @@ After this, only the original question will remain linked to the FAQ.
 
     >>> for question in firefox_faq.related_questions:
     ...     print(question.title)
+    ...
     Are there Fnords on the web?
 
 That change is also considered an answer:
@@ -326,8 +339,7 @@ It is not possible to modify the faq attribute directly:
 
 And it is not allowed to call linkFAQ() when the FAQ is already linked:
 
-    >>> message = fnord_question.linkFAQ(
-    ...     no_priv, firefox_faq, 'See the FAQ.')
+    >>> message = fnord_question.linkFAQ(no_priv, firefox_faq, "See the FAQ.")
     Traceback (most recent call last):
       ...
     lp.answers.errors.FAQTargetError: Cannot call linkFAQ() with already
@@ -336,16 +348,19 @@ And it is not allowed to call linkFAQ() when the FAQ is already linked:
 A FAQ can be linked to a 'solved' question, in which case, the status is
 not changed.
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> confirm_message = other_question.confirmAnswer(
-    ...     "That answered my question.", answer=other_question.messages[-1])
+    ...     "That answered my question.", answer=other_question.messages[-1]
+    ... )
     >>> print(other_question.status.title)
     Solved
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> message = other_question.linkFAQ(
-    ...     no_priv, firefox_faq,
-    ...     'If you look carefully, you will find the fnords!')
+    ...     no_priv,
+    ...     firefox_faq,
+    ...     "If you look carefully, you will find the fnords!",
+    ... )
     >>> print(message.action.title)
     Comment
 
diff --git a/lib/lp/answers/doc/faqcollection.rst b/lib/lp/answers/doc/faqcollection.rst
index 3314950..b5246dd 100644
--- a/lib/lp/answers/doc/faqcollection.rst
+++ b/lib/lp/answers/doc/faqcollection.rst
@@ -30,27 +30,51 @@ collection.
 
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> personset = getUtility(IPersonSet)
-    >>> no_priv = personset.getByName('no-priv')
-    >>> foo_bar = personset.getByEmail('foo.bar@xxxxxxxxxxxxx')
-    >>> sample_person = personset.getByEmail('test@xxxxxxxxxxxxx')
+    >>> no_priv = personset.getByName("no-priv")
+    >>> foo_bar = personset.getByEmail("foo.bar@xxxxxxxxxxxxx")
+    >>> sample_person = personset.getByEmail("test@xxxxxxxxxxxxx")
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> faq_specifications = [
-    ...     (no_priv, 'How do I install Foo?',
-    ...      'See http://www.foo.org/install', 'foo install'),
-    ...     (sample_person, 'What is The Meaning of Life?',
-    ...      'A Monty Python film!', 'film monty python'),
-    ...     (foo_bar, 'How do I make money quickly off the Internet?',
-    ...      'Install this really nice software that you can find at '
-    ...      'http://www.getrichquick.com/.', None),
-    ...     (no_priv, 'How do I play the Game of Life?',
-    ...      'Keep breathing!', 'install'),
-    ...     (sample_person, 'Who really shot JFK?',
-    ...     'You decide: there were at least six conspiracies going on '
-    ...     'in Dallas on Nov 22nd 1963.', None),
-    ...     (no_priv, 'What were the famous last words?',
-    ...      'Who turned off the light?', None)
-    ...    ]
+    ...     (
+    ...         no_priv,
+    ...         "How do I install Foo?",
+    ...         "See http://www.foo.org/install";,
+    ...         "foo install",
+    ...     ),
+    ...     (
+    ...         sample_person,
+    ...         "What is The Meaning of Life?",
+    ...         "A Monty Python film!",
+    ...         "film monty python",
+    ...     ),
+    ...     (
+    ...         foo_bar,
+    ...         "How do I make money quickly off the Internet?",
+    ...         "Install this really nice software that you can find at "
+    ...         "http://www.getrichquick.com/.";,
+    ...         None,
+    ...     ),
+    ...     (
+    ...         no_priv,
+    ...         "How do I play the Game of Life?",
+    ...         "Keep breathing!",
+    ...         "install",
+    ...     ),
+    ...     (
+    ...         sample_person,
+    ...         "Who really shot JFK?",
+    ...         "You decide: there were at least six conspiracies going on "
+    ...         "in Dallas on Nov 22nd 1963.",
+    ...         None,
+    ...     ),
+    ...     (
+    ...         no_priv,
+    ...         "What were the famous last words?",
+    ...         "Who turned off the light?",
+    ...         None,
+    ...     ),
+    ... ]
 
     >>> from datetime import datetime, timedelta
     >>> from pytz import UTC
@@ -60,6 +84,7 @@ collection.
     >>> for owner, title, content, keywords in faq_specifications:
     ...     date = now + timedelta(minutes=len(faq_set))
     ...     faq_set.append(newFAQ(owner, title, content, keywords, date))
+    ...
 
     >>> login(ANONYMOUS)
 
@@ -84,16 +109,18 @@ requested collection:
 
     >>> from lp.services.webapp.interfaces import ILaunchBag
     >>> from lp.registry.interfaces.distribution import IDistributionSet
-    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
+    >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
     >>> ubuntu != collection
     True
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> foo_bar = getUtility(ILaunchBag).user
     >>> ubuntu_faq = ubuntu.newFAQ(
-    ...     foo_bar, 'Ubuntu Installation HowTo',
-    ...     'Ubuntu installation procedure can be found at: '
-    ...     'https://help.ubuntu.com/community/Installation')
+    ...     foo_bar,
+    ...     "Ubuntu Installation HowTo",
+    ...     "Ubuntu installation procedure can be found at: "
+    ...     "https://help.ubuntu.com/community/Installation";,
+    ... )
 
     >>> login(ANONYMOUS)
     >>> print(collection.getFAQ(ubuntu_faq.id))
@@ -111,6 +138,7 @@ When no criteria are given, all the FAQs in the collection are returned.
 
     >>> for faq in collection.searchFAQs():
     ...     print(faq.title)
+    ...
     What were the famous last words?
     Who really shot JFK?
     How do I play the Game of Life?
@@ -126,8 +154,9 @@ The first criterion is search_text. It will select FAQs matching the
 keywords specified. Keywords are looked for in the title, content and
 keywords field of the FAQ.
 
-    >>> for faq in collection.searchFAQs(search_text=u'install'):
+    >>> for faq in collection.searchFAQs(search_text="install"):
     ...     print(faq.title)
+    ...
     How do I install Foo?
     How do I play the Game of Life?
     How do I make money quickly off the Internet?
@@ -145,6 +174,7 @@ were created by the specified user.
 
     >>> for faq in collection.searchFAQs(owner=no_priv):
     ...     print(faq.title)
+    ...
     What were the famous last words?
     How do I play the Game of Life?
     How do I install Foo?
@@ -159,7 +189,8 @@ You can combine multiple criteria. Only FAQs matching all the criteria
 will be returned.
 
     >>> for faq in collection.searchFAQs(
-    ...         search_text=u'install', owner=no_priv):
+    ...     search_text="install", owner=no_priv
+    ... ):
     ...     print(faq.title)
     How do I install Foo?
     How do I play the Game of Life?
@@ -175,7 +206,8 @@ date of creation (most recent first):
 
     >>> from lp.answers.interfaces.faqcollection import FAQSort
     >>> for faq in collection.searchFAQs(
-    ...         search_text=u'install', sort=FAQSort.NEWEST_FIRST):
+    ...     search_text="install", sort=FAQSort.NEWEST_FIRST
+    ... ):
     ...     print(faq.title)
     How do I play the Game of Life?
     How do I make money quickly off the Internet?
@@ -186,6 +218,7 @@ first:
 
     >>> for faq in collection.searchFAQs(sort=FAQSort.OLDEST_FIRST):
     ...     print(faq.title)
+    ...
     How do I install Foo?
     What is The Meaning of Life?
     How do I make money quickly off the Internet?
diff --git a/lib/lp/answers/doc/faqtarget.rst b/lib/lp/answers/doc/faqtarget.rst
index 548243a..d02cdd9 100644
--- a/lib/lp/answers/doc/faqtarget.rst
+++ b/lib/lp/answers/doc/faqtarget.rst
@@ -30,14 +30,14 @@ The newFAQ() method is used to create a new IFAQ object on the target.
 That method is only available to a user who has 'launchpad.Append' on
 the target.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> from lp.services.webapp.authorization import check_permission
     >>> from lp.services.webapp.interfaces import ILaunchBag
-    >>> check_permission('launchpad.Append', target)
+    >>> check_permission("launchpad.Append", target)
     False
 
     >>> no_priv = getUtility(ILaunchBag).user
-    >>> target.newFAQ(no_priv, 'Title', 'Summary', content='Content')
+    >>> target.newFAQ(no_priv, "Title", "Summary", content="Content")
     Traceback (most recent call last):
       ...
     zope.security.interfaces.Unauthorized: ...
@@ -49,8 +49,8 @@ or one of its answer contacts is authorized to create a new FAQ.
 
     >>> removeSecurityProxy(target).owner = no_priv
     >>> from lp.services.webapp.authorization import clear_cache
-    >>> clear_cache() # Clear authorization cache for check_permission.
-    >>> check_permission('launchpad.Append', target)
+    >>> clear_cache()  # Clear authorization cache for check_permission.
+    >>> check_permission("launchpad.Append", target)
     True
 
     >>> removeSecurityProxy(target).owner = old_owner
@@ -58,15 +58,15 @@ or one of its answer contacts is authorized to create a new FAQ.
     # An answer contact must have a preferred language registered.
 
     >>> from lp.services.worlddata.interfaces.language import ILanguageSet
-    >>> no_priv.addLanguage(getUtility(ILanguageSet)['en'])
+    >>> no_priv.addLanguage(getUtility(ILanguageSet)["en"])
     >>> target.addAnswerContact(no_priv, no_priv)
     True
 
-    >>> clear_cache() # Clear authorization cache for check_permission.
-    >>> check_permission('launchpad.Append', target)
+    >>> clear_cache()  # Clear authorization cache for check_permission.
+    >>> check_permission("launchpad.Append", target)
     True
 
-    >>> faq = target.newFAQ(no_priv, 'Title', 'Content')
+    >>> faq = target.newFAQ(no_priv, "Title", "Content")
 
 The returned object provides the IFAQ interface:
 
@@ -74,6 +74,7 @@ The returned object provides the IFAQ interface:
     >>> from lp.testing import admin_logged_in
     >>> with admin_logged_in():
     ...     verifyObject(IFAQ, faq)
+    ...
     True
 
 The newFAQ() requires an owner, title, and content parameter. It also
@@ -86,8 +87,12 @@ FAQ's keywords.
     >>> now = datetime.now(UTC)
 
     >>> faq = target.newFAQ(
-    ...     no_priv, 'How to do something', 'Explain how to do something.',
-    ...     keywords='documentation howto', date_created=now)
+    ...     no_priv,
+    ...     "How to do something",
+    ...     "Explain how to do something.",
+    ...     keywords="documentation howto",
+    ...     date_created=now,
+    ... )
 
     >>> print(faq.owner.displayname)
     No Privileges Person
@@ -132,18 +137,20 @@ requested target:
 
     >>> from lp.services.webapp.interfaces import ILaunchBag
     >>> from lp.registry.interfaces.distribution import IDistributionSet
-    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
+    >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
     >>> ubuntu != target
     True
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> foo_bar = getUtility(ILaunchBag).user
     >>> ubuntu_faq = ubuntu.newFAQ(
-    ...     foo_bar, 'Ubuntu Installation HowTo',
-    ...     'Ubuntu installation procedure can be found at: '
-    ...     'https://help.ubuntu.com/community/Installation')
+    ...     foo_bar,
+    ...     "Ubuntu Installation HowTo",
+    ...     "Ubuntu installation procedure can be found at: "
+    ...     "https://help.ubuntu.com/community/Installation";,
+    ... )
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> print(target.getFAQ(ubuntu_faq.id))
     None
 
@@ -163,20 +170,27 @@ common words and stop words.
     # Create more FAQs.
 
     >>> faq = target.newFAQ(
-    ...     no_priv, 'How to answer a question',
-    ...     'Description on how to use the Answer Tracker can be found at: '
-    ...     'https://help.launchpad.net/AnswerTrackerDocumentation')
+    ...     no_priv,
+    ...     "How to answer a question",
+    ...     "Description on how to use the Answer Tracker can be found at: "
+    ...     "https://help.launchpad.net/AnswerTrackerDocumentation";,
+    ... )
     >>> faq = target.newFAQ(
-    ...     no_priv, 'How to become a Launchpad king',
-    ...     'The secret to achieve uber-karma is to answer questions using '
-    ...     'the Launchpad Answer Tracker')
+    ...     no_priv,
+    ...     "How to become a Launchpad king",
+    ...     "The secret to achieve uber-karma is to answer questions using "
+    ...     "the Launchpad Answer Tracker",
+    ... )
     >>> faq = target.newFAQ(
-    ...     no_priv, 'How to use bug mail',
-    ...     'The syntax of bug mail commands is described at: '
-    ...     'https://help.launchpad.net/BugTrackerEmailInterface')
+    ...     no_priv,
+    ...     "How to use bug mail",
+    ...     "The syntax of bug mail commands is described at: "
+    ...     "https://help.launchpad.net/BugTrackerEmailInterface";,
+    ... )
 
-    >>> for faq in target.findSimilarFAQs('How do I use the Answer Tracker'):
+    >>> for faq in target.findSimilarFAQs("How do I use the Answer Tracker"):
     ...     print(faq.title)
+    ...
     How to answer a question
     How to become a Launchpad king
 
@@ -186,8 +200,9 @@ appear in the content in the other document).
 
 If there are no similar FAQ, no result should be returned:
 
-    >>> for faq in target.findSimilarFAQs('How do I do this?'):
+    >>> for faq in target.findSimilarFAQs("How do I do this?"):
     ...     print(faq.title)
+    ...
 
 Since only common and stop words are in that summary, no similar FAQ
 could be found.
diff --git a/lib/lp/answers/doc/karma.rst b/lib/lp/answers/doc/karma.rst
index 8de44d0..c38e049 100644
--- a/lib/lp/answers/doc/karma.rst
+++ b/lib/lp/answers/doc/karma.rst
@@ -13,11 +13,13 @@ actions we consider to be a reasonable contribution.
     >>> from lp.registry.interfaces.product import IProductSet
     >>> from lp.registry.model.karma import KarmaCategory
     >>> from lp.services.database.interfaces import IStore
-    >>> answers_category = IStore(KarmaCategory).find(
-    ...     KarmaCategory, name="answers").one()
+    >>> answers_category = (
+    ...     IStore(KarmaCategory).find(KarmaCategory, name="answers").one()
+    ... )
     >>> answers_karma_actions = answers_category.karmaactions
-    >>> for action in sorted(answers_karma_actions, key=attrgetter('title')):
+    >>> for action in sorted(answers_karma_actions, key=attrgetter("title")):
     ...     print(action.title)
+    ...
     Answered question
     Asked question
     Comment made on a question
@@ -35,8 +37,8 @@ actions we consider to be a reasonable contribution.
     Solved own question
 
     >>> person_set = getUtility(IPersonSet)
-    >>> sample_person = person_set.getByEmail('test@xxxxxxxxxxxxx')
-    >>> foo_bar = person_set.getByEmail('foo.bar@xxxxxxxxxxxxx')
+    >>> sample_person = person_set.getByEmail("test@xxxxxxxxxxxxx")
+    >>> foo_bar = person_set.getByEmail("foo.bar@xxxxxxxxxxxxx")
 
 Setup an event listener to help ensure karma is assigned when it should.
 
@@ -54,6 +56,7 @@ to order our messages.
     ...     while True:
     ...         now += timedelta(seconds=5)
     ...         yield now
+    ...
     >>> now = timegenerator(datetime.now(UTC))
 
 
@@ -64,11 +67,14 @@ Karma Actions
 Creating a question
 ...................
 
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> firefox = getUtility(IProductSet)['firefox']
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> firefox = getUtility(IProductSet)["firefox"]
     >>> firefox_question = firefox.newQuestion(
-    ...     title='New question', description='Question description.',
-    ...     owner=sample_person, datecreated=next(now))
+    ...     title="New question",
+    ...     description="Question description.",
+    ...     owner=sample_person,
+    ...     datecreated=next(now),
+    ... )
     Karma added: action=questionasked, product=firefox, person=name12
 
 
@@ -79,9 +85,11 @@ The expireQuestion() workflow method doesn't grant any karma because it
 will usually be called by an automated script.
 
     >>> msg = firefox_question.expireQuestion(
-    ...     foo_bar, 'Expiring because of inactivity. Reopen if you are '
-    ...     'still having the problem and provide additional information.',
-    ...     datecreated=next(now))
+    ...     foo_bar,
+    ...     "Expiring because of inactivity. Reopen if you are "
+    ...     "still having the problem and provide additional information.",
+    ...     datecreated=next(now),
+    ... )
 
 
 Reopening a question
@@ -89,7 +97,8 @@ Reopening a question
 
     >>> msg = firefox_question.reopen(
     ...     "Firefox doesn't have any 'Quick Searches' in its bookmarks.",
-    ...     datecreated=next(now))
+    ...     datecreated=next(now),
+    ... )
     Karma added: action=questionreopened, product=firefox, person=name12
 
 
@@ -97,8 +106,10 @@ Requesting for more information
 ...............................
 
     >>> msg = firefox_question.requestInfo(
-    ...     foo_bar, 'What "Quick Searches" do you want?',
-    ...     datecreated=next(now))
+    ...     foo_bar,
+    ...     'What "Quick Searches" do you want?',
+    ...     datecreated=next(now),
+    ... )
     Karma added: action=questionrequestedinfo, product=firefox, person=name16
 
 
@@ -106,8 +117,8 @@ Giving back more information
 ............................
 
     >>> msg = firefox_question.giveInfo(
-    ...     'The same one than shipped upstreams.',
-    ...     datecreated=next(now))
+    ...     "The same one than shipped upstreams.", datecreated=next(now)
+    ... )
     Karma added: action=questiongaveinfo, product=firefox, person=name12
 
 
@@ -115,8 +126,11 @@ Giving an answer to a question
 ..............................
 
     >>> msg = firefox_question.giveAnswer(
-    ...     foo_bar, "Ok, I see what you mean. You need to install them "
-    ...     "manually for now.", datecreated=next(now))
+    ...     foo_bar,
+    ...     "Ok, I see what you mean. You need to install them "
+    ...     "manually for now.",
+    ...     datecreated=next(now),
+    ... )
     Karma added: action=questiongaveanswer, product=firefox, person=name16
 
 
@@ -124,8 +138,10 @@ Adding a comment
 ................
 
     >>> msg = firefox_question.addComment(
-    ...     foo_bar, 'You could also fill a bug about that, if you like.',
-    ...     datecreated=next(now))
+    ...     foo_bar,
+    ...     "You could also fill a bug about that, if you like.",
+    ...     datecreated=next(now),
+    ... )
     Karma added: action=questioncommentadded, product=firefox, person=name16
 
 
@@ -138,7 +154,9 @@ receives karma.
 
     >>> msg = firefox_question.confirmAnswer(
     ...     "Ok, thanks. I'll open a bug about this then.",
-    ...     answer=msg, datecreated=next(now))
+    ...     answer=msg,
+    ...     datecreated=next(now),
+    ... )
     Karma added: action=questionansweraccepted, product=firefox, person=name12
     Karma added: action=questionanswered, product=firefox, person=name16
 
@@ -147,7 +165,8 @@ Rejecting a question
 ....................
 
     >>> msg = firefox_question.reject(
-    ...     foo_bar, 'This should really be a bug report.')
+    ...     foo_bar, "This should really be a bug report."
+    ... )
     Karma added: action=questionrejected, product=firefox, person=name16
 
 
@@ -156,33 +175,40 @@ Changing the status
 
 We do not grant karma for status change made outside of workflow:
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> from lp.answers.enums import QuestionStatus
     >>> msg = firefox_question.setStatus(
-    ...     foo_bar, QuestionStatus.OPEN, 'That rejection was an error.',
-    ...     datecreated=next(now))
+    ...     foo_bar,
+    ...     QuestionStatus.OPEN,
+    ...     "That rejection was an error.",
+    ...     datecreated=next(now),
+    ... )
 
 
 Changing the title of a question
 ................................
 
     >>> from lp.services.webapp.snapshot import notify_modified
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> with notify_modified(firefox_question, ['title']):
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> with notify_modified(firefox_question, ["title"]):
     ...     firefox_question.title = (
     ...         'Firefox 1.5.0.5 does not have any "Quick Searches" '
-    ...         'installed by default')
+    ...         "installed by default"
+    ...     )
+    ...
     Karma added: action=questiontitlechanged, product=firefox, person=name12
 
 
 Changing the description of a question
 ......................................
 
-    >>> with notify_modified(firefox_question, ['description']):
+    >>> with notify_modified(firefox_question, ["description"]):
     ...     firefox_question.description = (
     ...         'Firefox 1.5.0.5 does not have any "Quick Searches" '
-    ...         'installed in the bookmarks by default, like the official '
-    ...         'ones do.')
+    ...         "installed in the bookmarks by default, like the official "
+    ...         "ones do."
+    ...     )
+    ...
     Karma added: action=questiondescriptionchanged, product=firefox,
         person=name12
 
@@ -207,25 +233,29 @@ persons who were awarded it in the past.
     # This test must have no output
 
     >>> msg = firefox_question.giveAnswer(
-    ...     sample_person, "I was able to import some by following the "
+    ...     sample_person,
+    ...     "I was able to import some by following the "
     ...     "instructions on http://tinyurl.com/cyus4";,
-    ...     datecreated=next(now))
+    ...     datecreated=next(now),
+    ... )
 
 
 Creating a FAQ
 ..............
 
     >>> firefox_faq = firefox.newFAQ(
-    ...     sample_person, 'A FAQ', 'About something important')
+    ...     sample_person, "A FAQ", "About something important"
+    ... )
     Karma added: action=faqcreated, product=firefox, person=name12
 
 
 Modifying a FAQ
 ...............
 
-    >>> with notify_modified(firefox_faq, ['title', 'content']):
-    ...     firefox_faq.title = 'How can I make the Fnord appears?'
-    ...     firefox_faq.content = 'Install the Fnords highlighter extensions.'
+    >>> with notify_modified(firefox_faq, ["title", "content"]):
+    ...     firefox_faq.title = "How can I make the Fnord appears?"
+    ...     firefox_faq.content = "Install the Fnords highlighter extensions."
+    ...
     Karma added: action=faqedited, product=firefox, person=name12
 
 
@@ -239,6 +269,7 @@ actions have been tested. Only the obsolete methods remain.
     >>> obsolete_actions = set(answers_karma_actions) - added_karma_actions
     >>> for action in obsolete_actions:
     ...     print(action.title)
+    ...
     Solved own question
 
     # Unregister the event listener to make sure we won't interfere in
diff --git a/lib/lp/answers/doc/notifications.rst b/lib/lp/answers/doc/notifications.rst
index 6a9ea22..de7700f 100644
--- a/lib/lp/answers/doc/notifications.rst
+++ b/lib/lp/answers/doc/notifications.rst
@@ -7,14 +7,17 @@ Let's start with creating a question, and see what the resulting
 notification looks like:
 
     >>> from lp.answers.tests.test_question_notifications import (
-    ...     pop_questionemailjobs)
+    ...     pop_questionemailjobs,
+    ... )
     >>> from lp.registry.interfaces.distribution import IDistributionSet
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> sample_person = getUtility(ILaunchBag).user
-    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
+    >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
     >>> ubuntu_question = ubuntu.newQuestion(
-    ...     sample_person, "Can't install Ubuntu",
-    ...    "I insert the install CD in the CD-ROM drive, but it won't boot.")
+    ...     sample_person,
+    ...     "Can't install Ubuntu",
+    ...     "I insert the install CD in the CD-ROM drive, but it won't boot.",
+    ... )
 
 The notifications get sent to the question's subscribers, the question's
 target answer contacts as well as to the question's assignee. Initially,
@@ -27,6 +30,7 @@ is sent:
 
     >>> for sub in ubuntu_question.subscriptions:
     ...     print(sub.person.displayname)
+    ...
     Sample Person
 
     >>> notifications = pop_questionemailjobs()
@@ -57,7 +61,7 @@ footer the reason why the user is receiving the notification.
 The notification also includes a 'X-Launchpad-Question' header that
 contains information about the question.
 
-    >>> print(add_notification.headers['X-Launchpad-Question'])
+    >>> print(add_notification.headers["X-Launchpad-Question"])
     distribution=ubuntu; sourcepackage=None; status=Open;
     assignee=None; priority=Normal; language=en
 
@@ -66,16 +70,16 @@ notified about the changes as well:
 
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> from lp.services.worlddata.interfaces.language import ILanguageSet
-    >>> ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
+    >>> ubuntu_team = getUtility(IPersonSet).getByName("ubuntu-team")
     >>> login(ubuntu_team.teamowner.preferredemail.email)
-    >>> ubuntu_team.addLanguage(getUtility(ILanguageSet)['en'])
+    >>> ubuntu_team.addLanguage(getUtility(ILanguageSet)["en"])
     >>> ubuntu.addAnswerContact(ubuntu_team, ubuntu_team.teamowner)
     True
 
 And assign this question to Foo Bar, so that they will also receive
 notifications:
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> ubuntu_question.assignee = getUtility(ILaunchBag).user
 
 
@@ -87,16 +91,18 @@ will be sent.
 
     >>> from lp.services.webapp.snapshot import notify_modified
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> with notify_modified(
-    ...         ubuntu_question, ['title', 'description', 'target']):
+    ...     ubuntu_question, ["title", "description", "target"]
+    ... ):
     ...     ubuntu_question.title = "Installer doesn't work on a Mac"
     ...     ubuntu_question.description = (
     ...         "I insert the install CD in the CD-ROM\n"
     ...         "drive, but it won't boot.\n"
     ...         "\n"
-    ...         "It boots straight into MacOS 9.")
-    ...     ubuntu_question.target = ubuntu.getSourcePackage('libstdc++')
+    ...         "It boots straight into MacOS 9."
+    ...     )
+    ...     ubuntu_question.target = ubuntu.getSourcePackage("libstdc++")
 
 Three copies of the notification got sent, one to Sample Person, one to
 Foo Bar, and one to Ubuntu Team:
@@ -126,8 +132,9 @@ status whiteboard, priority. For example, if a question is # transferred
 to another QuestionTarget and priority is changed, # the notification
 does not include priority.
 
-    >>> with notify_modified(ubuntu_question, ['target']):
+    >>> with notify_modified(ubuntu_question, ["target"]):
     ...     ubuntu_question.target = ubuntu
+    ...
     >>> notifications = pop_questionemailjobs()
     >>> edit_notification = notifications[1]
     >>> print(edit_notification.body)
@@ -138,10 +145,11 @@ does not include priority.
 
 Changing the assignee will trigger a notification.
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
-    >>> with notify_modified(ubuntu_question, ['assignee']):
+    >>> login("foo.bar@xxxxxxxxxxxxx")
+    >>> no_priv = getUtility(IPersonSet).getByName("no-priv")
+    >>> with notify_modified(ubuntu_question, ["assignee"]):
     ...     ubuntu_question.assignee = no_priv
+    ...
     >>> notifications = pop_questionemailjobs()
     >>> edit_notification = notifications[1]
     >>> print(edit_notification.body)
@@ -153,8 +161,9 @@ Changing the assignee will trigger a notification.
 If we trigger a modification event when no changes worth notifying about
 was made, no notification is sent:
 
-    >>> with notify_modified(ubuntu_question, ['status']):
+    >>> with notify_modified(ubuntu_question, ["status"]):
     ...     pass
+    ...
 
     >>> notifications = pop_questionemailjobs()
     >>> len(notifications)
@@ -177,13 +186,16 @@ has been linked to it:
 
     >>> from lp.bugs.interfaces.bug import CreateBugParams
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
-    >>> with notify_modified(ubuntu_question, ['bugs']):
+    >>> login("no-priv@xxxxxxxxxxxxx")
+    >>> with notify_modified(ubuntu_question, ["bugs"]):
     ...     params = CreateBugParams(
-    ...         owner=no_priv, title="Installer fails on a Mac PPC",
-    ...         comment=ubuntu_question.description)
+    ...         owner=no_priv,
+    ...         title="Installer fails on a Mac PPC",
+    ...         comment=ubuntu_question.description,
+    ...     )
     ...     bug = ubuntu_question.target.createBug(params)
     ...     ubuntu_question.linkBug(bug)
+    ...
     True
 
     >>> notifications = pop_questionemailjobs()
@@ -205,8 +217,9 @@ Bug Unlinked Notification
 
 A notification is also sent when a bug is unlinked from the question:
 
-    >>> with notify_modified(ubuntu_question, ['bugs']):
+    >>> with notify_modified(ubuntu_question, ["bugs"]):
     ...     ubuntu_question.unlinkBug(bug)
+    ...
     True
 
     >>> notifications = pop_questionemailjobs()
@@ -239,7 +252,8 @@ The content of the notification will be different depending on the
 workflow action done.
 
     >>> request_message = ubuntu_question.requestInfo(
-    ...     no_priv, "What is your Mac model?")
+    ...     no_priv, "What is your Mac model?"
+    ... )
 
     >>> notifications = pop_questionemailjobs()
     >>> support_notification = notifications[1]
@@ -281,9 +295,9 @@ extra footer.
 Of course, if the owner unsubscribes from the question, they won't receive
 a notification.
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> ubuntu_question.unsubscribe(sample_person, sample_person)
-    >>> message = ubuntu_question.giveInfo('A PowerMac 7200.')
+    >>> message = ubuntu_question.giveInfo("A PowerMac 7200.")
 
     >>> notifications = pop_questionemailjobs()
     >>> print(notifications[1].body)
@@ -298,7 +312,7 @@ a notification.
 The notification for new messages on the question contain a 'References'
 header to the previous message for threading purpose.
 
-    >>> references = notifications[0].headers['References']
+    >>> references = notifications[0].headers["References"]
     >>> print(references)
     <...>
 
@@ -318,9 +332,10 @@ giveInfo() transitions, let's see the other ones.
 Notifications for expireQuestion()
 ..................................
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> message = ubuntu_question.expireQuestion(
-    ...     no_priv, "Expired because of no recent activity.")
+    ...     no_priv, "Expired because of no recent activity."
+    ... )
     >>> notifications = pop_questionemailjobs()
 
 Default notification when the question is expired:
@@ -359,17 +374,20 @@ Notifications for reopen()
 (This example will also show that comments are wrapped for 72 columns
 display.)
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> from lp.services.messages.interfaces.message import IMessageSet
     >>> email_msg = getUtility(IMessageSet).fromText(
     ...     subject=(
     ...         "Re: [Question %d]: Installer doesn't work on "
-    ...         "a Mac" % ubuntu_question.id),
+    ...         "a Mac" % ubuntu_question.id
+    ...     ),
     ...     content=(
     ...         "I really need some help. I tried googling a bit but didn't "
     ...         "find anything useful.\n\nPlease provide some help to a "
-    ...         "newbie."),
-    ...     owner=sample_person)
+    ...         "newbie."
+    ...     ),
+    ...     owner=sample_person,
+    ... )
     >>> message = ubuntu_question.reopen(email_msg)
     >>> notifications = pop_questionemailjobs()
 
@@ -410,14 +428,16 @@ Notification received by the owner:
 Notifications for giveAnswer()
 ..............................
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> answer_message = ubuntu_question.giveAnswer(
-    ...     no_priv, "Actually, your model is an OldWorld Mac. It needs "
+    ...     no_priv,
+    ...     "Actually, your model is an OldWorld Mac. It needs "
     ...     "some configuration on the Mac side to boot the installer. You "
     ...     "will need to install BootX and some other files in your System "
     ...     "Folder.\n\nConsult "
     ...     "https://help.ubuntu.com/community/Installation/OldWorldMacs "
-    ...     "for all the details.")
+    ...     "for all the details.",
+    ... )
 
     >>> notifications = pop_questionemailjobs()
 
@@ -466,10 +486,12 @@ Notification received by the owner:
 Notifications for confirm()
 ...........................
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> message = ubuntu_question.confirmAnswer(
     ...     "I've installed BootX and the installer CD is now booting. "
-    ...     "Thanks!", answer=answer_message)
+    ...     "Thanks!",
+    ...     answer=answer_message,
+    ... )
 
     >>> notifications = pop_questionemailjobs()
 
@@ -499,10 +521,12 @@ Notification received by the owner:
 Notifications for addComment()
 ..............................
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> message = ubuntu_question.addComment(
-    ...     no_priv, "Unless you have lots of RAM... and even then, the "
-    ...     "system will probably be very slow.")
+    ...     no_priv,
+    ...     "Unless you have lots of RAM... and even then, the "
+    ...     "system will probably be very slow.",
+    ... )
 
     >>> notifications = pop_questionemailjobs()
 
@@ -530,10 +554,11 @@ Notification received by the owner:
 Notifications for reject()
 ..........................
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> foo_bar = getUtility(ILaunchBag).user
     >>> message = ubuntu_question.reject(
-    ...     foo_bar, "Yeah! It will be awfully slow.")
+    ...     foo_bar, "Yeah! It will be awfully slow."
+    ... )
 
     >>> notifications = pop_questionemailjobs()
 
@@ -571,9 +596,10 @@ Notifications for setStatus()
 .............................
 
     >>> from lp.answers.enums import QuestionStatus
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> message = ubuntu_question.setStatus(
-    ...     foo_bar, QuestionStatus.SOLVED, "The rejection was a mistake.")
+    ...     foo_bar, QuestionStatus.SOLVED, "The rejection was a mistake."
+    ... )
 
     >>> notifications = pop_questionemailjobs()
 
@@ -606,20 +632,22 @@ Notifications for linkFAQ()
 When a user links a FAQ to a question, the notification includes that
 information before the message.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> from lp.registry.interfaces.product import IProductSet
-    >>> firefox = getUtility(IProductSet).getByName('firefox')
+    >>> firefox = getUtility(IProductSet).getByName("firefox")
     >>> firefox_question = firefox.newQuestion(
-    ...     no_priv, 'How can I play Flash?', 'I want Flash!')
+    ...     no_priv, "How can I play Flash?", "I want Flash!"
+    ... )
     >>> ignore = pop_questionemailjobs()
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> firefox_faq = firefox.getFAQ(10)
     >>> print(firefox_faq.title)
     How do I install plugins (Shockwave, QuickTime, etc.)?
 
     >>> message = firefox_question.linkFAQ(
-    ...     sample_person, firefox_faq, "Read the FAQ.")
+    ...     sample_person, firefox_faq, "Read the FAQ."
+    ... )
     >>> notifications = pop_questionemailjobs()
 
     >>> print(notifications[0].body)
@@ -640,7 +668,8 @@ information before the message.
 If the FAQ is unlinked, the notification will look like:
 
     >>> message = firefox_question.linkFAQ(
-    ...     sample_person, None, "Sorry, this wasn't so useful.")
+    ...     sample_person, None, "Sorry, this wasn't so useful."
+    ... )
     >>> notifications = pop_questionemailjobs()
 
     >>> print(notifications[0].body)
@@ -678,30 +707,33 @@ receive notifications related to it.
     # Register salgado as answer contact, this makes the pt_BR language
     # supported in Ubuntu.
 
-    >>> salgado = getUtility(IPersonSet).getByName('salgado')
+    >>> salgado = getUtility(IPersonSet).getByName("salgado")
     >>> ubuntu.addAnswerContact(salgado, salgado)
     True
 
     >>> from operator import attrgetter
     >>> for lang in sorted(
-    ...         ubuntu.getSupportedLanguages(), key=attrgetter('code')):
+    ...     ubuntu.getSupportedLanguages(), key=attrgetter("code")
+    ... ):
     ...     print(lang.code)
     en
     pt_BR
 
     >>> from lp.services.worlddata.interfaces.language import ILanguageSet
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> pt_BR_question = ubuntu.newQuestion(
-    ...     sample_person, title=(
-    ...     u"Abrir uma p\xe1gina que requer java quebra o firefox"),
+    ...     sample_person,
+    ...     title=("Abrir uma p\xe1gina que requer java quebra o firefox"),
     ...     description=(
-    ...         u'Eu uso Ubuntu em um AMD64 e instalei o plugin java '
-    ...         u'blackdown. O plugin \xe9 exibido em about:plugins e '
-    ...         u'quando eu abro a pagina '
-    ...         u'http://java.com/en/download/help/testvm.xml, ela carrega '
-    ...         u'corretamente e mostra a minha versao do java. No entanto, '
-    ...         u'mover o mouse na pagina faz com que o firefox quebre.'),
-    ...     language=getUtility(ILanguageSet)['pt_BR'])
+    ...         "Eu uso Ubuntu em um AMD64 e instalei o plugin java "
+    ...         "blackdown. O plugin \xe9 exibido em about:plugins e "
+    ...         "quando eu abro a pagina "
+    ...         "http://java.com/en/download/help/testvm.xml, ela carrega "
+    ...         "corretamente e mostra a minha versao do java. No entanto, "
+    ...         "mover o mouse na pagina faz com que o firefox quebre."
+    ...     ),
+    ...     language=getUtility(ILanguageSet)["pt_BR"],
+    ... )
     >>> notifications = pop_questionemailjobs()
 
     >>> print(backslashreplace(notifications[0].subject))
@@ -712,7 +744,8 @@ status changed, only the subscribers speaking that language will receive
 the notifications.
 
     >>> pt_BR_question.giveInfo(
-    ...     "Veja o screenshot: http://tinyurl.com/y8jq8z";)
+    ...     "Veja o screenshot: http://tinyurl.com/y8jq8z";
+    ... )
     <lp.answers.model.questionmessage.QuestionMessage...>
 
     >>> ignore = pop_questionemailjobs()
@@ -726,11 +759,13 @@ For example, the French language is not spoken by any Ubuntu answer
 contacts. So after posting a question in French, a notification will be
 sent to the support list about that question:
 
-    >>> french = getUtility(ILanguageSet)['fr']
+    >>> french = getUtility(ILanguageSet)["fr"]
     >>> french_question = ubuntu.newQuestion(
-    ...     sample_person, title="Impossible d'installer Ubuntu",
-    ...     description=u"Le CD ne semble pas fonctionn\xe9.",
-    ...     language=french)
+    ...     sample_person,
+    ...     title="Impossible d'installer Ubuntu",
+    ...     description="Le CD ne semble pas fonctionn\xe9.",
+    ...     language=french,
+    ... )
     >>> notifications = pop_questionemailjobs()
 
     >>> print(notifications[1].subject)
@@ -742,6 +777,7 @@ sent to the support list about that question:
 
     >>> def recode_text(notification):
     ...     return backslashreplace(notification.body)
+    ...
 
     >>> notification_body = recode_text(notifications[1])
     >>> print(notification_body)
@@ -772,8 +808,9 @@ No notification will be sent to the answer contacts when this question
 is modified. Only the owner will receive a modification notification
 with a warning appended to it.
 
-    >>> with notify_modified(french_question, ['title']):
-    ...     french_question.title = u"CD d'Ubuntu ne d\xe9marre pas"
+    >>> with notify_modified(french_question, ["title"]):
+    ...     french_question.title = "CD d'Ubuntu ne d\xe9marre pas"
+    ...
     >>> notifications = pop_questionemailjobs()
 
     >>> notification_body = recode_text(notifications[0])
diff --git a/lib/lp/answers/doc/person.rst b/lib/lp/answers/doc/person.rst
index d908b02..3045fc4 100644
--- a/lib/lp/answers/doc/person.rst
+++ b/lib/lp/answers/doc/person.rst
@@ -17,7 +17,7 @@ various search criteria.
     >>> from lp.answers.interfaces.questionsperson import IQuestionsPerson
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> personset = getUtility(IPersonSet)
-    >>> foo_bar_raw = personset.getByEmail('foo.bar@xxxxxxxxxxxxx')
+    >>> foo_bar_raw = personset.getByEmail("foo.bar@xxxxxxxxxxxxx")
     >>> foo_bar = IQuestionsPerson(foo_bar_raw)
 
 
@@ -27,8 +27,9 @@ Search text
 The search_text parameter will limit the questions to those matching
 the query using the regular full text algorithm.
 
-    >>> for question in foo_bar.searchQuestions(search_text=u'firefox'):
+    >>> for question in foo_bar.searchQuestions(search_text="firefox"):
     ...     print(question.title, question.status.title)
+    ...
     Firefox loses focus and gets stuck              Open
     mailto: problem in webpage                      Solved
     Newly installed plug-in doesn't seem to be used Answered
@@ -43,7 +44,8 @@ the constant defined in the QuestionSort enumeration.
 
     >>> from lp.answers.enums import QuestionSort
     >>> for question in foo_bar.searchQuestions(
-    ...     search_text=u'firefox', sort=QuestionSort.OLDEST_FIRST):
+    ...     search_text="firefox", sort=QuestionSort.OLDEST_FIRST
+    ... ):
     ...     print(question.id, question.title, question.status.title)
     4 Firefox loses focus and gets stuck              Open
     6 Newly installed plug-in doesn't seem to be used Answered
@@ -53,6 +55,7 @@ When no text search is done, the default sort order is newest first.
 
     >>> for question in foo_bar.searchQuestions():
     ...     print(question.id, question.title, question.status.title)
+    ...
     11 Continue playing after shutdown                      Open
     10 Play DVDs in Totem                                   Answered
      9 mailto: problem in webpage                           Solved
@@ -70,14 +73,16 @@ parameter can be used to control the list of statuses to select.
 
     >>> from lp.answers.enums import QuestionStatus
     >>> for question in foo_bar.searchQuestions(
-    ...         status=QuestionStatus.INVALID):
+    ...     status=QuestionStatus.INVALID
+    ... ):
     ...     print(question.title, question.status.title)
     Firefox is slow and consumes too much RAM   Invalid
 
 The status parameter can also take a list of statuses.
 
     >>> for question in foo_bar.searchQuestions(
-    ...         status=(QuestionStatus.SOLVED, QuestionStatus.INVALID)):
+    ...     status=(QuestionStatus.SOLVED, QuestionStatus.INVALID)
+    ... ):
     ...     print(question.title, question.status.title)
     mailto: problem in webpage                  Solved
     Firefox is slow and consumes too much RAM   Invalid
@@ -95,7 +100,8 @@ QuestionParticipation.COMMENTER is used.
 
     >>> from lp.answers.enums import QuestionParticipation
     >>> for question in foo_bar.searchQuestions(
-    ...         participation=QuestionParticipation.COMMENTER, status=None):
+    ...     participation=QuestionParticipation.COMMENTER, status=None
+    ... ):
     ...     print(question.title)
     Continue playing after shutdown
     Play DVDs in Totem
@@ -107,7 +113,8 @@ QuestionParticipation.SUBSCRIBER will only select the questions to which the
 person is subscribed.
 
     >>> for question in foo_bar.searchQuestions(
-    ...         participation=QuestionParticipation.SUBSCRIBER, status=None):
+    ...     participation=QuestionParticipation.SUBSCRIBER, status=None
+    ... ):
     ...     print(question.title)
     Slow system
     Firefox is slow and consumes too much RAM
@@ -115,7 +122,8 @@ person is subscribed.
 QuestionParticipation.OWNER selects the questions that the person created.
 
     >>> for question in foo_bar.searchQuestions(
-    ...         participation=QuestionParticipation.OWNER, status=None):
+    ...     participation=QuestionParticipation.OWNER, status=None
+    ... ):
     ...     print(question.title)
     Slow system
     Firefox loses focus and gets stuck
@@ -125,7 +133,8 @@ QuestionParticipation.ANSWERER selects the questions for which the person gave
 an answer.
 
     >>> for question in foo_bar.searchQuestions(
-    ...         participation=QuestionParticipation.ANSWERER, status=None):
+    ...     participation=QuestionParticipation.ANSWERER, status=None
+    ... ):
     ...     print(question.title)
     mailto: problem in webpage
     Firefox is slow and consumes too much RAM
@@ -133,17 +142,23 @@ an answer.
 QuestionParticipation.ASSIGNEE selects that questions which are assigned to
 the person.
 
-    >>> list(foo_bar.searchQuestions(
-    ...         participation=QuestionParticipation.ASSIGNEE, status=None))
+    >>> list(
+    ...     foo_bar.searchQuestions(
+    ...         participation=QuestionParticipation.ASSIGNEE, status=None
+    ...     )
+    ... )
     []
 
 If a list of these constants is used, all of these participation types
 will be selected.
 
     >>> for question in foo_bar.searchQuestions(
-    ...         participation=(QuestionParticipation.OWNER,
-    ...                        QuestionParticipation.ANSWERER),
-    ...         status=None):
+    ...     participation=(
+    ...         QuestionParticipation.OWNER,
+    ...         QuestionParticipation.ANSWERER,
+    ...     ),
+    ...     status=None,
+    ... ):
     ...     print(question.title)
     mailto: problem in webpage
     Slow system
@@ -159,8 +174,8 @@ possible to filter questions by the language they were written in.  One or a
 sequence of ILanguage object can be passed in to specify the language filter.
 
     >>> from lp.services.worlddata.interfaces.language import ILanguageSet
-    >>> spanish = getUtility(ILanguageSet)['es']
-    >>> english = getUtility(ILanguageSet)['en']
+    >>> spanish = getUtility(ILanguageSet)["es"]
+    >>> english = getUtility(ILanguageSet)["en"]
 
 Foo bar doesn't have any questions written in Spanish.
 
@@ -170,11 +185,11 @@ Foo bar doesn't have any questions written in Spanish.
 But Carlos has one.
 
     # Because not everyone uses a real editor <wink>
-    >>> carlos_raw = personset.getByName('carlos')
+    >>> carlos_raw = personset.getByName("carlos")
     >>> carlos = IQuestionsPerson(carlos_raw)
-    >>> for question in carlos.searchQuestions(
-    ...         language=(english, spanish)):
+    >>> for question in carlos.searchQuestions(language=(english, spanish)):
     ...     print(question.title, question.language.code)
+    ...
     Problema al recompilar kernel con soporte smp (doble-núcleo)    es
 
 
@@ -187,8 +202,12 @@ also includes questions on which the person requested more information or gave
 an answer and are back in the OPEN state.
 
     >>> for question in foo_bar.searchQuestions(needs_attention=True):
-    ...     print(question.status.title, question.owner.displayname,
-    ...           question.title)
+    ...     print(
+    ...         question.status.title,
+    ...         question.owner.displayname,
+    ...         question.title,
+    ...     )
+    ...
     Open              Sample Person Continue playing after shutdown
     Needs information Foo Bar       Slow system
 
@@ -199,9 +218,10 @@ Search combinations
 The results are the intersection of the sets delimited by each criteria.
 
     >>> for question in foo_bar.searchQuestions(
-    ...         search_text=u'firefox OR Java',
-    ...         status=QuestionStatus.ANSWERED,
-    ...         participation=QuestionParticipation.COMMENTER):
+    ...     search_text="firefox OR Java",
+    ...     status=QuestionStatus.ANSWERED,
+    ...     participation=QuestionParticipation.COMMENTER,
+    ... ):
     ...     print(question.title, question.status.title)
     Installation of Java Runtime Environment for Mozilla    Answered
     Newly installed plug-in doesn't seem to be used         Answered
@@ -214,9 +234,14 @@ IQuestionsPerson also defines a getQuestionLanguages() attribute which
 contains the set of languages used by all of the questions in which this
 person is involved.
 
-    >>> print(', '.join(
-    ...     sorted(language.code
-    ...            for language in foo_bar.getQuestionLanguages())))
+    >>> print(
+    ...     ", ".join(
+    ...         sorted(
+    ...             language.code
+    ...             for language in foo_bar.getQuestionLanguages()
+    ...         )
+    ...     )
+    ... )
     en
 
 This includes questions which the person owns, and questions that the user is
@@ -224,44 +249,64 @@ subscribed to...
 
     >>> from lp.answers.interfaces.questioncollection import IQuestionSet
     >>> pt_BR_question = getUtility(IQuestionSet).get(13)
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> pt_BR_question.subscribe(foo_bar_raw)
     <lp.answers.model.questionsubscription.QuestionSubscription...>
 
-    >>> print(', '.join(
-    ...     sorted(language.code
-    ...            for language in foo_bar.getQuestionLanguages())))
+    >>> print(
+    ...     ", ".join(
+    ...         sorted(
+    ...             language.code
+    ...             for language in foo_bar.getQuestionLanguages()
+    ...         )
+    ...     )
+    ... )
     en, pt_BR
 
 ...and questions for which they're the answerer...
 
     >>> es_question = getUtility(IQuestionSet).get(12)
-    >>> es_question.reject(foo_bar_raw, 'Reject question.')
+    >>> es_question.reject(foo_bar_raw, "Reject question.")
     <lp.answers.model.questionmessage.QuestionMessage...>
 
-    >>> print(', '.join(
-    ...     sorted(language.code
-    ...            for language in foo_bar.getQuestionLanguages())))
+    >>> print(
+    ...     ", ".join(
+    ...         sorted(
+    ...             language.code
+    ...             for language in foo_bar.getQuestionLanguages()
+    ...         )
+    ...     )
+    ... )
     en, es, pt_BR
 
 ...as well as questions which are assigned to the user...
 
     >>> pt_BR_question.assignee = carlos_raw
-    >>> print(', '.join(
-    ...     sorted(language.code
-    ...            for language in carlos.getQuestionLanguages())))
+    >>> print(
+    ...     ", ".join(
+    ...         sorted(
+    ...             language.code
+    ...             for language in carlos.getQuestionLanguages()
+    ...         )
+    ...     )
+    ... )
     es, pt_BR
 
 ...and questions on which the user commented.
 
     >>> en_question = getUtility(IQuestionSet).get(1)
-    >>> login('carlos@xxxxxxxxxxxxx')
-    >>> en_question.addComment(carlos_raw, 'A simple comment.')
+    >>> login("carlos@xxxxxxxxxxxxx")
+    >>> en_question.addComment(carlos_raw, "A simple comment.")
     <lp.answers.model.questionmessage.QuestionMessage...>
 
-    >>> print(', '.join(
-    ...     sorted(language.code
-    ...            for language in carlos.getQuestionLanguages())))
+    >>> print(
+    ...     ", ".join(
+    ...         sorted(
+    ...             language.code
+    ...             for language in carlos.getQuestionLanguages()
+    ...         )
+    ...     )
+    ... )
     en, es, pt_BR
 
 
@@ -272,7 +317,7 @@ IQuestionsPerson defines getDirectAnswerQuestionTargets that can be used to
 retrieve a list of IQuestionTargets that a person subscribed themselves to
 as an answer contact.
 
-    >>> no_priv_raw = personset.getByName('no-priv')
+    >>> no_priv_raw = personset.getByName("no-priv")
     >>> no_priv = IQuestionsPerson(no_priv_raw)
     >>> no_priv.getDirectAnswerQuestionTargets()
     []
@@ -286,7 +331,8 @@ as an answer contact.
     True
 
     >>> for target in no_priv.getDirectAnswerQuestionTargets():
-    ...    print(target.name)
+    ...     print(target.name)
+    ...
     firefox
 
 
@@ -308,9 +354,14 @@ contact through their team membership.
     >>> ubuntu.addAnswerContact(landscape_team, landscape_team.teamowner)
     True
 
-    >>> print(', '.join(
-    ...     sorted(target.name
-    ...            for target in no_priv.getTeamAnswerQuestionTargets())))
+    >>> print(
+    ...     ", ".join(
+    ...         sorted(
+    ...             target.name
+    ...             for target in no_priv.getTeamAnswerQuestionTargets()
+    ...         )
+    ...     )
+    ... )
     ubuntu
 
 Indirect team membership is also taken in consideration.  For example, when
@@ -318,7 +369,7 @@ the Landscape Team joins the Translator Team, targets for which the Translator
 team is an answer contact will be included in No Privileges Person's supported
 IQuestionTargets.
 
-    >>> translator_team = personset.getByName('ubuntu-translators')
+    >>> translator_team = personset.getByName("ubuntu-translators")
     >>> no_priv_raw.inTeam(translator_team)
     False
     >>> ignored = translator_team.addMember(landscape_team, carlos_raw)
@@ -327,19 +378,26 @@ IQuestionTargets.
     # order to make landscape_team an actual member of translator_team.
     >>> login(landscape_team.teamowner.preferredemail.email)
     >>> landscape_team.acceptInvitationToBeMemberOf(
-    ...     translator_team, comment='something')
+    ...     translator_team, comment="something"
+    ... )
 
     >>> no_priv_raw.hasParticipationEntryFor(translator_team)
     True
-    >>> evolution_package = ubuntu.getSourcePackage('evolution')
-    >>> login('carlos@xxxxxxxx')
+    >>> evolution_package = ubuntu.getSourcePackage("evolution")
+    >>> login("carlos@xxxxxxxx")
     >>> translator_team.addLanguage(english)
     >>> evolution_package.addAnswerContact(
-    ...     translator_team, translator_team.teamowner)
+    ...     translator_team, translator_team.teamowner
+    ... )
     True
-    >>> print(', '.join(
-    ...     sorted(target.name
-    ...            for target in no_priv.getTeamAnswerQuestionTargets())))
+    >>> print(
+    ...     ", ".join(
+    ...         sorted(
+    ...             target.name
+    ...             for target in no_priv.getTeamAnswerQuestionTargets()
+    ...         )
+    ...     )
+    ... )
     evolution, ubuntu
 
 
@@ -352,14 +410,15 @@ are in the results.
 If the Firefox project is deactivated, it is removed from the list of
 supported projects.
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
 
     # Unlink the source packages so the project can be deactivated.
     >>> from lp.testing import unlink_source_packages
     >>> unlink_source_packages(firefox)
     >>> firefox.active = False
-    >>> sorted(target.name
-    ...        for target in no_priv.getDirectAnswerQuestionTargets())
+    >>> sorted(
+    ...     target.name for target in no_priv.getDirectAnswerQuestionTargets()
+    ... )
     []
 
 When the Firefox project is reactivated, the answer contact relationship is
@@ -367,7 +426,12 @@ visible.  These relationships are persistent for cases where we only want is
 deactivated for a short period.
 
     >>> firefox.active = True
-    >>> print(', '.join(
-    ...     sorted(target.name
-    ...            for target in no_priv.getDirectAnswerQuestionTargets())))
+    >>> print(
+    ...     ", ".join(
+    ...         sorted(
+    ...             target.name
+    ...             for target in no_priv.getDirectAnswerQuestionTargets()
+    ...         )
+    ...     )
+    ... )
     firefox
diff --git a/lib/lp/answers/doc/projectgroup.rst b/lib/lp/answers/doc/projectgroup.rst
index 1099d3d..2c7d44a 100644
--- a/lib/lp/answers/doc/projectgroup.rst
+++ b/lib/lp/answers/doc/projectgroup.rst
@@ -9,9 +9,11 @@ ISearchableByQuestionOwner interfaces.
     >>> from lp.testing import verifyObject
     >>> from lp.registry.interfaces.projectgroup import IProjectGroupSet
     >>> from lp.answers.interfaces.questioncollection import (
-    ...     ISearchableByQuestionOwner, IQuestionCollection)
+    ...     ISearchableByQuestionOwner,
+    ...     IQuestionCollection,
+    ... )
 
-    >>> mozilla_project = getUtility(IProjectGroupSet).getByName('mozilla')
+    >>> mozilla_project = getUtility(IProjectGroupSet).getByName("mozilla")
     >>> verifyObject(IQuestionCollection, mozilla_project)
     True
     >>> verifyObject(ISearchableByQuestionOwner, mozilla_project)
@@ -27,34 +29,37 @@ project group's searchQuestions() method.
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> from lp.registry.interfaces.product import IProductSet
 
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> thunderbird = getUtility(IProductSet).getByName('thunderbird')
-    >>> sample_person = getUtility(IPersonSet).getByName('name12')
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> thunderbird = getUtility(IProductSet).getByName("thunderbird")
+    >>> sample_person = getUtility(IPersonSet).getByName("name12")
     >>> question = thunderbird.newQuestion(
     ...     sample_person,
     ...     "SVG attachments aren't displayed ",
     ...     "It would be a nice feature if SVG attachments could be displayed"
-    ...     " inlined.")
+    ...     " inlined.",
+    ... )
 
-    >>> for question in mozilla_project.searchQuestions(search_text=u'svg'):
+    >>> for question in mozilla_project.searchQuestions(search_text="svg"):
     ...     print(question.title, question.target.displayname)
+    ...
     SVG attachments aren't displayed            Mozilla Thunderbird
     Problem showing the SVG demo on W3C site    Mozilla Firefox
 
 In the case where a project group has no projects, there are no results.
 
-    >>> aaa_project = getUtility(IProjectGroupSet).getByName('aaa')
+    >>> aaa_project = getUtility(IProjectGroupSet).getByName("aaa")
     >>> list(aaa_project.searchQuestions())
     []
 
 Questions can be searched by all the standard searchQuestions() parameters.
 See questiontarget.rst for the full details.
 
-    >>> from lp.answers.enums import (
-    ...     QuestionSort, QuestionStatus)
+    >>> from lp.answers.enums import QuestionSort, QuestionStatus
     >>> for question in mozilla_project.searchQuestions(
-    ...     owner=sample_person, status=QuestionStatus.OPEN,
-    ...     sort=QuestionSort.OLDEST_FIRST):
+    ...     owner=sample_person,
+    ...     status=QuestionStatus.OPEN,
+    ...     sort=QuestionSort.OLDEST_FIRST,
+    ... ):
     ...     print(question.title, question.target.displayname)
     Problem showing the SVG demo on W3C site    Mozilla Firefox
     SVG attachments aren't displayed            Mozilla Thunderbird
@@ -68,9 +73,14 @@ questions in the project group's projects.
 
     # The Firefox project group has one question created in Brazilian
     # Portuguese.
-    >>> print(', '.join(
-    ...     sorted(language.code
-    ...            for language in mozilla_project.getQuestionLanguages())))
+    >>> print(
+    ...     ", ".join(
+    ...         sorted(
+    ...             language.code
+    ...             for language in mozilla_project.getQuestionLanguages()
+    ...         )
+    ...     )
+    ... )
     en, pt_BR
 
 In the case where a project group has no projects, there are no results.
diff --git a/lib/lp/answers/doc/question.rst b/lib/lp/answers/doc/question.rst
index a84f7f1..fe69882 100644
--- a/lib/lp/answers/doc/question.rst
+++ b/lib/lp/answers/doc/question.rst
@@ -8,7 +8,7 @@ Questions are created and accessed using the IQuestionTarget interface.  This
 interface is available on Products, Distributions and
 DistributionSourcePackages.
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
 
     >>> from lp.testing import verifyObject
     >>> from lp.answers.interfaces.questiontarget import IQuestionTarget
@@ -16,14 +16,14 @@ DistributionSourcePackages.
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> from lp.registry.interfaces.product import IProductSet
 
-    >>> firefox = getUtility(IProductSet)['firefox']
+    >>> firefox = getUtility(IProductSet)["firefox"]
     >>> verifyObject(IQuestionTarget, firefox)
     True
-    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
+    >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
     >>> verifyObject(IQuestionTarget, ubuntu)
     True
 
-    >>> evolution_in_ubuntu = ubuntu.getSourcePackage('evolution')
+    >>> evolution_in_ubuntu = ubuntu.getSourcePackage("evolution")
     >>> verifyObject(IQuestionTarget, evolution_in_ubuntu)
     True
 
@@ -31,7 +31,7 @@ Although distribution series do not implement the IQuestionTarget interface,
 it is possible to adapt one to it.  The adapter is actually the distroseries's
 distribution.
 
-    >>> ubuntu_warty = ubuntu.getSeries('warty')
+    >>> ubuntu_warty = ubuntu.getSeries("warty")
     >>> IQuestionTarget.providedBy(ubuntu_warty)
     False
     >>> questiontarget = IQuestionTarget(ubuntu_warty)
@@ -41,7 +41,8 @@ distribution.
 SourcePackages are can be adapted to QuestionTargets.
 
     >>> evolution_in_hoary = ubuntu.currentseries.getSourcePackage(
-    ...     'evolution')
+    ...     "evolution"
+    ... )
     >>> questiontarget = IQuestionTarget(evolution_in_hoary)
     >>> verifyObject(IQuestionTarget, questiontarget)
     True
@@ -50,9 +51,11 @@ You create a new question by calling the newQuestion() method of an
 IQuestionTarget attribute.
 
     >>> sample_person = getUtility(IPersonSet).getByEmail(
-    ...     'test@xxxxxxxxxxxxx')
+    ...     "test@xxxxxxxxxxxxx"
+    ... )
     >>> firefox_question = firefox.newQuestion(
-    ...     sample_person, "Firefox question", "Unable to use Firefox")
+    ...     sample_person, "Firefox question", "Unable to use Firefox"
+    ... )
 
 The complete IQuestionTarget interface is documented in questiontarget.rst.
 
@@ -93,10 +96,12 @@ subscribers.
 
     >>> from operator import attrgetter
     >>> def print_subscribers(question):
-    ...     people = [subscription.person
-    ...               for subscription in question.subscriptions]
-    ...     for person in sorted(people, key=attrgetter('name')):
+    ...     people = [
+    ...         subscription.person for subscription in question.subscriptions
+    ...     ]
+    ...     for person in sorted(people, key=attrgetter("name")):
     ...         print(person.displayname)
+    ...
     >>> print_subscribers(firefox_question)
     Sample Person
 
@@ -126,7 +131,7 @@ It is also possible to adapt a question to its IQuestionTarget.
 
 The question can be assigned to a new IQuestionTarget.
 
-    >>> thunderbird = getUtility(IProductSet)['thunderbird']
+    >>> thunderbird = getUtility(IProductSet)["thunderbird"]
     >>> firefox_question.target = thunderbird
     >>> print(firefox_question.target.displayname)
     Mozilla Thunderbird
@@ -172,7 +177,7 @@ Whenever a question is created or changed, email notifications will be
 sent.  To receive such notification, one can subscribe to the bug using
 the subscribe() method.
 
-    >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
+    >>> no_priv = getUtility(IPersonSet).getByName("no-priv")
     >>> subscription = firefox_question.subscribe(no_priv)
 
 The subscribers include the owner and the newly subscribed person.
@@ -187,6 +192,7 @@ direct_recipients method.
 
     >>> for person in firefox_question.getDirectSubscribers():
     ...     print(person.displayname)
+    ...
     No Privileges Person
     Sample Person
 
@@ -215,6 +221,7 @@ subscribers along with the rationale for contacting them.
     ...         reason, header = subscribers.getReason(person)
     ...         text = removeSecurityProxy(reason).getReason()
     ...         print(header, person.displayname, text)
+    ...
     >>> print_reason(subscribers)
     Asker Sample Person
     You received this question notification because you asked the question.
@@ -229,8 +236,8 @@ part of the indirect subscribers list.
     []
 
     >>> from lp.services.worlddata.interfaces.language import ILanguageSet
-    >>> english = getUtility(ILanguageSet)['en']
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> english = getUtility(ILanguageSet)["en"]
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> no_priv.addLanguage(english)
     >>> firefox.addAnswerContact(no_priv, no_priv)
     True
@@ -255,7 +262,7 @@ are part of the indirect subscribers list.
     []
     >>> list(evolution_in_ubuntu.answer_contacts)
     []
-    >>> ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
+    >>> ubuntu_team = getUtility(IPersonSet).getByName("ubuntu-team")
     >>> login(ubuntu_team.teamowner.preferredemail.email)
     >>> ubuntu_team.addLanguage(english)
     >>> ubuntu.addAnswerContact(ubuntu_team, ubuntu_team.teamowner)
@@ -263,8 +270,10 @@ are part of the indirect subscribers list.
     >>> evolution_in_ubuntu.addAnswerContact(no_priv, no_priv)
     True
     >>> package_question = evolution_in_ubuntu.newQuestion(
-    ...     sample_person, 'Upgrading to Evolution 1.4 breaks plug-ins',
-    ...     "The FnordsHighlighter plug-in doesn't work after upgrade.")
+    ...     sample_person,
+    ...     "Upgrading to Evolution 1.4 breaks plug-ins",
+    ...     "The FnordsHighlighter plug-in doesn't work after upgrade.",
+    ... )
 
     >>> print_subscribers(package_question)
     Sample Person
@@ -273,6 +282,7 @@ are part of the indirect subscribers list.
     >>> indirect_subscribers = package_question.indirect_recipients
     >>> for person in indirect_subscribers:
     ...     print(person.displayname)
+    ...
     No Privileges Person
     Ubuntu Team
 
@@ -284,18 +294,20 @@ are part of the indirect subscribers list.
 
 The question's assignee is also part of the indirect subscription list:
 
-    >>> login('admin@xxxxxxxxxxxxx')
-    >>> package_question.assignee = getUtility(IPersonSet).getByName('name16')
+    >>> login("admin@xxxxxxxxxxxxx")
+    >>> package_question.assignee = getUtility(IPersonSet).getByName("name16")
     >>> del get_property_cache(package_question).indirect_recipients
     >>> indirect_subscribers = package_question.indirect_recipients
     >>> for person in indirect_subscribers:
     ...     print(person.displayname)
+    ...
     Foo Bar
     No Privileges Person
     Ubuntu Team
 
     >>> reason, header = indirect_subscribers.getReason(
-    ...     package_question.assignee)
+    ...     package_question.assignee
+    ... )
     >>> print(header, removeSecurityProxy(reason).getReason())
     Assignee
     You received this question notification because you are assigned to this
@@ -308,6 +320,7 @@ It too contains the question assignee.
     >>> indirect_subscribers = package_question.getIndirectSubscribers()
     >>> for person in indirect_subscribers:
     ...     print(person.displayname)
+    ...
     Foo Bar
     No Privileges Person
     Ubuntu Team
@@ -316,12 +329,13 @@ Notifications are sent to the list of direct and indirect subscribers.  The
 notification recipients list can be obtained by using the getRecipients()
 method.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> subscribers = firefox_question.getRecipients()
     >>> verifyObject(INotificationRecipientSet, subscribers)
     True
     >>> for person in subscribers:
     ...     print(person.displayname)
+    ...
     No Privileges Person
     Sample Person
 
@@ -349,20 +363,24 @@ that are not supported.
 
     >>> unsupported_questions = firefox.searchQuestions(unsupported=True)
     >>> for question in sorted(
-    ...         unsupported_questions, key=attrgetter('title')):
+    ...     unsupported_questions, key=attrgetter("title")
+    ... ):
     ...     print(question.title)
     Problemas de Impressão no Firefox
 
     >>> unsupported_questions = evolution_in_ubuntu.searchQuestions(
-    ...     unsupported=True)
+    ...     unsupported=True
+    ... )
     >>> sorted(question.title for question in unsupported_questions)
     []
 
     >>> warty_question_target = IQuestionTarget(ubuntu_warty)
     >>> unsupported_questions = warty_question_target.searchQuestions(
-    ...     unsupported=True)
+    ...     unsupported=True
+    ... )
     >>> for question in sorted(
-    ...         unsupported_questions, key=attrgetter('title')):
+    ...     unsupported_questions, key=attrgetter("title")
+    ... ):
     ...     print(question.title)
     Problema al recompilar kernel con soporte smp (doble-núcleo)
     عكس التغييرات غير المحفوظة للمستن؟
diff --git a/lib/lp/answers/doc/questionsets.rst b/lib/lp/answers/doc/questionsets.rst
index 8afcd0f..79e43ad 100644
--- a/lib/lp/answers/doc/questionsets.rst
+++ b/lib/lp/answers/doc/questionsets.rst
@@ -48,8 +48,9 @@ The search_text parameter will return questions matching the query using the
 regular full text algorithm.
 
     # Because not everyone uses a real editor <wink>
-    >>> for question in question_set.searchQuestions(search_text=u'firefox'):
+    >>> for question in question_set.searchQuestions(search_text="firefox"):
     ...     print(question.title, question.target.displayname)
+    ...
     Problemas de Impressão no Firefox                Mozilla Firefox
     Firefox loses focus and gets stuck               Mozilla Firefox
     Firefox cannot render Bank Site                  Mozilla Firefox
@@ -68,17 +69,25 @@ in.
 
     >>> from lp.answers.enums import QuestionStatus
     >>> for question in question_set.searchQuestions(
-    ...         status=QuestionStatus.INVALID):
-    ...     print(question.title, question.status.title,
-    ...           question.target.displayname)
+    ...     status=QuestionStatus.INVALID
+    ... ):
+    ...     print(
+    ...         question.title,
+    ...         question.status.title,
+    ...         question.target.displayname,
+    ...     )
     Firefox is slow and consumes too ...   Invalid mozilla-firefox in Ubuntu
 
 The status parameter can also take a list of statuses.
 
     >>> for question in question_set.searchQuestions(
-    ...         status=[QuestionStatus.SOLVED, QuestionStatus.INVALID]):
-    ...     print(question.title, question.status.title,
-    ...           question.target.displayname)
+    ...     status=[QuestionStatus.SOLVED, QuestionStatus.INVALID]
+    ... ):
+    ...     print(
+    ...         question.title,
+    ...         question.status.title,
+    ...         question.target.displayname,
+    ...     )
     mailto: problem in webpage             Solved mozilla-firefox in Ubuntu
     Firefox is slow and consumes too ...   Invalid mozilla-firefox in Ubuntu
 
@@ -90,9 +99,10 @@ The language parameter can be used to select only questions written in a
 particular language.
 
     >>> from lp.services.worlddata.interfaces.language import ILanguageSet
-    >>> spanish = getUtility(ILanguageSet)['es']
+    >>> spanish = getUtility(ILanguageSet)["es"]
     >>> for t in question_set.searchQuestions(language=spanish):
     ...     print(t.title)
+    ...
     Problema al recompilar kernel con soporte smp (doble-núcleo)
 
 
@@ -103,10 +113,14 @@ The returned set of questions is the intersection of the sets delimited by
 each criteria.
 
     >>> for question in question_set.searchQuestions(
-    ...     search_text=u'firefox',
-    ...     status=(QuestionStatus.OPEN, QuestionStatus.INVALID)):
-    ...     print(question.title, question.status.title,
-    ...           question.target.displayname)
+    ...     search_text="firefox",
+    ...     status=(QuestionStatus.OPEN, QuestionStatus.INVALID),
+    ... ):
+    ...     print(
+    ...         question.title,
+    ...         question.status.title,
+    ...         question.target.displayname,
+    ...     )
     Problemas de Impressão no Firefox Open           Mozilla Firefox
     Firefox is slow and consumes too much ...        mozilla-firefox in Ubuntu
     Firefox loses focus and gets stuck Open          Mozilla Firefox
@@ -124,7 +138,8 @@ of the constant defined in the QuestionSort enumeration.
 
     >>> from lp.answers.enums import QuestionSort
     >>> for question in question_set.searchQuestions(
-    ...     search_text=u'firefox', sort=QuestionSort.OLDEST_FIRST):
+    ...     search_text="firefox", sort=QuestionSort.OLDEST_FIRST
+    ... ):
     ...     print(question.id, question.title, question.target.displayname)
     14 عكس التغييرات غير المحفوظة للمستن؟               Ubuntu
     1 Firefox cannot render Bank Site                   Mozilla Firefox
@@ -137,7 +152,8 @@ of the constant defined in the QuestionSort enumeration.
 When no text search is done, the default sort order is by newest first.
 
     >>> for question in question_set.searchQuestions(
-    ...         status=QuestionStatus.OPEN)[:5]:
+    ...     status=QuestionStatus.OPEN
+    ... )[:5]:
     ...     print(question.id, question.title, question.target.displayname)
     13 Problemas de Impressão no Firefox                Mozilla Firefox
     12 Problema al recompilar kernel con soporte smp (doble-núcleo) Ubuntu
@@ -152,9 +168,14 @@ Question languages
 The getQuestionLanguages() method returns the set of languages in which
 questions are written in launchpad.
 
-    >>> print(', '.join(
-    ...     sorted(language.code
-    ...            for language in question_set.getQuestionLanguages())))
+    >>> print(
+    ...     ", ".join(
+    ...         sorted(
+    ...             language.code
+    ...             for language in question_set.getQuestionLanguages()
+    ...         )
+    ...     )
+    ... )
     ar, en, es, pt_BR
 
 
@@ -171,10 +192,10 @@ we'll set those up.
     >>> from lp.app.enums import ServiceUsage
     >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> from lp.registry.interfaces.product import IProductSet
-    >>> firefox = getUtility(IProductSet).getByName('firefox')
-    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
+    >>> firefox = getUtility(IProductSet).getByName("firefox")
+    >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
     >>> inactive = factory.makeProduct()
-    >>> login('admin@xxxxxxxxxxxxx')
+    >>> login("admin@xxxxxxxxxxxxx")
     >>> firefox.answers_usage = ServiceUsage.LAUNCHPAD
     >>> ubuntu.answers_usage = ServiceUsage.LAUNCHPAD
     >>> inactive.answers_usage = ServiceUsage.LAUNCHPAD
@@ -197,27 +218,32 @@ Then some recent questions are created on a number of projects.
     >>> from lp.answers.testing import QuestionFactory
     >>> from lp.registry.interfaces.person import IPersonSet
 
-    >>> firefox = getUtility(IProductSet).getByName('firefox')
-    >>> landscape = getUtility(IProductSet).getByName('landscape')
-    >>> launchpad = getUtility(IProductSet).getByName('launchpad')
-    >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
-    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
-
-    >>> login('no-priv@xxxxxxxxxxxxx')
-    >>> QuestionFactory.createManyByProject((
-    ...     ('ubuntu', 3),
-    ...     ('firefox', 2),
-    ...     ('landscape', 1),
-    ...     (inactive.name, 5),
-    ...     ))
+    >>> firefox = getUtility(IProductSet).getByName("firefox")
+    >>> landscape = getUtility(IProductSet).getByName("landscape")
+    >>> launchpad = getUtility(IProductSet).getByName("launchpad")
+    >>> no_priv = getUtility(IPersonSet).getByName("no-priv")
+    >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
+
+    >>> login("no-priv@xxxxxxxxxxxxx")
+    >>> QuestionFactory.createManyByProject(
+    ...     (
+    ...         ("ubuntu", 3),
+    ...         ("firefox", 2),
+    ...         ("landscape", 1),
+    ...         (inactive.name, 5),
+    ...     )
+    ... )
 
 A question is created just before the time limit on Launchpad.
 
     >>> from datetime import datetime, timedelta
     >>> from pytz import UTC
     >>> question = launchpad.newQuestion(
-    ...     no_priv, 'Launchpad question', 'A question',
-    ...     datecreated=datetime.now(UTC) - timedelta(days=61))
+    ...     no_priv,
+    ...     "Launchpad question",
+    ...     "A question",
+    ...     datecreated=datetime.now(UTC) - timedelta(days=61),
+    ... )
     >>> login(ANONYMOUS)
 
 The method returns only projects which officially use the Answer Tracker.  The
@@ -237,6 +263,7 @@ during the period.
     # the last 60 days.  Inactive projects are not returned either.
     >>> for project in question_set.getMostActiveProjects():
     ...     print(project.displayname)
+    ...
     Ubuntu
     Mozilla Firefox
 
@@ -246,6 +273,7 @@ project returned:
 
     >>> for project in question_set.getMostActiveProjects(limit=1):
     ...     print(project.displayname)
+    ...
     Ubuntu
 
 
@@ -260,30 +288,37 @@ on a set of IDistributionSourcePackage packages.
 
 It returns the number of open questions for each given package.
 
-    >>> ubuntu_evolution = ubuntu.getSourcePackage('evolution')
-    >>> ubuntu_pmount = ubuntu.getSourcePackage('pmount')
-    >>> debian = getUtility(IDistributionSet).getByName('debian')
-    >>> debian_evolution = debian.getSourcePackage('evolution')
-    >>> debian_pmount = debian.getSourcePackage('pmount')
+    >>> ubuntu_evolution = ubuntu.getSourcePackage("evolution")
+    >>> ubuntu_pmount = ubuntu.getSourcePackage("pmount")
+    >>> debian = getUtility(IDistributionSet).getByName("debian")
+    >>> debian_evolution = debian.getSourcePackage("evolution")
+    >>> debian_pmount = debian.getSourcePackage("pmount")
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> QuestionFactory.createManyByTarget(ubuntu_pmount, 4)
     [...]
     >>> QuestionFactory.createManyByTarget(debian_evolution, 3)
     [...]
     >>> open_question, closed_question = QuestionFactory.createManyByTarget(
-    ...     ubuntu_evolution, 2)
+    ...     ubuntu_evolution, 2
+    ... )
     >>> closed_question.setStatus(
-    ...     closed_question.owner, QuestionStatus.SOLVED, 'no comment')
+    ...     closed_question.owner, QuestionStatus.SOLVED, "no comment"
+    ... )
     <lp.answers.model.questionmessage.QuestionMessage ...>
 
     >>> packages = (
-    ...     ubuntu_evolution, ubuntu_pmount, debian_evolution, debian_pmount)
+    ...     ubuntu_evolution,
+    ...     ubuntu_pmount,
+    ...     debian_evolution,
+    ...     debian_pmount,
+    ... )
     >>> package_counts = question_set.getOpenQuestionCountByPackages(packages)
     >>> len(packages)
     4
     >>> for package in packages:
     ...     print("%s: %s" % (package.bugtargetname, package_counts[package]))
+    ...
     evolution (Ubuntu): 1
     pmount (Ubuntu): 4
     evolution (Debian): 3
diff --git a/lib/lp/answers/doc/questiontarget.rst b/lib/lp/answers/doc/questiontarget.rst
index f5d819c..7004d2e 100644
--- a/lib/lp/answers/doc/questiontarget.rst
+++ b/lib/lp/answers/doc/questiontarget.rst
@@ -13,7 +13,7 @@ implementing IQuestionTarget.
     #
     # Some parts of the IQuestionTarget interface are only accessible
     # to a registered user.
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
 
     >>> from zope.component import getUtility
     >>> from zope.interface.verify import verifyObject
@@ -30,7 +30,8 @@ Questions are always owned by a registered user.
 
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> sample_person = getUtility(IPersonSet).getByEmail(
-    ...     'test@xxxxxxxxxxxxx')
+    ...     "test@xxxxxxxxxxxxx"
+    ... )
 
 The newQuestion() method is used to create a question that will be associated
 with the target.  It takes as parameters the question's owner, title and
@@ -42,8 +43,12 @@ to UTC_NOW.
     >>> from pytz import UTC
     >>> now = datetime.now(UTC)
 
-    >>> question = target.newQuestion(sample_person, 'New question',
-    ...     'Question description', datecreated=now)
+    >>> question = target.newQuestion(
+    ...     sample_person,
+    ...     "New question",
+    ...     "Question description",
+    ...     datecreated=now,
+    ... )
     >>> print(question.title)
     New question
     >>> print(question.description)
@@ -63,6 +68,7 @@ subscribed to the question.
 
     >>> for subscription in question.subscriptions:
     ...     print(subscription.person.displayname)
+    ...
     Sample Person
 
 Questions can be written in any languages supported in Launchpad.  The
@@ -76,11 +82,14 @@ It is possible to create questions in another language than English, by
 passing in the language that the question is written in.
 
     >>> from lp.services.worlddata.interfaces.language import ILanguageSet
-    >>> french = getUtility(ILanguageSet)['fr']
+    >>> french = getUtility(ILanguageSet)["fr"]
     >>> question = target.newQuestion(
-    ...     sample_person, "De l'aide S.V.P.",
-    ...     "Pouvez-vous m'aider?", language=french,
-    ...     datecreated=now + timedelta(seconds=30))
+    ...     sample_person,
+    ...     "De l'aide S.V.P.",
+    ...     "Pouvez-vous m'aider?",
+    ...     language=french,
+    ...     datecreated=now + timedelta(seconds=30),
+    ... )
     >>> print(question.language.code)
     fr
 
@@ -88,7 +97,8 @@ Anonymous users cannot use newQuestion().
 
     >>> login(ANONYMOUS)
     >>> question = target.newQuestion(
-    ...     sample_person, 'This will fail', 'Failed?')
+    ...     sample_person, "This will fail", "Failed?"
+    ... )
     Traceback (most recent call last):
       ...
     zope.security.interfaces.Unauthorized: ...
@@ -117,40 +127,55 @@ Searching for questions
 
     # Create new questions for the following tests.  Odd questions will be
     # owned by Foo Bar and even questions will be owned by Sample Person.
-    >>> login('no-priv@xxxxxxxxxxxxx')
-    >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
+    >>> foo_bar = getUtility(IPersonSet).getByEmail("foo.bar@xxxxxxxxxxxxx")
     >>> questions = []
     >>> for num in range(5):
     ...     if num % 2:
     ...         owner = foo_bar
     ...     else:
     ...         owner = sample_person
-    ...     description = ('Support request description%d.\n'
-    ...         'This request index is %d.') % (num, num)
-    ...     questions.append(target.newQuestion(
-    ...         owner, 'Question title%d' % num, description,
-    ...         datecreated=now+timedelta(minutes=num+1)))
+    ...     description = (
+    ...         "Support request description%d.\n" "This request index is %d."
+    ...     ) % (num, num)
+    ...     questions.append(
+    ...         target.newQuestion(
+    ...             owner,
+    ...             "Question title%d" % num,
+    ...             description,
+    ...             datecreated=now + timedelta(minutes=num + 1),
+    ...         )
+    ...     )
+    ...
 
     # For more variety, we will set the status of the last to INVALID and the
     # fourth one to ANSWERED.
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
+    >>> foo_bar = getUtility(IPersonSet).getByEmail("foo.bar@xxxxxxxxxxxxx")
     >>> message = questions[-1].reject(
-    ...     foo_bar, 'Invalid question.', datecreated=now+timedelta(hours=1))
+    ...     foo_bar, "Invalid question.", datecreated=now + timedelta(hours=1)
+    ... )
     >>> message = questions[3].giveAnswer(
-    ...     sample_person, 'This is your answer.',
-    ...     datecreated=now+timedelta(hours=1))
+    ...     sample_person,
+    ...     "This is your answer.",
+    ...     datecreated=now + timedelta(hours=1),
+    ... )
 
     # Also add a reply from the owner on the first of these.
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> message = questions[0].giveInfo(
-    ...     'I think I forgot something.', datecreated=now+timedelta(hours=4))
+    ...     "I think I forgot something.",
+    ...     datecreated=now + timedelta(hours=4),
+    ... )
 
     # Create another one that will also have the word 'new' in its
     # description.
-    >>> question = target.newQuestion(sample_person, 'Another question',
-    ...     'Another new question that is actually very new.',
-    ...     datecreated=now+timedelta(hours=1))
+    >>> question = target.newQuestion(
+    ...     sample_person,
+    ...     "Another question",
+    ...     "Another new question that is actually very new.",
+    ...     datecreated=now + timedelta(hours=1),
+    ... )
     >>> login(ANONYMOUS)
 
 The searchQuestions() method is used to search for questions.
@@ -163,8 +188,9 @@ The search_text parameter will select the questions that contain the
 passed in text.  The standard text searching algorithm is used; see
 lib/lp/services/database/doc/textsearching.rst.
 
-    >>> for t in target.searchQuestions(search_text=u'new'):
+    >>> for t in target.searchQuestions(search_text="new"):
     ...     print(t.title)
+    ...
     New question
     Another question
 
@@ -181,6 +207,7 @@ The searchQuestions() method can also filter questions by status.
     >>> from lp.answers.enums import QuestionStatus
     >>> for t in target.searchQuestions(status=QuestionStatus.OPEN):
     ...     print(t.title)
+    ...
     Another question
     Question title2
     Question title1
@@ -193,6 +220,7 @@ default sort order is from newest to oldest.
 
     >>> for t in target.searchQuestions(status=QuestionStatus.INVALID):
     ...     print(t.title)
+    ...
     Question title4
 
 You can pass in a list of statuses, and you can also use the search_text and
@@ -200,8 +228,9 @@ status parameters at the same time.  This will search OPEN and INVALID
 questions with the word 'index'.
 
     >>> for t in target.searchQuestions(
-    ...     search_text=u'request index',
-    ...     status=(QuestionStatus.OPEN, QuestionStatus.INVALID)):
+    ...     search_text="request index",
+    ...     status=(QuestionStatus.OPEN, QuestionStatus.INVALID),
+    ... ):
     ...     print(t.title)
     Question title4
     Question title2
@@ -218,8 +247,9 @@ QuestionSort.  Previously, we saw the NEWEST_FIRST and RELEVANCY sort order.
 You can sort also from oldest to newest using the OLDEST_FIRST constant.
 
     >>> from lp.answers.enums import QuestionSort
-    >>> for t in target.searchQuestions(search_text='new',
-    ...                                 sort=QuestionSort.OLDEST_FIRST):
+    >>> for t in target.searchQuestions(
+    ...     search_text="new", sort=QuestionSort.OLDEST_FIRST
+    ... ):
     ...     print(t.title)
     New question
     Another question
@@ -229,9 +259,9 @@ EXPIRED, INVALID).  This also sorts from newest to oldest as a secondary key.
 Here we use status=None to search for all statuses; by default INVALID and
 EXPIRED questions are excluded.
 
-    >>> for t in target.searchQuestions(search_text='request index',
-    ...                                 status=None,
-    ...                                 sort=QuestionSort.STATUS):
+    >>> for t in target.searchQuestions(
+    ...     search_text="request index", status=None, sort=QuestionSort.STATUS
+    ... ):
     ...     print(t.status.title, t.title)
     Open Question title2
     Open Question title1
@@ -246,6 +276,7 @@ the questions will be sorted NEWEST_FIRST.
     # its status.
     >>> for t in target.searchQuestions(sort=QuestionSort.RELEVANCY):
     ...     print(t.title)
+    ...
     Another question
     Question title3
     Question title2
@@ -261,7 +292,8 @@ datelastquery attribute.
     # Question title0 sorts first because it has a message from its owner
     # after the others were created.
     >>> for t in target.searchQuestions(
-    ...                             sort=QuestionSort.RECENT_OWNER_ACTIVITY):
+    ...     sort=QuestionSort.RECENT_OWNER_ACTIVITY
+    ... ):
     ...     print(t.title)
     Question title0
     Another question
@@ -279,6 +311,7 @@ You can find question owned by a particular user by using the owner parameter.
 
     >>> for t in target.searchQuestions(owner=foo_bar):
     ...     print(t.title)
+    ...
     Question title3
     Question title1
 
@@ -289,13 +322,15 @@ Language
 The language criteria can be used to select only questions written in a
 particular language.
 
-    >>> english = getUtility(ILanguageSet)['en']
+    >>> english = getUtility(ILanguageSet)["en"]
     >>> for t in target.searchQuestions(language=french):
     ...     print(t.title)
+    ...
     De l'aide S.V.P.
 
     >>> for t in target.searchQuestions(language=(english, french)):
     ...     print(t.title)
+    ...
     Another question
     Question title3
     Question title2
@@ -314,23 +349,29 @@ state.  Questions on which the user gave an answer or requested for more
 information, and that are back in the OPEN state, are also included.
 
     # One of Sample Person's question gets to need attention from Foo Bar.
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> message = questions[0].requestInfo(
-    ...     foo_bar, 'Do you have a clue?',
-    ...     datecreated=now+timedelta(hours=1))
+    ...     foo_bar,
+    ...     "Do you have a clue?",
+    ...     datecreated=now + timedelta(hours=1),
+    ... )
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> message = questions[0].giveInfo(
-    ...     'I do, now please help me.', datecreated=now+timedelta(hours=2))
+    ...     "I do, now please help me.", datecreated=now + timedelta(hours=2)
+    ... )
 
     # Another one of Foo Bar's questions needs attention.
     >>> message = questions[1].requestInfo(
-    ...     sample_person, 'And you, do you have a clue?',
-    ...     datecreated=now+timedelta(hours=1))
+    ...     sample_person,
+    ...     "And you, do you have a clue?",
+    ...     datecreated=now + timedelta(hours=1),
+    ... )
 
     >>> login(ANONYMOUS)
     >>> for t in target.searchQuestions(needs_attention_from=foo_bar):
     ...     print(t.status.title, t.title, t.owner.displayname)
+    ...
     Answered Question title3 Foo Bar
     Needs information Question title1 Foo Bar
     Open Question title0 Sample Person
@@ -344,6 +385,7 @@ language that is not spoken by any of the Support Contacts.
 
     >>> for t in target.searchQuestions(unsupported=True):
     ...     print(t.title)
+    ...
     De l'aide S.V.P.
 
 
@@ -356,8 +398,9 @@ target text.  The questions don't have to contain all the words of the text.
     # This returns the same results as with the search 'new' because
     # all other words in the text are either common ('question', 'title') or
     # stop words ('with', 'a').
-    >>> for t in target.findSimilarQuestions('new questions with a title'):
+    >>> for t in target.findSimilarQuestions("new questions with a title"):
     ...     print(t.title)
+    ...
     New question
     Another question
 
@@ -385,7 +428,7 @@ contacts defined in the current IQuestionTarget context.
 You add an answer contact by using the addAnswerContact() method.  This
 is only available to registered users.
 
-    >>> name18 = getUtility(IPersonSet).getByName('name18')
+    >>> name18 = getUtility(IPersonSet).getByName("name18")
     >>> target.addAnswerContact(name18, name18)
     Traceback (most recent call last):
       ...
@@ -394,7 +437,7 @@ is only available to registered users.
 This method returns True when the contact was added the list and False when it
 was already on the list.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> target.addAnswerContact(name18, name18)
     True
     >>> people = [p.name for p in target.answer_contacts]
@@ -413,7 +456,7 @@ was already on the list.
 An answer contact must have at least one language among their preferred
 languages.
 
-    >>> sample_person = getUtility(IPersonSet).getByName('name12')
+    >>> sample_person = getUtility(IPersonSet).getByName("name12")
     >>> len(sample_person.languages)
     0
     >>> target.addAnswerContact(sample_person, sample_person)
@@ -460,10 +503,11 @@ support when there are no answer contacts.
     en
 
     # Let's add some answer contacts which speak different languages.
-    >>> login('carlos@xxxxxxxxxxxxx')
-    >>> carlos = getUtility(IPersonSet).getByName('carlos')
+    >>> login("carlos@xxxxxxxxxxxxx")
+    >>> carlos = getUtility(IPersonSet).getByName("carlos")
     >>> for language in carlos.languages:
     ...     print(language.code)
+    ...
     ca
     en
     es
@@ -472,10 +516,11 @@ support when there are no answer contacts.
 
 While daf has en_GB as one of his preferred languages...
 
-    >>> login('daf@xxxxxxxxxxxxx')
-    >>> daf = getUtility(IPersonSet).getByName('daf')
+    >>> login("daf@xxxxxxxxxxxxx")
+    >>> daf = getUtility(IPersonSet).getByName("daf")
     >>> for language in daf.languages:
     ...     print(language.code)
+    ...
     en_GB
     ja
     cy
@@ -486,10 +531,14 @@ While daf has en_GB as one of his preferred languages...
 English variants are converted to English.
 
     >>> from operator import attrgetter
-    >>> print(', '.join(
-    ...     language.code
-    ...     for language in sorted(target.getSupportedLanguages(),
-    ...                            key=attrgetter('code'))))
+    >>> print(
+    ...     ", ".join(
+    ...         language.code
+    ...         for language in sorted(
+    ...             target.getSupportedLanguages(), key=attrgetter("code")
+    ...         )
+    ...     )
+    ... )
     ca, cy, en, es, ja
 
 
@@ -500,10 +549,11 @@ getAnswerContactsForLanguage() method returns a list of answer contacts who
 support the specified language in their preferred languages.  Daf is in the
 list because he speaks an English variant, which is treated as English.
 
-    >>> spanish = getUtility(ILanguageSet)['es']
+    >>> spanish = getUtility(ILanguageSet)["es"]
     >>> answer_contacts = target.getAnswerContactsForLanguage(spanish)
     >>> for person in answer_contacts:
     ...     print(person.name)
+    ...
     carlos
 
     >>> answer_contacts = target.getAnswerContactsForLanguage(english)
@@ -519,9 +569,14 @@ A question's languages
 The getQuestionLanguages() method returns the set of languages used by all
 of the target's questions.
 
-    >>> print(', '.join(
-    ...     sorted(language.code
-    ...            for language in target.getQuestionLanguages())))
+    >>> print(
+    ...     ", ".join(
+    ...         sorted(
+    ...             language.code
+    ...             for language in target.getQuestionLanguages()
+    ...         )
+    ...     )
+    ... )
     en, fr
 
 
@@ -539,14 +594,18 @@ copied to the question.
     >>> from pytz import UTC
 
     >>> now = datetime.now(UTC)
-    >>> target = getUtility(IProductSet)['jokosher']
+    >>> target = getUtility(IProductSet)["jokosher"]
     >>> bug_params = CreateBugParams(
-    ...     title="Print is broken", comment="blah blah blah",
-    ...     owner=sample_person)
+    ...     title="Print is broken",
+    ...     comment="blah blah blah",
+    ...     owner=sample_person,
+    ... )
     >>> target_bug = target.createBug(bug_params)
     >>> bug_message = target_bug.newMessage(
-    ...     owner=sample_person, subject="Oops, my mistake",
-    ...     content="This is really a question.")
+    ...     owner=sample_person,
+    ...     subject="Oops, my mistake",
+    ...     content="This is really a question.",
+    ... )
 
     >>> target_question = target.createQuestionFromBug(target_bug)
 
@@ -562,6 +621,7 @@ copied to the question.
 
     >>> for bug in target_question.bugs:
     ...     print(bug.title)
+    ...
     Print is broken
     >>> print(target_question.messages[-1].text_contents)
     This is really a question.
diff --git a/lib/lp/answers/doc/workflow.rst b/lib/lp/answers/doc/workflow.rst
index 7763dcd..b5ce3af 100644
--- a/lib/lp/answers/doc/workflow.rst
+++ b/lib/lp/answers/doc/workflow.rst
@@ -8,6 +8,7 @@ question's lifecycle.  These are defined in the QuestionStatus enumeration.
     >>> from lp.answers.enums import QuestionStatus
     >>> for status in QuestionStatus.items:
     ...     print(status.name)
+    ...
     OPEN
     NEEDSINFO
     ANSWERED
@@ -21,6 +22,7 @@ actions are defined in the QuestionAction enumeration.
     >>> from lp.answers.enums import QuestionAction
     >>> for status in QuestionAction.items:
     ...     print(status.name)
+    ...
     REQUESTINFO
     GIVEINFO
     COMMENT
@@ -38,21 +40,21 @@ answer contact for the Ubuntu distribution.  Marilize Coetze is another user
 providing support.  Stub is a Launchpad administrator that isn't also in the
 Ubuntu Team owning the distribution.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
 
     >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> from lp.services.worlddata.interfaces.language import ILanguageSet
 
     >>> personset = getUtility(IPersonSet)
-    >>> sample_person = personset.getByEmail('test@xxxxxxxxxxxxx')
-    >>> no_priv = personset.getByEmail('no-priv@xxxxxxxxxxxxx')
-    >>> marilize = personset.getByEmail('marilize@xxxxxxx')
-    >>> stub = personset.getByName('stub')
-
-    >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
-    >>> english = getUtility(ILanguageSet)['en']
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> sample_person = personset.getByEmail("test@xxxxxxxxxxxxx")
+    >>> no_priv = personset.getByEmail("no-priv@xxxxxxxxxxxxx")
+    >>> marilize = personset.getByEmail("marilize@xxxxxxx")
+    >>> stub = personset.getByName("stub")
+
+    >>> ubuntu = getUtility(IDistributionSet)["ubuntu"]
+    >>> english = getUtility(ILanguageSet)["en"]
+    >>> login("test@xxxxxxxxxxxxx")
     >>> sample_person.addLanguage(english)
     >>> ubuntu.addAnswerContact(sample_person, sample_person)
     True
@@ -68,11 +70,11 @@ A question starts its lifecycle in the Open state.
     >>> now = datetime.now(UTC)
     >>> new_question_args = dict(
     ...     owner=no_priv,
-    ...     title='Unable to boot installer',
+    ...     title="Unable to boot installer",
     ...     description="I've tried installing Ubuntu on a Mac. "
-    ...                 "But the installer never boots.",
+    ...     "But the installer never boots.",
     ...     datecreated=now,
-    ...     )
+    ... )
     >>> question = ubuntu.newQuestion(**new_question_args)
     >>> print(question.status.title)
     Open
@@ -95,8 +97,10 @@ date of the question (which defaults to 'now').
     >>> question = ubuntu.newQuestion(**new_question_args)
     >>> now_plus_one_hour = now + timedelta(hours=1)
     >>> request_message = question.requestInfo(
-    ...     sample_person, 'What is your Mac model?',
-    ...     datecreated=now_plus_one_hour)
+    ...     sample_person,
+    ...     "What is your Mac model?",
+    ...     datecreated=now_plus_one_hour,
+    ... )
 
 We now have the IQuestionMessage that was added to the question messages
 history.
@@ -139,10 +143,11 @@ updated to the message's timestamp.
 The question owner can reply to this information by using the giveInfo()
 method which adds an IQuestionMessage with action GIVEINFO.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> now_plus_two_hours = now + timedelta(hours=2)
     >>> reply_message = question.giveInfo(
-    ...     "I have a PowerMac 7200.", datecreated=now_plus_two_hours)
+    ...     "I have a PowerMac 7200.", datecreated=now_plus_two_hours
+    ... )
 
     >>> print(reply_message.action.name)
     GIVEINFO
@@ -166,7 +171,7 @@ The giveAnswer() method is used for that purpose.  Like the requestInfo()
 method, it takes two mandatory parameters: the user providing the answer and
 the answer itself.
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> now_plus_three_hours = now + timedelta(hours=3)
     >>> answer_message = question.giveAnswer(
     ...     sample_person,
@@ -174,7 +179,8 @@ the answer itself.
     ...     "to boot the installer on that model. Consult "
     ...     "https://help.ubuntu.com/community/Installation/OldWorldMacs "
     ...     "for all the details.",
-    ...     datecreated=now_plus_three_hours)
+    ...     datecreated=now_plus_three_hours,
+    ... )
     >>> print(answer_message.action.name)
     ANSWER
     >>> print(answer_message.new_status.name)
@@ -192,13 +198,14 @@ At that point, the question is considered answered, but we don't have
 feedback from the user on whether it solved their problem or not.  If it
 doesn't, the user can reopen the question.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> tomorrow = now + timedelta(days=1)
     >>> reopen_message = question.reopen(
     ...     "I installed BootX and I've progressed somewhat. I now get the "
     ...     "boot screen. But soon after the Ubuntu progress bar appears, I "
     ...     "get a OOM Killer message appearing on the screen.",
-    ...      datecreated=tomorrow)
+    ...     datecreated=tomorrow,
+    ... )
     >>> print(reopen_message.action.name)
     REOPEN
     >>> print(reopen_message.new_status.name)
@@ -216,13 +223,14 @@ updated to the message's creation date.
 
 Once again, an answer is given.
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> tomorrow_plus_one_hour = tomorrow + timedelta(hours=1)
     >>> answer2_message = question.giveAnswer(
     ...     marilize,
     ...     "You probably do not have enough RAM to use the "
     ...     "graphical installer. You can try the alternate CD with the "
-    ...     "text installer.")
+    ...     "text installer.",
+    ... )
 
 The question is moved back to the ANSWERED state.
 
@@ -233,12 +241,14 @@ The question owner will hopefully come back to confirm that their problem is
 solved.  They can specify which answer message helped them solve their
 problem.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> two_weeks_from_now = now + timedelta(days=14)
     >>> confirm_message = question.confirmAnswer(
     ...     "I upgraded to 512M of RAM (found on eBay) and I've successfully "
     ...     "managed to install Ubuntu. Thanks for all the help.",
-    ...     datecreated=two_weeks_from_now, answer=answer_message)
+    ...     datecreated=two_weeks_from_now,
+    ...     answer=answer_message,
+    ... )
     >>> print(confirm_message.action.name)
     CONFIRM
     >>> print(confirm_message.new_status.name)
@@ -274,18 +284,19 @@ A new question is posed.
 
 The answer provides some useful information to the questioner.
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> tomorrow_plus_one_hour = tomorrow + timedelta(hours=1)
     >>> alt_answer_message = question.giveAnswer(
     ...     marilize,
     ...     "Are you using a pre-G3 Mac? They are very difficult "
     ...     "to install to. You must mess with the hardware to trick "
-    ...     "the core chips to let it install. You may not want to do this.")
+    ...     "the core chips to let it install. You may not want to do this.",
+    ... )
 
 The question owner has researched the problem, and has come to a solution
 themselves.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> self_answer_message = question.giveAnswer(
     ...     no_priv,
     ...     "I found some instructions on the Wiki on how to "
@@ -293,7 +304,8 @@ themselves.
     ...     "https://help.ubuntu.com/community/Installation/OldWorldMacs "
     ...     "This is complicated and since it's a very old machine, not "
     ...     "worth the trouble.",
-    ...     datecreated=now_plus_one_hour)
+    ...     datecreated=now_plus_one_hour,
+    ... )
 
 The question owner is considered to have given information that the problem is
 solved and the question is moved to the SOLVED state.  The 'answerer'
@@ -323,7 +335,8 @@ The question's solution date will be the date of the answer message.
     ...     "Thanks Marilize for your help. I don't think I'll put Ubuntu "
     ...     "Ubuntu on my Mac.",
     ...     datecreated=now_plus_one_hour,
-    ...     answer=alt_answer_message)
+    ...     answer=alt_answer_message,
+    ... )
     >>> print(confirm_message.action.name)
     CONFIRM
     >>> print(confirm_message.new_status.name)
@@ -348,7 +361,7 @@ It is also possible that nobody will answer the question, either because the
 question is too complex or too vague.  These questions are expired by using
 the expireQuestion() method.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> question = ubuntu.newQuestion(**new_question_args)
     >>> expire_message = question.expireQuestion(
     ...     sample_person,
@@ -356,7 +369,8 @@ the expireQuestion() method.
     ...     "weeks and this question was expired. If you are still having "
     ...     "this problem you should reopen the question and provide more "
     ...     "information about your problem.",
-    ...     datecreated=two_weeks_from_now)
+    ...     datecreated=two_weeks_from_now,
+    ... )
     >>> print(expire_message.action.name)
     EXPIRE
     >>> print(expire_message.new_status.name)
@@ -378,7 +392,8 @@ reopened.
     ...     "I'm installing on PowerMac 7200/120 with 32 Megs of RAM. After "
     ...     "I insert the CD and restart the computer, it boots straight "
     ...     "into Mac OS/9 instead of booting the installer.",
-    ...     datecreated=much_later)
+    ...     datecreated=much_later,
+    ... )
     >>> print(reopen_message.action.name)
     REOPEN
 
@@ -398,8 +413,11 @@ In this scenario the user posts an inappropriate message, such as a spam
 message or a request for Ubuntu CDs.
 
     >>> spam_question = ubuntu.newQuestion(
-    ...     no_priv, 'CDs', 'Please send 10 Ubuntu Dapper CDs.',
-    ...     datecreated=now)
+    ...     no_priv,
+    ...     "CDs",
+    ...     "Please send 10 Ubuntu Dapper CDs.",
+    ...     datecreated=now,
+    ... )
 
 Such questions can be rejected by an answer contact, a product or distribution
 owner, or a Launchpad administrator.
@@ -425,18 +443,19 @@ As a Launchpad administrator, so can Stub.
     True
 
     >>> login(marilize.preferredemail.email)
-    >>> spam_question.reject(
-    ...     marilize, "We don't send free CDs any more.")
+    >>> spam_question.reject(marilize, "We don't send free CDs any more.")
     Traceback (most recent call last):
       ...
     zope.security.interfaces.Unauthorized: ...
 
 When rejecting a question, a comment explaining the reason is given.
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> reject_message = spam_question.reject(
-    ...     sample_person, "We don't send free CDs any more.",
-    ...     datecreated=now_plus_one_hour)
+    ...     sample_person,
+    ...     "We don't send free CDs any more.",
+    ...     datecreated=now_plus_one_hour,
+    ... )
     >>> print(reject_message.action.name)
     REJECT
     >>> print(reject_message.new_status.name)
@@ -482,7 +501,7 @@ Changing the question status
 
 It is not possible to change the status attribute directly.
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> question = ubuntu.newQuestion(**new_question_args)
     >>> question.status = QuestionStatus.INVALID
     Traceback (most recent call last):
@@ -496,8 +515,11 @@ explaining the status change.
     >>> old_datelastquery = question.datelastquery
     >>> login(stub.preferredemail.email)
     >>> status_change_message = question.setStatus(
-    ...      stub, QuestionStatus.INVALID, 'Changed status to INVALID',
-    ...     datecreated=now_plus_one_hour)
+    ...     stub,
+    ...     QuestionStatus.INVALID,
+    ...     "Changed status to INVALID",
+    ...     datecreated=now_plus_one_hour,
+    ... )
 
 The method returns the IQuestionMessage recording the change.
 
@@ -518,11 +540,11 @@ The status change updates the last response date.
 If an answer was present on the question, the status change also clears
 the answer and solution date.
 
-    >>> msg = question.setStatus(stub, QuestionStatus.OPEN, 'Status change.')
-    >>> answer_message = question.giveAnswer(sample_person, 'Install BootX.')
+    >>> msg = question.setStatus(stub, QuestionStatus.OPEN, "Status change.")
+    >>> answer_message = question.giveAnswer(sample_person, "Install BootX.")
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
-    >>> msg = question.confirmAnswer('This worked.', answer=answer_message)
+    >>> login("no-priv@xxxxxxxxxxxxx")
+    >>> msg = question.confirmAnswer("This worked.", answer=answer_message)
     >>> question.date_solved is not None
     True
     >>> question.answer == answer_message
@@ -530,8 +552,11 @@ the answer and solution date.
 
     >>> login(stub.preferredemail.email)
     >>> status_change_message = question.setStatus(
-    ...     stub, QuestionStatus.OPEN, 'Reopen the question',
-    ...     datecreated=now_plus_one_hour)
+    ...     stub,
+    ...     QuestionStatus.OPEN,
+    ...     "Reopen the question",
+    ...     datecreated=now_plus_one_hour,
+    ... )
 
     >>> print(question.date_solved)
     None
@@ -541,8 +566,8 @@ the answer and solution date.
 When the status is changed by a user who doesn't have the launchpad.Admin
 permission, an Unauthorized exception is thrown.
 
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> question.setStatus(sample_person, QuestionStatus.EXPIRED, 'Expire.')
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> question.setStatus(sample_person, QuestionStatus.EXPIRED, "Expire.")
     Traceback (most recent call last):
       ...
     zope.security.interfaces.Unauthorized: ...
@@ -553,13 +578,13 @@ Adding Comments Without Changing the Status
 
 Comments can be added to questions without changing the question's status.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> old_status = question.status
     >>> old_datelastresponse = question.datelastresponse
     >>> old_datelastquery = question.datelastquery
     >>> comment = question.addComment(
-    ...     no_priv, 'This is a comment.',
-    ...     datecreated=now_plus_two_hours)
+    ...     no_priv, "This is a comment.", datecreated=now_plus_two_hours
+    ... )
 
     >>> print(comment.action.name)
     COMMENT
@@ -582,14 +607,14 @@ question target owners, and admins, can assign someone to answer a question.
 
 Sample Person is an answer contact for ubuntu, so they can set the assignee.
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> question.assignee = stub
     >>> print(question.assignee.displayname)
     Stuart Bishop
 
 Users without launchpad.Moderator privileges cannot set the assignee.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> question.assignee = sample_person
     Traceback (most recent call last):
       ...
@@ -605,40 +630,50 @@ the message they create and a ObjectModifiedEvent for the question.
 
     # Register an event listener that will print events it receives.
     >>> from lazr.lifecycle.interfaces import (
-    ...     IObjectCreatedEvent, IObjectModifiedEvent)
+    ...     IObjectCreatedEvent,
+    ...     IObjectModifiedEvent,
+    ... )
     >>> from lp.testing.fixture import ZopeEventHandlerFixture
     >>> from lp.answers.interfaces.question import IQuestion
 
     >>> def print_event(object, event):
-    ...     print("Received %s on %s" % (
-    ...         event.__class__.__name__.split('.')[-1],
-    ...         object.__class__.__name__.split('.')[-1]))
+    ...     print(
+    ...         "Received %s on %s"
+    ...         % (
+    ...             event.__class__.__name__.split(".")[-1],
+    ...             object.__class__.__name__.split(".")[-1],
+    ...         )
+    ...     )
+    ...
     >>> questionmessage_event_listener = ZopeEventHandlerFixture(
-    ...     print_event, (IQuestionMessage, IObjectCreatedEvent))
+    ...     print_event, (IQuestionMessage, IObjectCreatedEvent)
+    ... )
     >>> questionmessage_event_listener.setUp()
     >>> question_event_listener = ZopeEventHandlerFixture(
-    ...     print_event, (IQuestion, IObjectModifiedEvent))
+    ...     print_event, (IQuestion, IObjectModifiedEvent)
+    ... )
     >>> question_event_listener.setUp()
 
 Changing the status triggers the event.
 
     >>> login(stub.preferredemail.email)
     >>> msg = question.setStatus(
-    ...     stub, QuestionStatus.EXPIRED, 'Status change.')
+    ...     stub, QuestionStatus.EXPIRED, "Status change."
+    ... )
     Received ObjectCreatedEvent on QuestionMessage
     Received ObjectModifiedEvent on Question
 
 Rejecting the question triggers the events.
 
-    >>> msg = question.reject(stub, 'Close this question.')
+    >>> msg = question.reject(stub, "Close this question.")
     Received ObjectCreatedEvent on QuestionMessage
     Received ObjectModifiedEvent on Question
 
 Even only adding a comment without changing the status will send
 these events.
 
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> msg = question.addComment(sample_person, 'A comment')
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> msg = question.addComment(sample_person, "A comment")
     Received ObjectCreatedEvent on QuestionMessage
     Received ObjectModifiedEvent on Question
 
@@ -657,27 +692,32 @@ is reopened, a QuestionReopening is created.
     # created.
     >>> from lp.answers.interfaces.questionreopening import IQuestionReopening
     >>> reopening_event_listener = ZopeEventHandlerFixture(
-    ...     print_event, (IQuestionReopening, IObjectCreatedEvent))
+    ...     print_event, (IQuestionReopening, IObjectCreatedEvent)
+    ... )
     >>> reopening_event_listener.setUp()
 
 The most common use case is when a user confirms a solution, and then
 comes back to say that it doesn't, in fact, work.
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> question = ubuntu.newQuestion(**new_question_args)
     >>> answer_message = question.giveAnswer(
     ...     sample_person,
     ...     "You need some setup on the Mac side. "
     ...     "Follow the instructions at "
     ...     "https://help.ubuntu.com/community/Installation/OldWorldMacs";,
-    ...     datecreated=now_plus_one_hour)
+    ...     datecreated=now_plus_one_hour,
+    ... )
     >>> confirm_message = question.confirmAnswer(
     ...     "I've installed BootX and the installer now boot properly.",
-    ...     answer=answer_message, datecreated=now_plus_two_hours)
+    ...     answer=answer_message,
+    ...     datecreated=now_plus_two_hours,
+    ... )
     >>> reopen_message = question.reopen(
     ...     "Actually, although the installer boots properly. I'm not able "
     ...     "to pass beyond the partitioning.",
-    ...     datecreated=now_plus_three_hours)
+    ...     datecreated=now_plus_three_hours,
+    ... )
     Received ObjectCreatedEvent on QuestionReopening
 
 The reopening record is available through the reopenings attribute.
@@ -710,18 +750,22 @@ prior status of the question.
 A reopening also occurs when the question status is set back to OPEN after
 having been rejected.
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> question = ubuntu.newQuestion(**new_question_args)
     >>> reject_message = question.reject(
-    ...     sample_person, 'This is a frivoulous question.',
-    ...     datecreated=now_plus_one_hour)
+    ...     sample_person,
+    ...     "This is a frivoulous question.",
+    ...     datecreated=now_plus_one_hour,
+    ... )
 
     >>> login(stub.preferredemail.email)
     >>> status_change_message = question.setStatus(
-    ...     stub, QuestionStatus.OPEN,
-    ...     'Disregard previous rejection. '
-    ...     'Sample Person was having a bad day.',
-    ...     datecreated=now_plus_two_hours)
+    ...     stub,
+    ...     QuestionStatus.OPEN,
+    ...     "Disregard previous rejection. "
+    ...     "Sample Person was having a bad day.",
+    ...     datecreated=now_plus_two_hours,
+    ... )
     Received ObjectCreatedEvent on QuestionReopening
 
     >>> reopening = question.reopenings[0]
@@ -747,11 +791,12 @@ In all the workflow methods, it is possible to pass an IMessage instead of
 a string.
 
     >>> from lp.services.messages.interfaces.message import IMessageSet
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> messageset = getUtility(IMessageSet)
     >>> question = ubuntu.newQuestion(**new_question_args)
     >>> reject_message = messageset.fromText(
-    ...     'Reject', 'Because I feel like it.', sample_person)
+    ...     "Reject", "Because I feel like it.", sample_person
+    ... )
     >>> question_message = question.reject(sample_person, reject_message)
     >>> print(question_message.subject)
     Reject
diff --git a/lib/lp/answers/stories/answer-contact-report.rst b/lib/lp/answers/stories/answer-contact-report.rst
index 5458b9a..3868788 100644
--- a/lib/lp/answers/stories/answer-contact-report.rst
+++ b/lib/lp/answers/stories/answer-contact-report.rst
@@ -5,8 +5,8 @@ To view the answer contact report for a given person, the user chooses
 the 'Answer Contact For' link from the actions portlet while viewing
 the Person's page.
 
-    >>> anon_browser.open('http://answers.launchpad.test/~no-priv')
-    >>> anon_browser.getLink('Answer contact for').click()
+    >>> anon_browser.open("http://answers.launchpad.test/~no-priv";)
+    >>> anon_browser.getLink("Answer contact for").click()
     >>> print(anon_browser.title)
     Projects for which...
 
@@ -14,32 +14,34 @@ Since No Privileges Person is not an answer contact, the report states
 that.
 
     >>> content = find_main_content(anon_browser.contents)
-    >>> print(content.find('p').decode_contents())
+    >>> 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
 they registered for.
 
-    >>> anon_browser.open('http://answers.launchpad.test/~name16')
-    >>> anon_browser.getLink('Answer contact for').click()
+    >>> anon_browser.open("http://answers.launchpad.test/~name16";)
+    >>> anon_browser.getLink("Answer contact for").click()
     >>> print(anon_browser.title)
     Projects for which...
 
     >>> content = find_tag_by_id(
-    ...     anon_browser.contents, "direct-answer-contacts-for-list")
+    ...     anon_browser.contents, "direct-answer-contacts-for-list"
+    ... )
     >>> print(backslashreplace(extract_text(content)))
     gnomebaker
     ...mozilla-firefox... package in Ubuntu
 
     >>> content = find_tag_by_id(
-    ...     anon_browser.contents, "team-answer-contacts-for-list")
+    ...     anon_browser.contents, "team-answer-contacts-for-list"
+    ... )
     >>> print(extract_text(content))
     Gnome Applets
     gnomebaker
 
 Clicking on the name of the project will show the project answers.
 
-    >>> anon_browser.getLink('gnomebaker').click()
+    >>> anon_browser.getLink("gnomebaker").click()
     >>> print(anon_browser.title)
     Questions : gnomebaker
 
@@ -47,15 +49,15 @@ When the user is logged in, and they are visiting this page in their
 profile, they will see a link after each project to manage their
 registration.
 
-    >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
-    >>> browser.open(
-    ...     'http://answers.launchpad.test/~name12')
-    >>> browser.getLink('Answer contact for').click()
+    >>> browser.addHeader("Authorization", "Basic test@xxxxxxxxxxxxx:test")
+    >>> browser.open("http://answers.launchpad.test/~name12";)
+    >>> browser.getLink("Answer contact for").click()
     >>> print(browser.title)
     Projects for which...
 
     >>> content = find_tag_by_id(
-    ... browser.contents, "team-answer-contacts-for-list")
+    ...     browser.contents, "team-answer-contacts-for-list"
+    ... )
     >>> print(extract_text(content))
     Gnome Applets Unsubscribe team
     gnomebaker Unsubscribe team
@@ -67,14 +69,14 @@ registration.
 The Remove yourself/team links only appears in their profile. They cannot
 see the link for other users.
 
-    >>> browser.open(
-    ...     'http://answers.launchpad.test/~name16')
-    >>> browser.getLink('Answer contact for').click()
+    >>> browser.open("http://answers.launchpad.test/~name16";)
+    >>> browser.getLink("Answer contact for").click()
     >>> print(browser.title)
     Projects for which...
 
     >>> content = find_tag_by_id(
-    ...     browser.contents, "direct-answer-contacts-for-list")
+    ...     browser.contents, "direct-answer-contacts-for-list"
+    ... )
     >>> print(backslashreplace(extract_text(content)))
     gnomebaker
     ...mozilla-firefox... package in Ubuntu
diff --git a/lib/lp/answers/stories/distribution-package-answer-contact.rst b/lib/lp/answers/stories/distribution-package-answer-contact.rst
index 2d2ebd8..fc331ba 100644
--- a/lib/lp/answers/stories/distribution-package-answer-contact.rst
+++ b/lib/lp/answers/stories/distribution-package-answer-contact.rst
@@ -11,13 +11,13 @@ contacts as well as the distribution answer contacts.
     >>> from lp.services.webapp.interfaces import ILaunchBag
     >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> from lp.services.worlddata.interfaces.language import ILanguageSet
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
 
 Answer contacts must speak a language.
 
     >>> user = getUtility(ILaunchBag).user
-    >>> user.addLanguage(getUtility(ILanguageSet)['en'])
+    >>> user.addLanguage(getUtility(ILanguageSet)["en"])
     >>> ubuntu.addAnswerContact(user, user)
     True
     >>> flush_database_updates()
@@ -27,26 +27,26 @@ To reflect this, a user visiting a source package 'Answers' facet will
 see both a portlet listing the answer contacts for the source package
 and another one listing those of the distribution.
 
-    >>> anon_browser.open(
-    ...     'http://launchpad.test/ubuntu/+source/evolution')
-    >>> anon_browser.getLink('Answers').click()
+    >>> anon_browser.open("http://launchpad.test/ubuntu/+source/evolution";)
+    >>> anon_browser.getLink("Answers").click()
     >>> portlet = find_portlet(
-    ...     anon_browser.contents,
-    ...     'Answer contacts for evolution in Ubuntu')
+    ...     anon_browser.contents, "Answer contacts for evolution in Ubuntu"
+    ... )
 
 To register themselves as answer contact, the user clicks on the
 'Set answer contact' link. They need to login to access that function.
 
-    >>> anon_browser.getLink('Set answer contact').click()
+    >>> anon_browser.getLink("Set answer contact").click()
     Traceback (most recent call last):
       ...
     zope.security.interfaces.Unauthorized: ...
 
-    >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
+    >>> browser.addHeader("Authorization", "Basic test@xxxxxxxxxxxxx:test")
     >>> browser.open(
-    ...     'http://launchpad.test/ubuntu/+source/evolution/+questions')
-    >>> browser.getLink('Answers').click()
-    >>> browser.getLink('Set answer contact').click()
+    ...     "http://launchpad.test/ubuntu/+source/evolution/+questions";
+    ... )
+    >>> browser.getLink("Answers").click()
+    >>> browser.getLink("Set answer contact").click()
     >>> print(browser.title)
     Answer contact for evolution package...
 
@@ -55,11 +55,12 @@ clicking a checkbox. All the teams they're a member of are also displayed,
 and they can register these teams as well.
 
     >>> browser.getControl(
-    ...     "I want to be an answer contact for evolution").selected
+    ...     "I want to be an answer contact for evolution"
+    ... ).selected
     False
     >>> browser.getControl(
-    ...     "I want to be an answer contact for "
-    ...     "evolution").selected = True
+    ...     "I want to be an answer contact for " "evolution"
+    ... ).selected = True
     >>> browser.getControl("Landscape Developers").selected
     False
     >>> browser.getControl("Landscape Developers").selected = True
@@ -67,9 +68,10 @@ and they can register these teams as well.
 To save their choices, the user clicks on the 'Continue' button and
 a message is displayed to confirm the changes:
 
-    >>> browser.getControl('Continue').click()
-    >>> for message in find_tags_by_class(browser.contents, 'message'):
+    >>> browser.getControl("Continue").click()
+    >>> for message in find_tags_by_class(browser.contents, "message"):
     ...     print(extract_text(message))
+    ...
     You have been added as an answer contact for evolution in Ubuntu.
     English was added to Landscape Developers's preferred languages.
     Landscape Developers has been added as an answer contact for
@@ -78,17 +80,19 @@ a message is displayed to confirm the changes:
 To unregister as answer contact, the same page is used. Instead this
 time, we unselect the checkboxes:
 
-    >>> browser.getLink('Set answer contact').click()
+    >>> browser.getLink("Set answer contact").click()
     >>> browser.getControl(
-    ...     "I want to be an answer contact for evolution").selected
+    ...     "I want to be an answer contact for evolution"
+    ... ).selected
     True
     >>> browser.getControl(
-    ...     "I want to be an answer contact for "
-    ...     "evolution").selected = False
+    ...     "I want to be an answer contact for " "evolution"
+    ... ).selected = False
 
 Again a confirmation message is displayed.
 
-    >>> browser.getControl('Continue').click()
-    >>> for message in find_tags_by_class(browser.contents, 'message'):
+    >>> browser.getControl("Continue").click()
+    >>> for message in find_tags_by_class(browser.contents, "message"):
     ...     print(message.decode_contents())
+    ...
     You have been removed as an answer contact for evolution in Ubuntu.
diff --git a/lib/lp/answers/stories/faq-add.rst b/lib/lp/answers/stories/faq-add.rst
index ae54966..edda09a 100644
--- a/lib/lp/answers/stories/faq-add.rst
+++ b/lib/lp/answers/stories/faq-add.rst
@@ -7,16 +7,15 @@ privileges. This includes answer contacts and the project's owner.
 No Privileges Person is not an answer contact for Mozilla Firefox, nor
 the project owner, therefore they cannot create a new FAQ.
 
-    >>> user_browser.open('http://answers.launchpad.test/firefox')
-    >>> user_browser.getLink('All FAQs').click()
+    >>> user_browser.open("http://answers.launchpad.test/firefox";)
+    >>> user_browser.getLink("All FAQs").click()
 
-    >>> user_browser.getLink('Create a new FAQ')
+    >>> user_browser.getLink("Create a new FAQ")
     Traceback (most recent call last):
       ...
     zope.testbrowser.browser.LinkNotFoundError
 
-    >>> user_browser.open(
-    ...     'http://answers.launchpad.test/firefox/+createfaq')
+    >>> user_browser.open("http://answers.launchpad.test/firefox/+createfaq";)
     Traceback (most recent call last):
       ...
     zope.security.interfaces.Unauthorized: ...
@@ -26,21 +25,24 @@ project owner. They are looking for a FAQ about RSS, but they do not find
 one. They choose to create one because they have answered several questions
 from various sources about the subject.
 
-    >>> owner_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
-    >>> owner_browser.open('http://answers.launchpad.test/firefox')
-    >>> owner_browser.getLink('All FAQs').click()
+    >>> owner_browser = setupBrowser(auth="Basic test@xxxxxxxxxxxxx:test")
+    >>> owner_browser.open("http://answers.launchpad.test/firefox";)
+    >>> owner_browser.getLink("All FAQs").click()
     >>> print(owner_browser.title)
     FAQs for Mozilla Firefox : Questions : Mozilla Firefox
 
-    >>> print(extract_text(
-    ...     find_tag_by_id(owner_browser.contents, 'faqs-listing')))
+    >>> print(
+    ...     extract_text(
+    ...         find_tag_by_id(owner_browser.contents, "faqs-listing")
+    ...     )
+    ... )
     What's the keyboard shortcut for [random feature]?
     How do I install plugins (Shockwave, QuickTime, etc.)?
     How do I troubleshoot problems with extensions/themes?
     How do I install Extensions?
     How do I install Java?
 
-    >>> owner_browser.getLink('Create a new FAQ').click()
+    >>> owner_browser.getLink("Create a new FAQ").click()
 
 The page for creating a new FAQ contains three fields that are empty.
 Sample Person adds a title, keywords, and puts the answer into the
@@ -51,17 +53,17 @@ content field. They then submit the form using the 'Create' button.
     >>> print(owner_browser.title)
     Create a FAQ for...
 
-    >>> owner_browser.getControl('Title').value
+    >>> owner_browser.getControl("Title").value
     ''
-    >>> owner_browser.getControl('Keywords').value
+    >>> owner_browser.getControl("Keywords").value
     ''
-    >>> owner_browser.getControl('Content').value
+    >>> owner_browser.getControl("Content").value
     ''
 
-    >>> owner_browser.getControl('Title').value = 'How do I read RSS?'
-    >>> owner_browser.getControl('Keywords').value = 'RSS, bookmark'
-    >>> owner_browser.getControl('Content').value = 'Use the bookmark bar.'
-    >>> owner_browser.getControl('Create').click()
+    >>> owner_browser.getControl("Title").value = "How do I read RSS?"
+    >>> owner_browser.getControl("Keywords").value = "RSS, bookmark"
+    >>> owner_browser.getControl("Content").value = "Use the bookmark bar."
+    >>> owner_browser.getControl("Create").click()
 
 The FAQ is created and the browser displays the page for Sample Person.
 
@@ -71,21 +73,24 @@ The FAQ is created and the browser displays the page for Sample Person.
     FAQ #... : Questions : Mozilla Firefox
 
     >>> content = find_main_content(owner_browser.contents)
-    >>> print(extract_text(find_tag_by_id(content, 'faq-keywords')))
+    >>> print(extract_text(find_tag_by_id(content, "faq-keywords")))
     Keywords:
     RSS bookmark
-    >>> print(extract_text(find_tag_by_id(content, 'faq-content')))
+    >>> print(extract_text(find_tag_by_id(content, "faq-content")))
     Use the bookmark bar.
 
 They return to the project's list of FAQs to verify that the newest
 FAQ is listed first.
 
-    >>> owner_browser.getLink('List all FAQs').click()
+    >>> owner_browser.getLink("List all FAQs").click()
     >>> print(owner_browser.title)
     FAQs for Mozilla Firefox : Questions : Mozilla Firefox
 
-    >>> print(extract_text(
-    ...     find_tag_by_id(owner_browser.contents, 'faqs-listing')))
+    >>> print(
+    ...     extract_text(
+    ...         find_tag_by_id(owner_browser.contents, "faqs-listing")
+    ...     )
+    ... )
     How do I read RSS?
     What's the keyboard shortcut for [random feature]?
     How do I install plugins (Shockwave, QuickTime, etc.)?
diff --git a/lib/lp/answers/stories/faq-browse-and-search.rst b/lib/lp/answers/stories/faq-browse-and-search.rst
index f507d8f..d5ad455 100644
--- a/lib/lp/answers/stories/faq-browse-and-search.rst
+++ b/lib/lp/answers/stories/faq-browse-and-search.rst
@@ -18,14 +18,15 @@ Kubuntu must enable answers to access questions.
     >>> from lp.app.enums import ServiceUsage
     >>> from lp.registry.interfaces.distribution import IDistributionSet
 
-    >>> login('admin@xxxxxxxxxxxxx')
-    >>> getUtility(IDistributionSet)['kubuntu'].answers_usage = (
-    ...     ServiceUsage.LAUNCHPAD)
+    >>> login("admin@xxxxxxxxxxxxx")
+    >>> getUtility(IDistributionSet)[
+    ...     "kubuntu"
+    ... ].answers_usage = ServiceUsage.LAUNCHPAD
     >>> transaction.commit()
     >>> logout()
 
-    >>> browser.open('http://answers.launchpad.test/kubuntu')
-    >>> browser.getLink('All FAQs').click()
+    >>> browser.open("http://answers.launchpad.test/kubuntu";)
+    >>> browser.getLink("All FAQs").click()
 
 Unfortunately, it seems that nobody has problems or questions about
 Kubuntu:
@@ -35,7 +36,7 @@ Kubuntu:
     >>> print(browser.title)
     FAQs for Kubuntu : Questions : Kubuntu
 
-    >>> print(extract_text(find_main_content(browser.contents).find('p')))
+    >>> print(extract_text(find_main_content(browser.contents).find("p")))
     There are no FAQs for Kubuntu.
 
 
@@ -45,14 +46,14 @@ Browsing FAQs
 She learns through Fozzie Bear that support for Kubuntu is really
 happening on the Ubuntu project.
 
-    >>> browser.open('http://answers.launchpad.test/ubuntu')
-    >>> browser.getLink('All FAQs').click()
+    >>> browser.open("http://answers.launchpad.test/ubuntu";)
+    >>> browser.getLink("All FAQs").click()
     >>> print(browser.title)
     FAQs for Ubuntu : Questions : Ubuntu
 
 She sees a listing of the current FAQs about Ubuntu:
 
-    >>> print(extract_text(find_tag_by_id(browser.contents, 'faqs-listing')))
+    >>> print(extract_text(find_tag_by_id(browser.contents, "faqs-listing")))
     How can I play MP3/Divx/DVDs/Quicktime/Realmedia files or view
         Flash/Java web pages
     How can I customize my desktop?
@@ -62,10 +63,10 @@ She sees a listing of the current FAQs about Ubuntu:
 
 There is a 'Next' link to see the second batch of results:
 
-    >>> browser.getLink('Next').click()
+    >>> browser.getLink("Next").click()
     >>> print(browser.title)
     FAQs for Ubuntu : Questions : Ubuntu
-    >>> print(extract_text(find_tag_by_id(browser.contents, 'faqs-listing')))
+    >>> print(extract_text(find_tag_by_id(browser.contents, "faqs-listing")))
     Wireless Networking Documentation
 
 Going back to the first page, she realises that when she leaves the
@@ -73,17 +74,18 @@ mouse over a FAQ title, the beginning of the FAQ's content appears in
 a pop-up:
 
     >>> import re
-    >>> browser.getLink('First').click()
+    >>> browser.getLink("First").click()
     >>> faq_link = find_main_content(browser.contents).find(
-    ...     'a', text=re.compile('How can I play MP3/Divx/DVDs'))
-    >>> print(faq_link.find_parent('li')['title'])
+    ...     "a", text=re.compile("How can I play MP3/Divx/DVDs")
+    ... )
+    >>> print(faq_link.find_parent("li")["title"])
     Playing many common formats such as DVIX, MP3, DVD, or Flash ...
 
 She clicks on FAQ's title to display the complete FAQ content:
 
     >>> from lp.services.helpers import backslashreplace
 
-    >>> browser.getLink('How can I play MP3/Divx').click()
+    >>> browser.getLink("How can I play MP3/Divx").click()
     >>> print(backslashreplace(browser.title))
     FAQ #6 : Questions : Ubuntu
     >>> print(browser.url)
@@ -91,7 +93,7 @@ She clicks on FAQ's title to display the complete FAQ content:
 
 The FAQ page has a link back to the FAQ listing:
 
-    >>> browser.getLink('List all FAQs').click()
+    >>> browser.getLink("List all FAQs").click()
     >>> print(browser.title)
     FAQs for Ubuntu : Questions : Ubuntu
     >>> print(browser.url)
@@ -104,61 +106,62 @@ Searching FAQs
 All FAQs listing have a search box at the top, where the user can
 enter keywords that be used to filter the displayed FAQs.
 
-    >>> browser.getControl(name='field.search_text').value = 'crash on boot'
-    >>> browser.getControl('Search', index=0).click()
+    >>> browser.getControl(name="field.search_text").value = "crash on boot"
+    >>> browser.getControl("Search", index=0).click()
     >>> print(backslashreplace(browser.title))
     FAQs matching \u201ccrash on boot\u201d for Ubuntu : Questions : Ubuntu
 
 When no matches are found, a simple message is displayed:
 
-    >>> message = find_main_content(browser.contents).find('p')
+    >>> message = find_main_content(browser.contents).find("p")
     >>> print(backslashreplace(extract_text(message)))
     There are no FAQs for Ubuntu matching \u201ccrash on boot\u201d.
 
 Otherwise, the listing only contains the matching FAQs.
 
-    >>> browser.getControl(name='field.search_text').value = 'wifi'
-    >>> browser.getControl('Search', index=0).click()
+    >>> browser.getControl(name="field.search_text").value = "wifi"
+    >>> browser.getControl("Search", index=0).click()
 
-    >>> print(extract_text(find_tag_by_id(browser.contents, 'faqs-listing')))
+    >>> print(extract_text(find_tag_by_id(browser.contents, "faqs-listing")))
     Wireless Networking Documentation
 
 When searching for FAQs, a link to the questions matching the same
 keywords is displayed. (The link is only displayed when there are
 matches.)
 
-    >>> browser.getControl(name='field.search_text').value = 'plugin'
-    >>> browser.getControl('Search', index=0).click()
+    >>> browser.getControl(name="field.search_text").value = "plugin"
+    >>> browser.getControl("Search", index=0).click()
 
-    >>> message = find_main_content(browser.contents).find('p')
+    >>> message = find_main_content(browser.contents).find("p")
     >>> print(extract_text(message))
     You can also consult the list of 1 question(s) matching “plugin”.
 
 Following the link will show the questions results:
 
-    >>> browser.getLink('1 question(s)').click()
+    >>> browser.getLink("1 question(s)").click()
     >>> print(browser.title)
     Questions : Ubuntu
 
-    >>> questions = find_tag_by_id(browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> questions = find_tag_by_id(browser.contents, "question-listing")
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     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
 the same keywords.
 
-    >>> message = find_tag_by_id(browser.contents, 'found-matching-faqs')
+    >>> message = find_tag_by_id(browser.contents, "found-matching-faqs")
     >>> print(extract_text(message))
     You can also consult the list of 1 FAQ(s) matching “plugin”.
 
 Following the link will show the questions results:
 
-    >>> browser.getLink('1 FAQ(s)').click()
+    >>> browser.getLink("1 FAQ(s)").click()
     >>> print(backslashreplace(browser.title))
     FAQs matching \u201cplugin\u201d for Ubuntu : Questions : Ubuntu
 
-    >>> print(extract_text(find_tag_by_id(browser.contents, 'faqs-listing')))
+    >>> print(extract_text(find_tag_by_id(browser.contents, "faqs-listing")))
     How can I play MP3/Divx/DVDs/Quicktime/Realmedia files or view
         Flash/Java web pages
 
@@ -170,9 +173,10 @@ Although distribution source packages aren't directly associated with
 FAQs, the 'All FAQs' link that appears in that context links to the
 distribution FAQs.
 
-    >>> browser.open('http://answers.launchpad.test/ubuntu/'
-    ...              '+source/mozilla-firefox')
-    >>> browser.getLink('All FAQs').click()
+    >>> browser.open(
+    ...     "http://answers.launchpad.test/ubuntu/"; "+source/mozilla-firefox"
+    ... )
+    >>> browser.getLink("All FAQs").click()
     >>> print(browser.title)
     FAQs for Ubuntu : Questions : Ubuntu
     >>> print(browser.url)
@@ -184,12 +188,12 @@ Accessing an FAQ directly
 
 Asking for a non-existent FAQ or an invalid ID will raise a 404 error.
 
-    >>> anon_browser.open('http://answers.launchpad.test/ubuntu/+faq/171717')
+    >>> anon_browser.open("http://answers.launchpad.test/ubuntu/+faq/171717";)
     Traceback (most recent call last):
       ...
     zope.publisher.interfaces.NotFound: ...
 
-    >>> anon_browser.open('http://answers.launchpad.test/ubuntu/+faq/bad')
+    >>> anon_browser.open("http://answers.launchpad.test/ubuntu/+faq/bad";)
     Traceback (most recent call last):
       ...
     zope.publisher.interfaces.NotFound: ...
diff --git a/lib/lp/answers/stories/faq-edit.rst b/lib/lp/answers/stories/faq-edit.rst
index 4182ab7..3d047f8 100644
--- a/lib/lp/answers/stories/faq-edit.rst
+++ b/lib/lp/answers/stories/faq-edit.rst
@@ -9,17 +9,17 @@ That action is only available to project owners. That's why the link doesn't
 appear for the anonymous user nor No Privileges Person:
 
     >>> from lp.services.helpers import backslashreplace
-    >>> anon_browser.open('http://answers.launchpad.test/firefox/+faq/7')
+    >>> anon_browser.open("http://answers.launchpad.test/firefox/+faq/7";)
     >>> print(backslashreplace(anon_browser.title))
     FAQ #7 : Questions : Mozilla Firefox
 
-    >>> anon_browser.getLink('Edit FAQ')
+    >>> anon_browser.getLink("Edit FAQ")
     Traceback (most recent call last):
     ...
     zope.testbrowser.browser.LinkNotFoundError
 
-    >>> user_browser.open('http://answers.launchpad.test/firefox/+faq/7')
-    >>> user_browser.getLink('Edit FAQ')
+    >>> user_browser.open("http://answers.launchpad.test/firefox/+faq/7";)
+    >>> user_browser.getLink("Edit FAQ")
     Traceback (most recent call last):
     ...
     zope.testbrowser.browser.LinkNotFoundError
@@ -27,12 +27,14 @@ appear for the anonymous user nor No Privileges Person:
 Even trying to access the link directly will fail:
 
     >>> anon_browser.open(
-    ...     'http://answers.launchpad.test/firefox/+faq/7/+edit')
+    ...     "http://answers.launchpad.test/firefox/+faq/7/+edit";
+    ... )
     Traceback (most recent call last):
     ...
     zope.security.interfaces.Unauthorized: ...
     >>> user_browser.open(
-    ...     'http://answers.launchpad.test/firefox/+faq/7/+edit')
+    ...     "http://answers.launchpad.test/firefox/+faq/7/+edit";
+    ... )
     Traceback (most recent call last):
     ...
     zope.security.interfaces.Unauthorized: ...
@@ -40,9 +42,9 @@ Even trying to access the link directly will fail:
 The link is accessible to Sample Person who is the owner of the Firefox
 project:
 
-    >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
-    >>> browser.open('http://answers.launchpad.test/firefox/+faq/7')
-    >>> browser.getLink('Edit FAQ').click()
+    >>> browser.addHeader("Authorization", "Basic test@xxxxxxxxxxxxx:test")
+    >>> browser.open("http://answers.launchpad.test/firefox/+faq/7";)
+    >>> browser.getLink("Edit FAQ").click()
     >>> print(browser.url)
     http://answers.launchpad.test/firefox/+faq/7/+edit
     >>> print(browser.title)
@@ -51,30 +53,32 @@ project:
 The user can change the title, keywords and content fields. They then
 click 'Save' to save their changes.
 
-    >>> print(browser.getControl('Title').value)
+    >>> print(browser.getControl("Title").value)
     How do I install Java?
-    >>> browser.getControl('Keywords').value
+    >>> browser.getControl("Keywords").value
     ''
-    >>> print(browser.getControl('Content').value)
+    >>> print(browser.getControl("Content").value)
     Windows
     On Windows, ...
 
-    >>> browser.getControl('Keywords').value = (
-    ...     'windows ubuntu plugins extensions')
-    >>> browser.getControl('Content').value += (
-    ...     '\nUbuntu:\nSee https://help.ubuntu.com/community/Java\n')
+    >>> browser.getControl(
+    ...     "Keywords"
+    ... ).value = "windows ubuntu plugins extensions"
+    >>> browser.getControl(
+    ...     "Content"
+    ... ).value += "\nUbuntu:\nSee https://help.ubuntu.com/community/Java\n";
 
-    >>> browser.getControl('Save').click()
+    >>> browser.getControl("Save").click()
 
 The user can see their changes on the page:
 
     >>> print(browser.url)
     http://answers.launchpad.test/firefox/+faq/7
 
-    >>> print(extract_text(find_tag_by_id(browser.contents, 'faq-keywords')))
+    >>> print(extract_text(find_tag_by_id(browser.contents, "faq-keywords")))
     Keywords: windows ubuntu plugins extensions
 
-    >>> print(extract_text(find_tag_by_id(browser.contents, 'faq-content')))
+    >>> print(extract_text(find_tag_by_id(browser.contents, "faq-content")))
     Windows
     On Windows,...
     Ubuntu: See https://help.ubuntu.com/community/Java
@@ -82,5 +86,5 @@ The user can see their changes on the page:
 The 'Last updated by' field in the 'Lifecycle' portlet is updated
 with the name of the user who made the last modification:
 
-    >>> print(extract_text(find_tag_by_id(browser.contents, 'faq-updated')))
+    >>> print(extract_text(find_tag_by_id(browser.contents, "faq-updated")))
     Last updated by: Sample Person ...
diff --git a/lib/lp/answers/stories/project-add-question.rst b/lib/lp/answers/stories/project-add-question.rst
index 3011d28..9cc84e6 100644
--- a/lib/lp/answers/stories/project-add-question.rst
+++ b/lib/lp/answers/stories/project-add-question.rst
@@ -12,7 +12,7 @@ user's preferred languages are supported.
 Make the test browser look like it's coming from an arbitrary South African
 IP address, since we'll use that later.
 
-    >>> user_browser.addHeader('X_FORWARDED_FOR', '196.36.161.227')
+    >>> user_browser.addHeader("X_FORWARDED_FOR", "196.36.161.227")
 
 
 Ask a question about a Product in a ProjectGroup
@@ -24,14 +24,14 @@ exception (and the user will be prompted to login from another page).
 The logged user will see the Ask a Question page, for the Mozilla
 Project in this case.
 
-    >>> anon_browser.open('http://answers.launchpad.test/mozilla')
-    >>> anon_browser.getLink('Ask a question').click()
+    >>> anon_browser.open("http://answers.launchpad.test/mozilla";)
+    >>> anon_browser.getLink("Ask a question").click()
     Traceback (most recent call last):
       ...
     zope.security.interfaces.Unauthorized: ...
 
-    >>> user_browser.open('http://answers.launchpad.test/mozilla')
-    >>> user_browser.getLink('Ask a question').click()
+    >>> user_browser.open("http://answers.launchpad.test/mozilla";)
+    >>> user_browser.getLink("Ask a question").click()
     >>> print(user_browser.title)
     Ask a question...
 
@@ -41,23 +41,25 @@ products associated with the project. Note that the site policy is to
 use the word 'Project' for 'Products' (and 'Distributions') so that
 users do not have to learn our business' semantics.
 
-    >>> print(user_browser.getControl('Project').displayOptions)
+    >>> print(user_browser.getControl("Project").displayOptions)
     ['Mozilla Firefox', 'Mozilla Thunderbird']
 
 The first item in the list is the default value, and it will be
 submitted if the user does not change it.
 
-    >>> print(user_browser.getControl('Project').displayValue)
+    >>> print(user_browser.getControl("Project").displayValue)
     ['Mozilla Firefox']
 
 Like for the regular workflow, the user is shown a list of languages,
 with the supported languages flagged with an asterisk.
 
-    >>> print(user_browser.getControl('Language').displayValue)
+    >>> print(user_browser.getControl("Language").displayValue)
     ['English (en) *']
 
-    >>> langs = sorted(user_browser.getControl('Language').displayOptions)
-    >>> for lang in langs: print(lang)
+    >>> langs = sorted(user_browser.getControl("Language").displayOptions)
+    >>> for lang in langs:
+    ...     print(lang)
+    ...
     Afrikaans (af)
     English (en) *
     Sotho, Southern (st)
@@ -68,67 +70,71 @@ No Privileged Person enters a short summary of their problem, and submits
 the form with the 'Continue' button. In this case, a question for
 Firefox in English regarding SVG.
 
-    >>> user_browser.getControl('Summary').value = (
-    ...     'Problem with SVG')
-    >>> user_browser.getControl('Continue').click()
+    >>> user_browser.getControl("Summary").value = "Problem with SVG"
+    >>> user_browser.getControl("Continue").click()
 
 They're shown a list of similar questions related to the product Firefox
 that they submitted:
 
     >>> similar_questions = find_tag_by_id(
-    ...     user_browser.contents, 'similar-questions')
-    >>> for row in similar_questions.find_all('li'):
+    ...     user_browser.contents, "similar-questions"
+    ... )
+    >>> for row in similar_questions.find_all("li"):
     ...     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
 the question. The user chooses Thunderbird from the 'Project' product
 list.
 
-    >>> user_browser.getControl('Mozilla Thunderbird').selected = True
+    >>> user_browser.getControl("Mozilla Thunderbird").selected = True
 
 If they empty the question summary and submits the form, they'll be
 redirected to the first page. Let's assume they do this by accident as
 they revise the summary after reading the similar questions.
 
-    >>> user_browser.getControl('Summary').value = ''
-    >>> user_browser.getControl('Post Question').click()
+    >>> user_browser.getControl("Summary").value = ""
+    >>> user_browser.getControl("Post Question").click()
 
 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').decode_contents())
+    >>> 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
 selected. No Privileged Person re-enters their question summary, and
 submits the form.
 
-    >>> print(user_browser.getControl('Project').displayValue)
+    >>> print(user_browser.getControl("Project").displayValue)
     ['Mozilla Thunderbird']
 
-    >>> user_browser.getControl('Summary').value = (
-    ...     'Problem displaying complex SVG')
-    >>> user_browser.getControl('Continue').click()
+    >>> user_browser.getControl(
+    ...     "Summary"
+    ... ).value = "Problem displaying complex SVG"
+    >>> user_browser.getControl("Continue").click()
 
 The user is again shown similar questions, this time for the product
 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').decode_contents())
+    >>> print(soup.find("p").decode_contents())
     There are no existing FAQs or questions similar to the summary you
     entered.
 
 The user then elaborates upon their question by entering a description of
 the problem. They submit the form using the 'Post Question' button.
 
-    >>> user_browser.getControl('Description').value = (
-    ...  "I received an HTML message containing an inlined SVG\n"
-    ...  "representation of a chessboard. It didn't displayed properly.\n"
-    ...  "Is there a way to configure Thunderbird to display SVG properly?\n")
-    >>> user_browser.getControl('Post Question').click()
+    >>> user_browser.getControl("Description").value = (
+    ...     "I received an HTML message containing an inlined SVG\n"
+    ...     "representation of a chessboard. It didn't displayed properly.\n"
+    ...     "Is there a way to configure Thunderbird to display SVG "
+    ...     "properly?\n"
+    ... )
+    >>> user_browser.getControl("Post Question").click()
 
 No Privileged Person is taken to page displaying their question. From this
 point on, the user's interaction with the question follows to regular
@@ -159,25 +165,28 @@ be a supported language for Thunderbird Questions, which allows us to
 test the supported languages behaviour for non-English languages. Dafydd
 speaks Japanese, so we will use him.
 
-    >>> daf_browser = setupBrowser(auth='Basic daf@xxxxxxxxxxxxx:test')
-    >>> daf_browser.open('http://launchpad.test/~daf/+editlanguages')
+    >>> daf_browser = setupBrowser(auth="Basic daf@xxxxxxxxxxxxx:test")
+    >>> daf_browser.open("http://launchpad.test/~daf/+editlanguages";)
     >>> print(daf_browser.title)
     Language preferences...
 
-    >>> daf_browser.getControl('Japanese').selected
+    >>> daf_browser.getControl("Japanese").selected
     True
 
     >>> daf_browser.open(
-    ...     'http://answers.launchpad.test/thunderbird/+answer-contact')
+    ...     "http://answers.launchpad.test/thunderbird/+answer-contact";
+    ... )
     >>> print(daf_browser.title)
     Answer contact for...
 
-    >>> daf_browser.getControl('I want to be an answer contact for '
-    ...                        'Mozilla Thunderbird').selected = True
-    >>> daf_browser.getControl('Continue').click()
+    >>> daf_browser.getControl(
+    ...     "I want to be an answer contact for " "Mozilla Thunderbird"
+    ... ).selected = True
+    >>> daf_browser.getControl("Continue").click()
     >>> content = find_main_content(daf_browser.contents)
-    >>> for message in content.find_all('div', 'informational message'):
-    ...      print(message.decode_contents())
+    >>> for message in content.find_all("div", "informational message"):
+    ...     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
@@ -185,15 +194,14 @@ then have a condition for certain products, Thunderbird in this example,
 where the user's languages and the answer contact's languages will
 match. This condition demonstrates the supported language behaviour.
 
-    >>> user_browser.open(
-    ...     'http://launchpad.test/~no-priv/+editlanguages')
+    >>> user_browser.open("http://launchpad.test/~no-priv/+editlanguages";)
     >>> print(user_browser.title)
     Language preferences...
 
-    >>> user_browser.getControl('Japanese').selected = True
-    >>> user_browser.getControl('Save').click()
+    >>> user_browser.getControl("Japanese").selected = True
+    >>> user_browser.getControl("Save").click()
     >>> soup = find_main_content(user_browser.contents)
-    >>> print(soup.find('div', 'informational message').decode_contents())
+    >>> 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
@@ -203,8 +211,9 @@ list. This indicates that they can ask a question in Japanese or English
 and expect someone to reply in the same language.
 
     >>> user_browser.open(
-    ...     'http://answers.launchpad.test/firefox/+addquestion')
-    >>> print(user_browser.getControl('Language').displayOptions)
+    ...     "http://answers.launchpad.test/firefox/+addquestion";
+    ... )
+    >>> print(user_browser.getControl("Language").displayOptions)
     ['English (en) *', 'Japanese (ja)']
 
 The supported languages will not be shown immediately when Sample Person
@@ -230,8 +239,8 @@ Privileges Person visits the Ask a question page from a project just as
 No Privileged Person did above, but this time in wants to do so in
 Japanese.
 
-    >>> user_browser.open('http://answers.launchpad.test/mozilla')
-    >>> user_browser.getLink('Ask a question').click()
+    >>> user_browser.open("http://answers.launchpad.test/mozilla";)
+    >>> user_browser.getLink("Ask a question").click()
     >>> print(user_browser.title)
     Ask a question...
 
@@ -239,10 +248,10 @@ The page displays a list of products associated with the project. The
 first item in the list is the default value, and it will be submitted if
 the user does not change it.
 
-    >>> print(user_browser.getControl('Project').displayOptions)
+    >>> print(user_browser.getControl("Project").displayOptions)
     ['Mozilla Firefox', 'Mozilla Thunderbird']
 
-    >>> print(user_browser.getControl('Project').displayValue)
+    >>> print(user_browser.getControl("Project").displayValue)
     ['Mozilla Firefox']
 
 Like for the regular workflow, the user is shown a list of languages,
@@ -253,26 +262,27 @@ languages other than the default language of English. If the user were
 to submit their question in another language, they might find that the
 language is supported on the next page.
 
-    >>> print(user_browser.getControl('Language').displayOptions)
+    >>> print(user_browser.getControl("Language").displayOptions)
     ['English (en) *', 'Japanese (ja)']
 
-    >>> user_browser.getControl('Language').value = ['en']
+    >>> user_browser.getControl("Language").value = ["en"]
 
 No Privileges Person enters a short summary of their problem in English
 because Japanese is not listed as supported. They submits the form with
 the 'Continue' button without setting the product. In this case, they are
 asking a question for Firefox in English regarding SVG.
 
-    >>> user_browser.getControl('Summary').value = (
-    ...     'Problem displaying complex SVG')
-    >>> user_browser.getControl('Continue').click()
+    >>> user_browser.getControl(
+    ...     "Summary"
+    ... ).value = "Problem displaying complex SVG"
+    >>> user_browser.getControl("Continue").click()
 
 They're shown a list of similar questions related to the product Firefox.
 They can see which of their preferred languages are supported for the
 Firefox product by reviewing which languages has asterisks in the
 Languages list--only English in the example.
 
-    >>> print(user_browser.getControl('Language').displayOptions)
+    >>> print(user_browser.getControl("Language").displayOptions)
     ['English (en) *', 'Japanese (ja)']
 
 No Privileges Person can still change the product for which they're asking
@@ -282,8 +292,8 @@ product list and reviews the list of supported languages again. The
 language list does not change because the Thunderbird was not submitted
 as the product.
 
-    >>> user_browser.getControl('Mozilla Thunderbird').selected = True
-    >>> print(user_browser.getControl('Language').displayOptions)
+    >>> user_browser.getControl("Mozilla Thunderbird").selected = True
+    >>> print(user_browser.getControl("Language").displayOptions)
     ['English (en) *', 'Japanese (ja)']
 
 If No Privileges Person asks a question in Japanese, it will be
@@ -300,22 +310,23 @@ Let's try this again from the starting page, but this time, No
 Privileges Person correctly chooses Thunderbird as the subject of their
 question.
 
-    >>> user_browser.open('http://answers.launchpad.test/mozilla')
-    >>> user_browser.getLink('Ask a question').click()
+    >>> user_browser.open("http://answers.launchpad.test/mozilla";)
+    >>> user_browser.getLink("Ask a question").click()
     >>> print(user_browser.title)
     Ask a question...
 
-    >>> user_browser.getControl('Mozilla Thunderbird').selected = True
+    >>> user_browser.getControl("Mozilla Thunderbird").selected = True
 
 They write their summary in English as he sees that is the only supported
 Language, and 'Continues' to the next page.
 
-    >>> print(user_browser.getControl('Language').displayOptions)
+    >>> print(user_browser.getControl("Language").displayOptions)
     ['English (en) *', 'Japanese (ja)']
 
-    >>> user_browser.getControl('Summary').value = (
-    ...     'Problem displaying complex SVG')
-    >>> user_browser.getControl('Continue').click()
+    >>> user_browser.getControl(
+    ...     "Summary"
+    ... ).value = "Problem displaying complex SVG"
+    >>> user_browser.getControl("Continue").click()
 
 The product Thunderbird that they selected on the previous screen is still
 selected. They can see that this product has support for Japanese as well
@@ -324,22 +335,24 @@ list. Japanese is supported because Dafydd speaks Japanese and is an
 answer contact for Thunderbird. We see this only after a question
 summary is submitted for a product.
 
-    >>> print(user_browser.getControl('Language').displayOptions)
+    >>> print(user_browser.getControl("Language").displayOptions)
     ['English (en) *', 'Japanese (ja) *']
 
 No Privileges Person sets the language to Japanese, changes their question
 summary, writes a description, and submits the form with the 'Post
 Question' button.
 
-    >>> print(user_browser.getControl('Project').displayValue)
+    >>> print(user_browser.getControl("Project").displayValue)
     ['Mozilla Thunderbird']
 
-    >>> user_browser.getControl('Japanese (ja) *').selected = True
-    >>> user_browser.getControl('Summary').value = (
-    ...     'Pretend this is written in Japanese')
-    >>> user_browser.getControl('Description').value = (
-    ...      "Something in kanji and hiragana.")
-    >>> user_browser.getControl('Post Question').click()
+    >>> user_browser.getControl("Japanese (ja) *").selected = True
+    >>> user_browser.getControl(
+    ...     "Summary"
+    ... ).value = "Pretend this is written in Japanese"
+    >>> user_browser.getControl(
+    ...     "Description"
+    ... ).value = "Something in kanji and hiragana."
+    >>> user_browser.getControl("Post Question").click()
 
 The user is taken to page displaying their question. Changing the language
 or the summary did not search for similar questions again--the question
diff --git a/lib/lp/answers/stories/question-add-in-other-languages.rst b/lib/lp/answers/stories/question-add-in-other-languages.rst
index 3f5a2e0..f80fb81 100644
--- a/lib/lp/answers/stories/question-add-in-other-languages.rst
+++ b/lib/lp/answers/stories/question-add-in-other-languages.rst
@@ -5,15 +5,15 @@ It is possible to ask questions in a language other than English. The
 'Ask a question' page has a pop-up where the user can select the language
 of the question. By default, the question language is 'English'.
 
-    >>> user_browser.open('http://launchpad.test/ubuntu/+questions')
-    >>> user_browser.getLink('Ask a question').click()
-    >>> user_browser.getControl('Language').value
+    >>> user_browser.open("http://launchpad.test/ubuntu/+questions";)
+    >>> user_browser.getLink("Ask a question").click()
+    >>> user_browser.getControl("Language").value
     ['en']
 
 The user may choose from any of their preferred languages and there is a
 link to enable to change their preferred languages:
 
-    >>> user_browser.getLink('Change your preferred languages').click()
+    >>> user_browser.getLink("Change your preferred languages").click()
     >>> print(user_browser.title)
     Language preferences...
     >>> user_browser.url
@@ -21,10 +21,10 @@ link to enable to change their preferred languages:
 
 The languages that are supported are displayed with an asterisk.
 
-    >>> browser.addHeader('Authorization', 'Basic salgado@xxxxxxxxxx:test')
-    >>> browser.open('http://launchpad.test/ubuntu/+addquestion')
+    >>> browser.addHeader("Authorization", "Basic salgado@xxxxxxxxxx:test")
+    >>> browser.open("http://launchpad.test/ubuntu/+addquestion";)
 
-    >>> browser.getControl('Language').displayOptions
+    >>> browser.getControl("Language").displayOptions
     ['English (en) *', 'Portuguese (Brazil) (pt_BR)']
 
 Although it's possible to ask questions in any of the user's preferred
@@ -32,17 +32,19 @@ languages, we need to do some checks to warn the user in case they're using
 a language that is not spoken/understood by any of the context's answer
 contacts.
 
-    >>> browser.getControl('Language').value = ['pt_BR']
-    >>> browser.getControl('Summary').value = (
-    ...     'Abrir uma pagina que requer java quebra o firefox')
-    >>> browser.getControl('Continue').click()
+    >>> browser.getControl("Language").value = ["pt_BR"]
+    >>> browser.getControl(
+    ...     "Summary"
+    ... ).value = "Abrir uma pagina que requer java quebra o firefox"
+    >>> browser.getControl("Continue").click()
 
 At this point we'll present any similar questions (in any language)
 /and/ a warning message explaining that the chosen language is not
 understood by any member of the support community.
 
     >>> similar_questions = find_tag_by_id(
-    ...     browser.contents, 'similar-questions')
+    ...     browser.contents, "similar-questions"
+    ... )
 
     XXX: Making search fast has a significant impact on this tests' use case,
     because there are 9 terms - the query has to ignore 7 of the terms to
@@ -56,8 +58,9 @@ understood by any member of the support community.
     #'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'):
+    >>> for tag in find_tags_by_class(browser.contents, "warning message"):
     ...     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
@@ -67,22 +70,22 @@ understood by any member of the support community.
 The user can still use the 'Change preferred language' link to change
 the list of available languages:
 
-    >>> browser.getLink('Change your preferred languages').url
+    >>> browser.getLink("Change your preferred languages").url
     '.../+editmylanguages'
 
 Since we've already shown the warning, we won't try to block the user
 from asking a question in the language of their choice.
 
-    >>> browser.getControl('Language').value
+    >>> browser.getControl("Language").value
     ['pt_BR']
-    >>> browser.getControl('Description').value = (
-    ...     u'Eu uso Ubuntu em um AMD64 e instalei o plugin java blackdown. '
-    ...     u'O plugin \xe9 exibido em about:plugins e quando eu abro a '
-    ...     u'pagina http://java.com/en/download/help/testvm.xml, ela '
-    ...     u'carrega corretamente e mostra a minha versao do java. No '
-    ...     u'entanto, mover o mouse na pagina faz com que o firefox quebre.'
-    ...     ).encode('utf-8')
-    >>> browser.getControl('Post Question').click()
+    >>> browser.getControl("Description").value = (
+    ...     "Eu uso Ubuntu em um AMD64 e instalei o plugin java blackdown. "
+    ...     "O plugin \xe9 exibido em about:plugins e quando eu abro a "
+    ...     "pagina http://java.com/en/download/help/testvm.xml, ela "
+    ...     "carrega corretamente e mostra a minha versao do java. No "
+    ...     "entanto, mover o mouse na pagina faz com que o firefox quebre."
+    ... ).encode("utf-8")
+    >>> browser.getControl("Post Question").click()
     >>> browser.url
     '.../ubuntu/+question/...'
     >>> print(browser.title)
@@ -95,43 +98,43 @@ the language in the question details portlet.
 
     >>> from lp.services.beautifulsoup import BeautifulSoup
     >>> soup = BeautifulSoup(browser.contents)
-    >>> print(soup.find('div', id='question')['lang'])
+    >>> print(soup.find("div", id="question")["lang"])
     pt-BR
-    >>> print(soup.html['dir'])
+    >>> print(soup.html["dir"])
     ltr
-    >>> print(extract_text(find_tag_by_id(soup, 'question-lang')))
+    >>> print(extract_text(find_tag_by_id(soup, "question-lang")))
     Language: Portuguese (Brazil) ...
 
 It's also possible that the user chose English in the first page but
 then changed their mind on the second page.
 
-    >>> browser = setupBrowser(auth='Basic daf@xxxxxxxxxxxxx:test')
-    >>> browser.open('http://launchpad.test/ubuntu/+addquestion')
+    >>> browser = setupBrowser(auth="Basic daf@xxxxxxxxxxxxx:test")
+    >>> browser.open("http://launchpad.test/ubuntu/+addquestion";)
 
-    >>> browser.getControl('Language').value = ['en']
-    >>> browser.getControl('Summary').value = 'some random words'
-    >>> browser.getControl('Continue').click()
+    >>> browser.getControl("Language").value = ["en"]
+    >>> browser.getControl("Summary").value = "some random words"
+    >>> browser.getControl("Continue").click()
 
 In this case they won't be warned, because we assume all members of the
 support community can understand English.
 
-    >>> len(find_tags_by_class(browser.contents, 'warning message'))
+    >>> len(find_tags_by_class(browser.contents, "warning message"))
     0
 
 If now they change their mind and decides to enter the question details in
 Welsh, we'll have to warn them.
 
-    >>> browser.getControl('Language').value = ['cy']
-    >>> browser.getControl('Summary').value = 'Gofyn cymorth'
-    >>> browser.getControl('Description').value = (
-    ...     'Ghai damweiniol gair.')
-    >>> browser.getControl('Post Question').click()
+    >>> browser.getControl("Language").value = ["cy"]
+    >>> browser.getControl("Summary").value = "Gofyn cymorth"
+    >>> browser.getControl("Description").value = "Ghai damweiniol gair."
+    >>> browser.getControl("Post Question").click()
 
     >>> browser.url
     'http://launchpad.test/ubuntu/+addquestion'
 
-    >>> for tag in find_tags_by_class(browser.contents, 'warning message'):
+    >>> for tag in find_tags_by_class(browser.contents, "warning message"):
     ...     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
@@ -141,15 +144,18 @@ Welsh, we'll have to warn them.
 If they change the language to another unsupported language, we will
 display the warning again.
 
-    >>> browser.getControl('Language').value = ['ja']
-    >>> browser.getControl('Summary').value = (
-    ...     u'\u52a9\u3051\u306e\u8981\u6c42'.encode('utf-8'))
-    >>> browser.getControl('Description').value = (
-    ...     u'\u3042\u308b\u4efb\u610f\u5358\u8a9e\u3002'.encode('utf-8'))
-    >>> browser.getControl('Post Question').click()
+    >>> browser.getControl("Language").value = ["ja"]
+    >>> browser.getControl(
+    ...     "Summary"
+    ... ).value = "\u52a9\u3051\u306e\u8981\u6c42".encode("utf-8")
+    >>> browser.getControl(
+    ...     "Description"
+    ... ).value = "\u3042\u308b\u4efb\u610f\u5358\u8a9e\u3002".encode("utf-8")
+    >>> browser.getControl("Post Question").click()
 
-    >>> for tag in find_tags_by_class(browser.contents, 'warning message'):
+    >>> for tag in find_tags_by_class(browser.contents, "warning message"):
     ...     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
@@ -159,9 +165,9 @@ display the warning again.
 If even after the warning they decide to go ahead, we have to accept the
 new question.
 
-    >>> browser.getControl('Post Question').click()
+    >>> browser.getControl("Post Question").click()
     >>> browser.url
     '.../ubuntu/+question/...'
     >>> print(browser.title)
     Question #... : Questions : Ubuntu
-    >>> portlet = find_tag_by_id(browser.contents, 'portlet-details')
+    >>> portlet = find_tag_by_id(browser.contents, "portlet-details")
diff --git a/lib/lp/answers/stories/question-add.rst b/lib/lp/answers/stories/question-add.rst
index b24893c..9be06dc 100644
--- a/lib/lp/answers/stories/question-add.rst
+++ b/lib/lp/answers/stories/question-add.rst
@@ -5,24 +5,24 @@ There are two paths available to a user to ask a new question. The first
 one involes two steps. First, go to the product or distribution for
 which support is desired:
 
-    >>> browser.open('http://answers.launchpad.test/ubuntu')
+    >>> browser.open("http://answers.launchpad.test/ubuntu";)
     >>> print(browser.title)
     Questions : Ubuntu
 
 The user sees an involvement link to ask a question.
 
-    >>> link = find_tag_by_id(browser.contents, 'involvement').a
+    >>> link = find_tag_by_id(browser.contents, "involvement").a
     >>> print(extract_text(link))
     Ask a question
 
 Asking a new question requires logging in:
 
-    >>> browser.getLink('Ask a question').click()
+    >>> browser.getLink("Ask a question").click()
     Traceback (most recent call last):
       ...
     zope.security.interfaces.Unauthorized: ...
-    >>> user_browser.open('http://answers.launchpad.test/ubuntu/')
-    >>> user_browser.getLink('Ask a question').click()
+    >>> user_browser.open("http://answers.launchpad.test/ubuntu/";)
+    >>> user_browser.getLink("Ask a question").click()
     >>> print(user_browser.title)
     Ask a question...
 
@@ -37,12 +37,13 @@ start the creation process. Questions created this way will be
 associated with the source package of the used application.
 
     >>> user_browser.open(
-    ...     'http://launchpad.test/ubuntu/hoary/'
-    ...     '+sources/mozilla-firefox/+gethelp')
+    ...     "http://launchpad.test/ubuntu/hoary/";
+    ...     "+sources/mozilla-firefox/+gethelp"
+    ... )
     >>> print(user_browser.title)
     Help and support...
 
-    >>> user_browser.getLink('Ask a question').click()
+    >>> user_browser.getLink("Ask a question").click()
     >>> print(user_browser.title)
     Ask a question...
 
@@ -58,9 +59,9 @@ exists.
 That step cannot be skipped by the user, if they just click 'Continue',
 an error message will be displayed.
 
-    >>> user_browser.getControl('Summary').value
+    >>> user_browser.getControl("Summary").value
     ''
-    >>> user_browser.getControl('Continue').click()
+    >>> user_browser.getControl("Continue").click()
     >>> print_feedback_messages(user_browser.contents)
     There is 1 error.
     You must enter a summary of your problem.
@@ -72,27 +73,27 @@ XXX: Original search, disabled due to performance issues RBC 20100725. This
 will be reinstated when cheap relevance filtering is available / when search
 is overhauled.
 
-    >>> user_browser.getControl('Summary').value = (
-    ...     'Visiting a web page requiring java crashes firefox')
+    >>> user_browser.getControl(
+    ...     "Summary"
+    ... ).value = "Visiting a web page requiring java crashes firefox"
 
 For now, use a closer search:
 
-    >>> user_browser.getControl('Summary').value = (
-    ...     'java web pages')
-    >>> user_browser.getControl('Continue').click()
+    >>> user_browser.getControl("Summary").value = "java web pages"
+    >>> user_browser.getControl("Continue").click()
     >>> contents = find_main_content(user_browser.contents)
-    >>> similar_faqs = contents.find(id='similar-faqs')
+    >>> similar_faqs = contents.find(id="similar-faqs")
     >>> print(extract_text(similar_faqs))
     How can I play MP3/Divx/DVDs/Quicktime/Realmedia files or view
         Flash/Java web pages
-    >>> print(similar_faqs.a['href'])
+    >>> print(similar_faqs.a["href"])
     http://answers.launchpad.test/ubuntu/+faq/...
 
-    >>> similar_questions = contents.find(id='similar-questions')
+    >>> similar_questions = contents.find(id="similar-questions")
     >>> print(backslashreplace(extract_text(similar_questions)))
     8: Installation of Java Runtime Environment for Mozilla  (Answered)
         posted on ... in ...mozilla-firefox... package in Ubuntu
-    >>> print(similar_questions.a['href'])
+    >>> print(similar_questions.a["href"])
     http://answers.../ubuntu/+source/mozilla-firefox/+question/...
 
 The beginning of the description appears in a small pop-up when the
@@ -100,8 +101,9 @@ mouse is left over the question's title.
 
     >>> import re
     >>> question_link = contents.find(
-    ...     'a', text=re.compile('Installation of Java'))
-    >>> print(question_link.find_parent('li')['title'])
+    ...     "a", text=re.compile("Installation of Java")
+    ... )
+    >>> print(question_link.find_parent("li")["title"])
     <BLANKLINE>
     When opening http://www.gotomypc.com/ with Mozilla, a java run time
     ennvironment plugin is requested.
@@ -115,8 +117,9 @@ Similarly, the beginning of the FAQ's content appears when the mouse
 hovers on the FAQ's title:
 
     >>> faq_link = contents.find(
-    ...     'a', text=re.compile('How can I play MP3/Divx'))
-    >>> print(faq_link.find_parent('li')['title'])
+    ...     "a", text=re.compile("How can I play MP3/Divx")
+    ... )
+    >>> print(faq_link.find_parent("li")["title"])
     Playing many common formats such as DVIX, MP3, DVD, or Flash
     animations require the installation of plugins.
     <BLANKLINE>
@@ -131,12 +134,12 @@ If the shown questions don't help the user, they may post a new question
 by filling in the 'Description' field. They may also edit the
 summary they provided.
 
-    >>> user_browser.getControl('Summary').value
+    >>> user_browser.getControl("Summary").value
     'java web pages'
 
 If the user doesn't provide details, they'll get an error message:
 
-    >>> user_browser.getControl('Post Question').click()
+    >>> user_browser.getControl("Post Question").click()
     >>> print_feedback_messages(user_browser.contents)
     There is 1 error.
     You must provide details about your problem.
@@ -144,8 +147,8 @@ If the user doesn't provide details, they'll get an error message:
 And if they decide to remove the title, they'll be brought back to the
 first step:
 
-    >>> user_browser.getControl('Summary').value = ''
-    >>> user_browser.getControl('Post Question').click()
+    >>> user_browser.getControl("Summary").value = ""
+    >>> user_browser.getControl("Post Question").click()
     >>> print_feedback_messages(user_browser.contents)
     There are 2 errors.
     You must enter a summary of your problem.
@@ -153,20 +156,25 @@ first step:
 Entering a valid title and description will create the new question and
 redirect the user to the question page.
 
-    >>> user_browser.getControl('Summary').value = (
-    ...     'Visiting a web page requiring java crashes firefox')
-    >>> user_browser.getControl('Continue').click()
-    >>> user_browser.getControl('Description').value = (
-    ... "I use Ubuntu on AMD64 and firefox is slow.")
-    >>> user_browser.getControl('Post Question').click()
+    >>> user_browser.getControl(
+    ...     "Summary"
+    ... ).value = "Visiting a web page requiring java crashes firefox"
+    >>> user_browser.getControl("Continue").click()
+    >>> user_browser.getControl(
+    ...     "Description"
+    ... ).value = "I use Ubuntu on AMD64 and firefox is slow."
+    >>> user_browser.getControl("Post Question").click()
     >>> user_browser.url
     '.../ubuntu/+source/mozilla-firefox/+question/...'
     >>> print(user_browser.title)
     Question #... : Questions : mozilla-firefox package : Ubuntu
 
-    >>> print(extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'registration')))
+    >>> print(
+    ...     extract_text(
+    ...         find_tag_by_id(user_browser.contents, "registration")
+    ...     )
+    ... )
     Asked by No Privileges Person ...
     >>> contents = find_main_content(user_browser.contents)
-    >>> print(extract_text(contents.find('div', 'report')))
+    >>> print(extract_text(contents.find("div", "report")))
     I use Ubuntu on AMD64 ...
diff --git a/lib/lp/answers/stories/question-answer-contact.rst b/lib/lp/answers/stories/question-answer-contact.rst
index cdaa6a5..9a2b572 100644
--- a/lib/lp/answers/stories/question-answer-contact.rst
+++ b/lib/lp/answers/stories/question-answer-contact.rst
@@ -9,18 +9,22 @@ The list of answer contacts is displayed in the 'Answer Contact' portlet
 which is available on the 'Answers' facet of the product or
 distribution.
 
-    >>> browser.addHeader('Authorization', 'Basic test@xxxxxxxxxxxxx:test')
-    >>> browser.addHeader('Accept-Language', 'en, es')
-    >>> browser.open('http://launchpad.test/ubuntu/+questions')
-    >>> print(extract_text(
-    ...     find_tag_by_id(
-    ...         browser.contents, 'portlet-answer-contacts-ubuntu')))
+    >>> browser.addHeader("Authorization", "Basic test@xxxxxxxxxxxxx:test")
+    >>> browser.addHeader("Accept-Language", "en, es")
+    >>> browser.open("http://launchpad.test/ubuntu/+questions";)
+    >>> print(
+    ...     extract_text(
+    ...         find_tag_by_id(
+    ...             browser.contents, "portlet-answer-contacts-ubuntu"
+    ...         )
+    ...     )
+    ... )
     Answer contacts for Ubuntu
 
 Anybody can become an answer contact. To register as an answer contact,
 the user clicks on the 'Set answer Contact' link:
 
-    >>> browser.getLink('Set answer contact').click()
+    >>> browser.getLink("Set answer contact").click()
     >>> print(browser.title)
     Answer contact for...
 
@@ -32,8 +36,7 @@ the user clicks on the 'Set answer Contact' link:
 That page displays a series of checkboxes. One for the user and one for
 each team that user is an administrator of.
 
-    >>> browser.getControl(
-    ...     name='field.answer_contact_teams').options
+    >>> browser.getControl(name="field.answer_contact_teams").options
     ['landscape-developers', 'launchpad-users']
 
 The user can select any of these checkboxes to register as an answer
@@ -42,9 +45,10 @@ our case, the user decides to register themselves and the Landscape
 Developers team.
 
     >>> browser.getControl(
-    ...     "I want to be an answer contact for Ubuntu").selected = True
+    ...     "I want to be an answer contact for Ubuntu"
+    ... ).selected = True
     >>> browser.getControl("Landscape Developers").selected = True
-    >>> browser.getControl('Continue').click()
+    >>> browser.getControl("Continue").click()
 
 Answer contacts must tell Launchpad which languages they provide help
 in. As a convenience, the Answer Tracker will set the Person's preferred
@@ -54,36 +58,38 @@ that their preferred languages were set, and uses the link to in the
 notice to review the changes. Sample Person's browser sends the accept-
 languages header with English and Spanish.
 
-    >>> browser.getLink('Your preferred languages').click()
+    >>> browser.getLink("Your preferred languages").click()
     >>> print(browser.title)
     Language preferences...
 
-    >>> browser.getControl('English', index=0).selected
+    >>> browser.getControl("English", index=0).selected
     True
 
-    >>> browser.getControl('Spanish').selected
+    >>> browser.getControl("Spanish").selected
     True
 
 To remove oneself from the answer contacts, the user uses the same 'Set
 answer contact' link and uncheck themselves or the team they want to remove
 from the answer contact list.
 
-    >>> browser.open('http://launchpad.test/ubuntu/+questions')
-    >>> browser.getLink('Set answer contact').click()
+    >>> browser.open("http://launchpad.test/ubuntu/+questions";)
+    >>> browser.getLink("Set answer contact").click()
     >>> browser.getControl(
-    ...     "I want to be an answer contact for Ubuntu").selected
+    ...     "I want to be an answer contact for Ubuntu"
+    ... ).selected
     True
 
     >>> browser.getControl("Landscape Developers").selected
     True
 
     >>> browser.getControl("Landscape Developers").selected = False
-    >>> browser.getControl('Continue').click()
+    >>> browser.getControl("Continue").click()
 
 A confirmation message is displayed:
 
-    >>> for tag in find_tags_by_class(browser.contents, 'message'):
+    >>> for tag in find_tags_by_class(browser.contents, "message"):
     ...     print(tag.decode_contents())
+    ...
     Landscape Developers has been removed as an answer contact for Ubuntu.
 
 
@@ -92,8 +98,8 @@ Product Answer Contacts
 
 The 'Set answer contact' action is also available on products:
 
-    >>> browser.open('http://answers.launchpad.test/firefox')
-    >>> browser.getLink('Set answer contact').click()
+    >>> browser.open("http://answers.launchpad.test/firefox";)
+    >>> browser.getLink("Set answer contact").click()
     >>> print(browser.title)
     Answer contact for...
 
@@ -115,25 +121,26 @@ main story above. They want Landscape Developers team to be answer
 contacts for Ubuntu, but only for Spanish questions to keep the email
 traffic to a manageable volume.
 
-    >>> browser.open('http://answers.launchpad.test/ubuntu/')
+    >>> browser.open("http://answers.launchpad.test/ubuntu/";)
     >>> print(browser.title)
     Questions : Ubuntu
 
-    >>> browser.getLink('Set answer contact').click()
+    >>> browser.getLink("Set answer contact").click()
     >>> browser.getControl("Landscape Developers").selected = True
-    >>> browser.getControl('Continue').click()
-    >>> for message in find_tags_by_class(browser.contents, 'message'):
+    >>> browser.getControl("Continue").click()
+    >>> for message in find_tags_by_class(browser.contents, "message"):
     ...     print(extract_text(message))
+    ...
     Landscape Developers has been added as an answer contact for Ubuntu.
 
 Sample Person navigates to the team page to set it's preferred
 languages. They must add Spanish to the team's preferred languages.
 
-    >>> browser.open('http://launchpad.test/~landscape-developers')
+    >>> browser.open("http://launchpad.test/~landscape-developers";)
     >>> browser.title
     'Landscape Developers in Launchpad'
 
-    >>> browser.getLink('Set preferred languages').click()
+    >>> browser.getLink("Set preferred languages").click()
     >>> print(browser.title)
     Language preferences...
 
@@ -143,9 +150,9 @@ the team's preferred languages. We did not see the notification this
 time since the languages are set. Sample Person unselects English, and
 selects Spanish.
 
-    >>> browser.getControl('English', index=0).selected
+    >>> browser.getControl("English", index=0).selected
     True
 
-    >>> browser.getControl('English', index=0).selected = False
-    >>> browser.getControl('Spanish').selected = True
-    >>> browser.getControl('Save').click()
+    >>> browser.getControl("English", index=0).selected = False
+    >>> browser.getControl("Spanish").selected = True
+    >>> browser.getControl("Save").click()
diff --git a/lib/lp/answers/stories/question-answers-vhost.rst b/lib/lp/answers/stories/question-answers-vhost.rst
index b031bf9..2a71aa8 100644
--- a/lib/lp/answers/stories/question-answers-vhost.rst
+++ b/lib/lp/answers/stories/question-answers-vhost.rst
@@ -9,7 +9,7 @@ selected facet is the Answers facet.
 Product
 -------
 
-    >>> anon_browser.open('http://answers.launchpad.test/firefox')
+    >>> anon_browser.open("http://answers.launchpad.test/firefox";)
     >>> print(anon_browser.title)
     Questions : Mozilla Firefox
 
@@ -17,7 +17,7 @@ Product
 Distribution
 ------------
 
-    >>> anon_browser.open('http://answers.launchpad.test/ubuntu')
+    >>> anon_browser.open("http://answers.launchpad.test/ubuntu";)
     >>> print(anon_browser.title)
     Questions : Ubuntu
 
@@ -26,7 +26,8 @@ Distribution Source Package
 ---------------------------
 
     >>> anon_browser.open(
-    ...     'http://answers.launchpad.test/ubuntu/+source/mozilla-firefox')
+    ...     "http://answers.launchpad.test/ubuntu/+source/mozilla-firefox";
+    ... )
     >>> print(anon_browser.title)
     Questions : mozilla-firefox package : Ubuntu
 
@@ -34,8 +35,7 @@ Distribution Source Package
 ProjectGroup
 ------------
 
-    >>> anon_browser.open(
-    ...     'http://answers.launchpad.test/mozilla')
+    >>> anon_browser.open("http://answers.launchpad.test/mozilla";)
     >>> print(anon_browser.title)
     Questions : The Mozilla Project
 
@@ -43,6 +43,6 @@ ProjectGroup
 Person
 ------
 
-    >>> anon_browser.open('http://answers.launchpad.test/~name16')
+    >>> anon_browser.open("http://answers.launchpad.test/~name16";)
     >>> print(anon_browser.title)
     Questions : Foo Bar
diff --git a/lib/lp/answers/stories/question-browse-and-search.rst b/lib/lp/answers/stories/question-browse-and-search.rst
index 38e2787..90b15a7 100644
--- a/lib/lp/answers/stories/question-browse-and-search.rst
+++ b/lib/lp/answers/stories/question-browse-and-search.rst
@@ -7,7 +7,7 @@ searching features of the Answer Tracker.
 Make the test browser look like it's coming from an arbitrary South African
 IP address, since we'll use that later.
 
-    >>> browser.addHeader('X_FORWARDED_FOR', '196.36.161.227')
+    >>> browser.addHeader("X_FORWARDED_FOR", "196.36.161.227")
 
 
 When Nobody Uses the Answer Tracker
@@ -23,27 +23,28 @@ Kubuntu must enable answers to access questions.
     >>> from lp.app.enums import ServiceUsage
     >>> from lp.registry.interfaces.distribution import IDistributionSet
 
-    >>> login('admin@xxxxxxxxxxxxx')
-    >>> getUtility(IDistributionSet)['kubuntu'].answers_usage = (
-    ...     ServiceUsage.LAUNCHPAD)
+    >>> login("admin@xxxxxxxxxxxxx")
+    >>> getUtility(IDistributionSet)[
+    ...     "kubuntu"
+    ... ].answers_usage = ServiceUsage.LAUNCHPAD
     >>> transaction.commit()
     >>> logout()
 
-    >>> browser.open('http://launchpad.test/kubuntu')
-    >>> browser.getLink('Answers').click()
+    >>> browser.open("http://launchpad.test/kubuntu";)
+    >>> browser.getLink("Answers").click()
 
 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').decode_contents())
+    >>> 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.
 
-    >>> browser.open('http://launchpad.test/aaa')
-    >>> browser.getLink('Answers')
+    >>> browser.open("http://launchpad.test/aaa";)
+    >>> browser.getLink("Answers")
     Traceback (most recent call last):
      ...
     zope.testbrowser.browser.LinkNotFoundError
@@ -54,15 +55,16 @@ Browsing Questions
 He realises that support for Kubuntu is probably going on in the Ubuntu
 Answers page and goes there to check.
 
-    >>> browser.open('http://launchpad.test/ubuntu/+questions')
+    >>> browser.open("http://launchpad.test/ubuntu/+questions";)
     >>> print(browser.title)
     Questions : Ubuntu
 
 He sees a listing of the current questions posted on Ubuntu:
 
     >>> soup = find_main_content(browser.contents)
-    >>> for question in soup.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> for question in soup.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Continue playing after shutdown
     Play DVDs in Totem
     mailto: problem in webpage
@@ -73,26 +75,27 @@ None of the listed question titles quite match his problem. He sees that
 there is another page of questions, so he goes to the next page of
 results. There, he finds only one other question:
 
-    >>> browser.getLink('Next').click()
+    >>> browser.getLink("Next").click()
     >>> print(browser.title)
     Questions : Ubuntu
     >>> soup = find_main_content(browser.contents)
-    >>> for question in soup.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> for question in soup.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Installation failed
 
 This is the last results page, so the next and last links are greyed
 out.
 
-    >>> 'Next' in browser.contents
+    >>> "Next" in browser.contents
     True
-    >>> browser.getLink('Next')
+    >>> browser.getLink("Next")
     Traceback (most recent call last):
       ..
     zope.testbrowser.browser.LinkNotFoundError
-    >>> 'Last' in browser.contents
+    >>> "Last" in browser.contents
     True
-    >>> browser.getLink('Last')
+    >>> browser.getLink("Last")
     Traceback (most recent call last):
       ..
     zope.testbrowser.browser.LinkNotFoundError
@@ -100,20 +103,20 @@ out.
 He decides to go the first page. He remembered one question title that
 might have been remotely related to his problem.
 
-    >>> browser.getLink('First').click()
+    >>> browser.getLink("First").click()
 
 Since he is on the first page, the 'First' and 'Previous' links are
 greyed out:
 
-    >>> 'Previous' in browser.contents
+    >>> "Previous" in browser.contents
     True
-    >>> browser.getLink('Previous')
+    >>> browser.getLink("Previous")
     Traceback (most recent call last):
       ..
     zope.testbrowser.browser.LinkNotFoundError
-    >>> 'First' in browser.contents
+    >>> "First" in browser.contents
     True
-    >>> browser.getLink('First')
+    >>> browser.getLink("First")
     Traceback (most recent call last):
       ..
     zope.testbrowser.browser.LinkNotFoundError
@@ -123,22 +126,22 @@ description appears in a small pop-up:
 
     >>> import re
     >>> soup = find_main_content(browser.contents)
-    >>> question_link = soup.find('a', text=re.compile('Play DVDs'))
-    >>> print(question_link.find_parent('tr')['title'])
+    >>> question_link = soup.find("a", text=re.compile("Play DVDs"))
+    >>> print(question_link.find_parent("tr")["title"])
     How do you play DVDs in Totem..........?
 
-    >>> question_link = soup.find('a', text=re.compile('Slow system'))
-    >>> print(question_link.find_parent('tr')['title'])
+    >>> question_link = soup.find("a", text=re.compile("Slow system"))
+    >>> print(question_link.find_parent("tr")["title"])
     I get really poor hard drive performance.
 
 He clicks on the question title to obtain the question page where the
 details of the question are available.
 
-    >>> browser.getLink('Slow system').click()
+    >>> browser.getLink("Slow system").click()
     >>> print(browser.title)
     Question #7 : ...
     >>> soup = find_main_content(browser.contents)
-    >>> soup('div', 'report')
+    >>> soup("div", "report")
     [<div class="report"><p>I get really poor hard drive
     performance.</p></div>]
 
@@ -155,9 +158,9 @@ problem from someone on IRC. He is told to read question 9 on the
 Launchpad Answer Tracker. He visits the main page and enters '9'
 to jump to the question.
 
-    >>> browser.open('http://answers.launchpad.test/')
-    >>> browser.getControl(name='field.search_text').value = '9'
-    >>> browser.getControl('Find Answers').click()
+    >>> browser.open("http://answers.launchpad.test/";)
+    >>> browser.getControl(name="field.search_text").value = "9"
+    >>> browser.getControl("Find Answers").click()
     >>> from lp.services.helpers import backslashreplace
     >>> print(backslashreplace(browser.title))
     Question #9 : ...
@@ -168,9 +171,9 @@ get extensions to work. He copies the text ' #6 ' from the page
 and pastes it into the main page of the Answer Tracker to read
 the answer.
 
-    >>> browser.open('http://answers.launchpad.test/')
-    >>> browser.getControl(name='field.search_text').value = ' #6 '
-    >>> browser.getControl('Find Answers').click()
+    >>> browser.open("http://answers.launchpad.test/";)
+    >>> browser.getControl(name="field.search_text").value = " #6 "
+    >>> browser.getControl("Find Answers").click()
     >>> print(backslashreplace(browser.title))
     Question #6 : ...
 
@@ -179,13 +182,13 @@ Joe finds a reference to question 8 in a blog. He copies 'question 8'
 and pastes it into the text field on the Answer Tracker main page. He
 is shown search results instead of the question.
 
-    >>> browser.open('http://answers.launchpad.test/')
-    >>> browser.getControl(name='field.search_text').value = 'question 8'
-    >>> browser.getControl('Find Answers').click()
+    >>> browser.open("http://answers.launchpad.test/";)
+    >>> browser.getControl(name="field.search_text").value = "question 8"
+    >>> browser.getControl("Find Answers").click()
     >>> print(browser.title)
     Questions matching "question 8"
 
-    >>> print(find_main_content(browser.contents).find('p').decode_contents())
+    >>> print(find_main_content(browser.contents).find("p").decode_contents())
     There are no questions matching "question 8" with the requested statuses.
 
 
@@ -201,9 +204,9 @@ whenever he has Firefox open. Luckily for Average Joe, searching for
 similar questions is easy: on the question listing page, he just
 enters his search key and hit the 'Search' button.
 
-    >>> browser.open('http://launchpad.test/ubuntu/+questions')
-    >>> browser.getControl(name='field.search_text').value = 'firefox is slow'
-    >>> browser.getControl('Search', index=0).click()
+    >>> browser.open("http://launchpad.test/ubuntu/+questions";)
+    >>> browser.getControl(name="field.search_text").value = "firefox is slow"
+    >>> browser.getControl("Search", index=0).click()
 
 Unfortunately, the search doesn't return any similar questions:
 
@@ -222,32 +225,33 @@ selected. He adds 'Invalid' to the selection, and run his search again.
 
     >>> from lp.testing.pages import strip_label
 
-    >>> statuses = browser.getControl(name='field.status').displayValue
+    >>> statuses = browser.getControl(name="field.status").displayValue
     >>> [strip_label(status) for status in statuses]
     ['Open', 'Needs information', 'Answered', 'Solved']
-    >>> browser.getControl('Invalid').selected = True
-    >>> browser.getControl('Search', index=0).click()
+    >>> browser.getControl("Invalid").selected = True
+    >>> browser.getControl("Search", index=0).click()
 
 This time, the search returns one item.
 
     >>> soup = find_main_content(browser.contents)
-    >>> for question in soup.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> for question in soup.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Firefox is slow and consumes too much RAM
 
 He clicks on the link to read the question description.
 
-    >>> browser.getLink('Firefox is slow').click()
+    >>> browser.getLink("Firefox is slow").click()
     >>> print(browser.title)
     Question #3 : ...
 
 The user must choose at least one status when searching questions. An
 error is displayed when the user forgets to select a status.
 
-    >>> browser.open('http://launchpad.test/ubuntu/+questions')
-    >>> browser.getControl(name='field.status').displayValue = []
-    >>> browser.getControl('Search', index=0).click()
-    >>> messages = find_tags_by_class(browser.contents, 'message')
+    >>> browser.open("http://launchpad.test/ubuntu/+questions";)
+    >>> browser.getControl(name="field.status").displayValue = []
+    >>> browser.getControl("Search", index=0).click()
+    >>> messages = find_tags_by_class(browser.contents, "message")
     >>> print(messages[0].decode_contents())
     You must choose at least one status.
 
@@ -264,12 +268,12 @@ source package's question listing.
     # We should use goBack() here but can't because of bug #98372:
     # zope.testbrowser truncates document content after goBack().
     #>>> browser.goBack()
-    >>> browser.open('http://launchpad.test/ubuntu/+questions')
-    >>> browser.getLink('mozilla-firefox').click()
+    >>> browser.open("http://launchpad.test/ubuntu/+questions";)
+    >>> browser.getLink("mozilla-firefox").click()
     >>> browser.title
     'Questions : mozilla-firefox package : Ubuntu'
     >>> soup = find_main_content(browser.contents)
-    >>> print(soup.find('table', 'listing'))
+    >>> print(soup.find("table", "listing"))
     <table...
     ...mailto: problem in webpage...2006-07-20...
     ...Installation of Java Runtime Environment for Mozilla...2006-07-20...
@@ -279,12 +283,12 @@ Average Joe wants to see all questions but listed from the oldest to the
 newest. Again, he adds the 'Invalid' status to the selection and
 selects the 'oldest first' sort order.
 
-    >>> browser.getControl('Invalid').selected = True
-    >>> browser.getControl('oldest first').selected = True
-    >>> browser.getControl('Search', index=0).click()
+    >>> browser.getControl("Invalid").selected = True
+    >>> browser.getControl("oldest first").selected = True
+    >>> browser.getControl("Search", index=0).click()
 
     >>> soup = find_main_content(browser.contents)
-    >>> print(soup.find('table', 'listing'))
+    >>> print(soup.find("table", "listing"))
     <table...
     ...Firefox is slow and consumes too much RAM...2005-09-05...
     ...Installation of Java Runtime Environment for Mozilla...2006-07-20...
@@ -304,13 +308,14 @@ Open Questions
 Nice Guy likes helping others. He uses the 'Open' link to view the most
 recent questions on Mozilla Firefox.
 
-    >>> browser.open('http://launchpad.test/firefox/+questions')
-    >>> browser.getLink('Open').click()
+    >>> browser.open("http://launchpad.test/firefox/+questions";)
+    >>> browser.getLink("Open").click()
     >>> print(browser.title)
     Questions : Mozilla Firefox
-    >>> questions = find_tag_by_id(browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> questions = find_tag_by_id(browser.contents, "question-listing")
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     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
@@ -319,18 +324,19 @@ Note that the default sort order for this listing is
 'recently updated first' so that questions which received new information
 from the submitter shows up first:
 
-    >>> browser.getControl(name='field.sort').displayValue
+    >>> browser.getControl(name="field.sort").displayValue
     ['recently updated first']
 
 That listing is also searchable. Since he's has lots of experience
 dealing with plugins problems, he always start by a search for such
 problems:
 
-    >>> browser.getControl(name='field.search_text').value = 'plugin'
-    >>> browser.getControl('Search', index=0).click()
-    >>> questions = find_tag_by_id(browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> browser.getControl(name="field.search_text").value = "plugin"
+    >>> browser.getControl("Search", index=0).click()
+    >>> questions = find_tag_by_id(browser.contents, "question-listing")
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Problem showing the SVG demo on W3C site
 
 
@@ -342,16 +348,17 @@ A random user has a problem with firefox in Ubuntu. They use the
 similar problems. (This listing includes both 'Answered' and 'Solved'
 questions.)
 
-    >>> browser.open('http://launchpad.test/ubuntu/+questions')
-    >>> browser.getLink('Answered').click()
+    >>> browser.open("http://launchpad.test/ubuntu/+questions";)
+    >>> browser.getLink("Answered").click()
     >>> print(browser.title)
     Questions : Ubuntu
-    >>> statuses = browser.getControl(name='field.status').displayValue
+    >>> statuses = browser.getControl(name="field.status").displayValue
     >>> [strip_label(status) for status in statuses]
     ['Answered', 'Solved']
-    >>> questions = find_tag_by_id(browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> questions = find_tag_by_id(browser.contents, "question-listing")
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Play DVDs in Totem
     mailto: problem in webpage
     Installation of Java Runtime Environment for Mozilla
@@ -359,8 +366,8 @@ questions.)
 This report is also searchable. They're having a problem with Evolution, so
 they enter 'Evolution' as a keyword and hit the search button.
 
-    >>> browser.getControl(name='field.search_text').value = 'Evolution'
-    >>> browser.getControl('Search', index=0).click()
+    >>> browser.getControl(name="field.search_text").value = "Evolution"
+    >>> browser.getControl("Search", index=0).click()
 
     >>> search_summary = find_main_content(browser.contents)
     >>> print(search_summary)
@@ -379,48 +386,56 @@ to list all the questions they ever made about that package.
 They need to login to access that page:
 
     >>> anon_browser.open(
-    ...     'http://launchpad.test/ubuntu/+source/mozilla-firefox/'
-    ...     '+questions')
-    >>> anon_browser.getLink('My questions').click()
+    ...     "http://launchpad.test/ubuntu/+source/mozilla-firefox/";
+    ...     "+questions"
+    ... )
+    >>> anon_browser.getLink("My questions").click()
     Traceback (most recent call last):
       ...
     zope.security.interfaces.Unauthorized: ...
 
     >>> sample_person_browser = setupBrowser(
-    ...     auth='Basic test@xxxxxxxxxxxxx:test')
+    ...     auth="Basic test@xxxxxxxxxxxxx:test"
+    ... )
     >>> sample_person_browser.open(
-    ...     'http://launchpad.test/ubuntu/+source/mozilla-firefox/'
-    ...     '+questions')
-    >>> sample_person_browser.getLink('My questions').click()
+    ...     "http://launchpad.test/ubuntu/+source/mozilla-firefox/";
+    ...     "+questions"
+    ... )
+    >>> sample_person_browser.getLink("My questions").click()
     >>> print(repr(sample_person_browser.title))
     'Questions you asked about mozilla-firefox in Ubuntu : Questions :
     mozilla-firefox package : Ubuntu'
     >>> questions = find_tag_by_id(
-    ...     sample_person_browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    ...     sample_person_browser.contents, "question-listing"
+    ... )
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     mailto: problem in webpage
     Installation of Java Runtime Environment for Mozilla
 
 Their problem was about integrating their email client in firefox, so they
 enter 'email client in firefox'
 
-    >>> sample_person_browser.getControl(name='field.search_text').value = (
-    ...     'email client in firefox')
+    >>> sample_person_browser.getControl(
+    ...     name="field.search_text"
+    ... ).value = "email client in firefox"
 
 They also remember that their question was answered, so they unselect the
 other statuses and hit the search button.
 
-    >>> sample_person_browser.getControl('Open').selected = False
-    >>> sample_person_browser.getControl('Invalid').selected = False
-    >>> sample_person_browser.getControl('Search', index=0).click()
+    >>> sample_person_browser.getControl("Open").selected = False
+    >>> sample_person_browser.getControl("Invalid").selected = False
+    >>> sample_person_browser.getControl("Search", index=0).click()
 
 The exact question they were searching for is displayed!
 
     >>> questions = find_tag_by_id(
-    ...     sample_person_browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    ...     sample_person_browser.contents, "question-listing"
+    ... )
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     mailto: problem in webpage
 
 If the user didn't make any questions on the product, a message
@@ -429,17 +444,22 @@ informing them of this fact is displayed.
 gnomebaker must enable answers to access questions.
 
     >>> from lp.registry.interfaces.product import IProductSet
-    >>> login('admin@xxxxxxxxxxxxx')
-    >>> getUtility(IProductSet)['gnomebaker'].answers_usage = (
-    ...     ServiceUsage.LAUNCHPAD)
+    >>> login("admin@xxxxxxxxxxxxx")
+    >>> getUtility(IProductSet)[
+    ...     "gnomebaker"
+    ... ].answers_usage = ServiceUsage.LAUNCHPAD
     >>> transaction.commit()
     >>> logout()
 
     >>> sample_person_browser.open(
-    ...     'http://launchpad.test/gnomebaker/+questions')
-    >>> sample_person_browser.getLink('My questions').click()
-    >>> print(find_main_content(
-    ...     sample_person_browser.contents).find('p').decode_contents())
+    ...     "http://launchpad.test/gnomebaker/+questions";
+    ... )
+    >>> sample_person_browser.getLink("My questions").click()
+    >>> print(
+    ...     find_main_content(sample_person_browser.contents)
+    ...     .find("p")
+    ...     .decode_contents()
+    ... )
     You didn't ask any questions about gnomebaker.
 
 
@@ -454,29 +474,33 @@ information and that are now back in the 'Open' state.
 
 They need to login to access that page:
 
-    >>> anon_browser.open('http://launchpad.test/distros/ubuntu/+questions')
-    >>> anon_browser.getLink('Need attention').click()
+    >>> anon_browser.open("http://launchpad.test/distros/ubuntu/+questions";)
+    >>> anon_browser.getLink("Need attention").click()
     Traceback (most recent call last):
       ...
     zope.security.interfaces.Unauthorized: ...
 
     >>> sample_person_browser.open(
-    ...     'http://launchpad.test/distros/ubuntu/+questions')
-    >>> sample_person_browser.getLink('Need attention').click()
+    ...     "http://launchpad.test/distros/ubuntu/+questions";
+    ... )
+    >>> sample_person_browser.getLink("Need attention").click()
     >>> print(sample_person_browser.title)
     Questions needing your attention for Ubuntu : Questions : Ubuntu
     >>> questions = find_tag_by_id(
-    ...     sample_person_browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    ...     sample_person_browser.contents, "question-listing"
+    ... )
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Play DVDs in Totem
     Installation of Java Runtime Environment for Mozilla
 
 Like all other report, this one is searchable:
 
     >>> sample_person_browser.getControl(
-    ...     name='field.search_text').value = 'evolution'
-    >>> sample_person_browser.getControl('Search', index=0).click()
+    ...     name="field.search_text"
+    ... ).value = "evolution"
+    >>> sample_person_browser.getControl("Search", index=0).click()
     >>> print(sample_person_browser.title)
     Questions matching "evolution" needing your attention for Ubuntu :
     Questions : Ubuntu
@@ -490,10 +514,14 @@ If there is no questions needing the user's attention, a message
 informing them of this fact is displayed.
 
     >>> sample_person_browser.open(
-    ...    'http://launchpad.test/products/gnomebaker/+questions')
-    >>> sample_person_browser.getLink('Need attention').click()
-    >>> print(find_main_content(
-    ...     sample_person_browser.contents).find('p').decode_contents())
+    ...     "http://launchpad.test/products/gnomebaker/+questions";
+    ... )
+    >>> sample_person_browser.getLink("Need attention").click()
+    >>> print(
+    ...     find_main_content(sample_person_browser.contents)
+    ...     .find("p")
+    ...     .decode_contents()
+    ... )
     No questions need your attention for gnomebaker.
 
 
@@ -508,14 +536,15 @@ that the person was involved with. This includes questions that
 the person asked, answered, is assigned to, is subscribed to, or
 commented on.
 
-    >>> browser.open('http://launchpad.test/~name16')
-    >>> browser.getLink('Answers').click()
+    >>> browser.open("http://launchpad.test/~name16";)
+    >>> browser.getLink("Answers").click()
     >>> print(browser.title)
     Questions : Foo Bar
 
-    >>> questions = find_tag_by_id(browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> questions = find_tag_by_id(browser.contents, "question-listing")
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Continue playing after shutdown
     Play DVDs in Totem
     mailto: problem in webpage
@@ -524,14 +553,15 @@ commented on.
 
 That listing is batched when there are many questions:
 
-    >>> browser.getLink('Next')
+    >>> browser.getLink("Next")
     <Link...>
 
 The listing contains a 'In' column that shows the context where the
 questions was made.
 
-    >>> for question in questions.find_all('td', 'question-target'):
-    ...     print(question.find('a').decode_contents())
+    >>> for question in questions.find_all("td", "question-target"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Ubuntu
     Ubuntu
     mozilla-firefox in Ubuntu
@@ -540,7 +570,7 @@ questions was made.
 
 These contexts are links to the context question listing.
 
-    >>> browser.getLink('mozilla-firefox in Ubuntu').click()
+    >>> browser.getLink("mozilla-firefox in Ubuntu").click()
     >>> print(repr(browser.title))
     'Questions : mozilla-firefox package : Ubuntu'
 
@@ -548,14 +578,17 @@ The listing is searchable and can restrict also the list of displayed
 questions to a particular status:
 
     # goBack() doesn't work.
-    >>> browser.open('http://launchpad.test/~name16/+questions')
-    >>> browser.getControl(name='field.search_text').value = 'Firefox'
-    >>> browser.getControl(name='field.status').displayValue = [
-    ...     'Solved', 'Invalid']
-    >>> browser.getControl('Search', index=0).click()
-    >>> questions = find_tag_by_id(browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> browser.open("http://launchpad.test/~name16/+questions";)
+    >>> browser.getControl(name="field.search_text").value = "Firefox"
+    >>> browser.getControl(name="field.status").displayValue = [
+    ...     "Solved",
+    ...     "Invalid",
+    ... ]
+    >>> browser.getControl("Search", index=0).click()
+    >>> questions = find_tag_by_id(browser.contents, "question-listing")
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Firefox is slow and consumes too much RAM
     mailto: problem in webpage
 
@@ -569,10 +602,10 @@ Assigned
 The assigned report only lists the questions to which the person is
 assigned.
 
-    >>> browser.getLink('Assigned').click()
+    >>> browser.getLink("Assigned").click()
     >>> print(browser.title)
     Questions for Foo Bar : Questions : Foo Bar
-    >>> print(find_main_content(browser.contents).find('p').decode_contents())
+    >>> print(find_main_content(browser.contents).find("p").decode_contents())
     No questions assigned to Foo Bar found with the requested statuses.
 
 
@@ -582,12 +615,13 @@ Answered
 The 'Answered' link displays all the questions where the person is the
 answerer.
 
-    >>> browser.getLink('Answered').click()
+    >>> browser.getLink("Answered").click()
     >>> print(browser.title)
     Questions for Foo Bar : Questions : Foo Bar
-    >>> questions = find_tag_by_id(browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> questions = find_tag_by_id(browser.contents, "question-listing")
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     mailto: problem in webpage
 
 
@@ -597,12 +631,13 @@ Commented
 The report available under the 'Commented' link displays all the
 questions commented on by the person.
 
-    >>> browser.getLink('Commented').click()
+    >>> browser.getLink("Commented").click()
     >>> print(browser.title)
     Questions for Foo Bar : Questions : Foo Bar
-    >>> questions = find_tag_by_id(browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> questions = find_tag_by_id(browser.contents, "question-listing")
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Continue playing after shutdown
     Play DVDs in Totem
     mailto: problem in webpage
@@ -616,12 +651,13 @@ Asked
 The 'Asked' link displays a listing containing all the questions
 asked by the person.
 
-    >>> browser.getLink('Asked').click()
+    >>> browser.getLink("Asked").click()
     >>> print(browser.title)
     Questions for Foo Bar : Questions : Foo Bar
-    >>> questions = find_tag_by_id(browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> questions = find_tag_by_id(browser.contents, "question-listing")
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Slow system
     Firefox loses focus and gets stuck
 
@@ -632,12 +668,13 @@ Need attention
 The 'Need attention' link displays all the questions that need
 the attention of that person.
 
-    >>> browser.getLink('Need attention').click()
+    >>> browser.getLink("Need attention").click()
     >>> print(browser.title)
     Questions for Foo Bar : Questions : Foo Bar
-    >>> questions = find_tag_by_id(browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> questions = find_tag_by_id(browser.contents, "question-listing")
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Continue playing after shutdown
     Slow system
 
@@ -648,12 +685,13 @@ Subscribed
 Foo Bar can find all the questions to which they are subscribed by
 visiting the 'Subscribed' link in the 'Answers' facet.
 
-    >>> browser.getLink('Subscribed').click()
+    >>> browser.getLink("Subscribed").click()
     >>> print(browser.title)
     Questions for Foo Bar : Questions : Foo Bar
-    >>> questions = find_tag_by_id(browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> questions = find_tag_by_id(browser.contents, "question-listing")
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Slow system
 
 
@@ -663,8 +701,8 @@ Browsing and Searching Questions in a ProjectGroup
 When going to the Answers facet of a project, a listing of all the
 questions filed against any of the project's products is displayed.
 
-    >>> browser.open('http://launchpad.test/mozilla')
-    >>> browser.getLink('Answers').click()
+    >>> browser.open("http://launchpad.test/mozilla";)
+    >>> browser.getLink("Answers").click()
     >>> print(browser.title)
     Questions : The Mozilla Project
 
@@ -672,13 +710,20 @@ The results are displayed in a format similar to the Person reports:
 there is an 'In' column displaying where the questions were filed.
 
     >>> def print_questions_with_target(contents):
-    ...     questions = find_tag_by_id(contents, 'question-listing')
-    ...     for question in questions.tbody.find_all('tr'):
-    ...         question_title = question.find(
-    ...             'td', 'questionTITLE').find('a').decode_contents()
-    ...         question_target = question.find(
-    ...             'td', 'question-target').find('a').decode_contents()
+    ...     questions = find_tag_by_id(contents, "question-listing")
+    ...     for question in questions.tbody.find_all("tr"):
+    ...         question_title = (
+    ...             question.find("td", "questionTITLE")
+    ...             .find("a")
+    ...             .decode_contents()
+    ...         )
+    ...         question_target = (
+    ...             question.find("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
     Firefox loses focus and gets stuck  Mozilla Firefox
@@ -687,32 +732,33 @@ there is an 'In' column displaying where the questions were filed.
 
 That listing is searchable:
 
-    >>> browser.getControl(name='field.search_text').value = 'SVG'
-    >>> browser.getControl('Search', index=0).click()
+    >>> browser.getControl(name="field.search_text").value = "SVG"
+    >>> browser.getControl("Search", index=0).click()
 
-    >>> questions = find_tag_by_id(browser.contents, 'question-listing')
-    >>> for question in questions.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> questions = find_tag_by_id(browser.contents, "question-listing")
+    >>> for question in questions.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Problem showing the SVG demo on W3C site
 
 The same standard reports than on regular QuestionTarget are available:
 
-    >>> browser.getLink('Open').click()
+    >>> browser.getLink("Open").click()
     >>> print(browser.title)
     Questions : The Mozilla Project
 
-    >>> browser.getLink('Answered').click()
+    >>> browser.getLink("Answered").click()
     >>> print(browser.title)
     Questions : The Mozilla Project
 
     # The next two reports are only available to a logged-in user.
-    >>> user_browser.open('http://launchpad.test/mozilla/+questions')
-    >>> user_browser.getLink('My questions').click()
+    >>> user_browser.open("http://launchpad.test/mozilla/+questions";)
+    >>> user_browser.getLink("My questions").click()
     >>> print(user_browser.title)
     Questions you asked about The Mozilla Project : Questions : The Mozilla
     Project
 
-    >>> user_browser.getLink('Need attention').click()
+    >>> user_browser.getLink("Need attention").click()
     >>> print(user_browser.title)
     Questions needing your attention for The Mozilla Project : Questions : The
     Mozilla Project
@@ -724,9 +770,9 @@ Searching All Questions
 It is possible from the Answer Tracker front page to search among all
 questions ever filed on Launchpad.
 
-    >>> browser.open('http://answers.launchpad.test/')
-    >>> browser.getControl(name='field.search_text').value = 'firefox'
-    >>> browser.getControl('Find Answers').click()
+    >>> browser.open("http://answers.launchpad.test/";)
+    >>> browser.getControl(name="field.search_text").value = "firefox"
+    >>> browser.getControl("Find Answers").click()
 
     >>> print(browser.title)
     Questions matching "firefox"
@@ -746,25 +792,24 @@ there is an 'In' column displaying where the questions were filed.
 
 Only the default set of statuses is searched:
 
-    >>> statuses = browser.getControl(name='field.status').displayValue
+    >>> statuses = browser.getControl(name="field.status").displayValue
     >>> [strip_label(status) for status in statuses]
     ['Open', 'Needs information', 'Answered', 'Solved']
 
 When no results are found, a message informs the user of this fact:
 
-    >>> browser.getControl(name='field.status').displayValue = ['Expired']
-    >>> browser.getControl('Search', index=0).click()
+    >>> browser.getControl(name="field.status").displayValue = ["Expired"]
+    >>> browser.getControl("Search", index=0).click()
 
-    >>> print(find_main_content(
-    ...     browser.contents).find('p').decode_contents())
+    >>> print(find_main_content(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
 display all questions asked in Launchpad with the selected statuses.
 
-    >>> browser.getControl(name='field.status').displayValue = ['Open']
-    >>> browser.getControl(name='field.search_text').value = ''
-    >>> browser.getControl('Search', index=0).click()
+    >>> browser.getControl(name="field.status").displayValue = ["Open"]
+    >>> browser.getControl(name="field.search_text").value = ""
+    >>> browser.getControl("Search", index=0).click()
 
     >>> print_questions_with_target(browser.contents)
     Continue playing after shutdown             Ubuntu
@@ -783,58 +828,60 @@ a distribution, product or project group.
 
 They must enter the project's name in the text field:
 
-    >>> anon_browser.open('http://answers.launchpad.test')
-    >>> anon_browser.getControl('One project').selected = True
-    >>> anon_browser.getControl('Find Answers').click()
+    >>> anon_browser.open("http://answers.launchpad.test";)
+    >>> anon_browser.getControl("One project").selected = True
+    >>> anon_browser.getControl("Find Answers").click()
 
-    >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
+    >>> for message in find_tags_by_class(anon_browser.contents, "message"):
     ...     print(message.decode_contents())
+    ...
     Please enter a project name
 
 Entering an invalid project also displays an error message:
 
-    >>> anon_browser.getControl(name='field.scope.target').value = 'invalid'
-    >>> anon_browser.getControl('Find Answers').click()
+    >>> anon_browser.getControl(name="field.scope.target").value = "invalid"
+    >>> anon_browser.getControl("Find Answers").click()
 
-    >>> for message in find_tags_by_class(anon_browser.contents, 'message'):
+    >>> for message in find_tags_by_class(anon_browser.contents, "message"):
     ...     print(message.decode_contents())
+    ...
     There is no project named 'invalid' registered in Launchpad
 
 If the browser supports javascript, there is a 'Choose' link available
 to help the user find an existing project. Since the test browser does
 not support javascript, it is turned into a "Find" link to the /questions.
 
-    >>> find_link = anon_browser.getLink('Find')
+    >>> find_link = anon_browser.getLink("Find")
     >>> print(find_link.url)
     http://answers.launchpad.test/questions...
 
 
 The form field can be filled manually without using the ajax widget.
 
-    >>> anon_browser.open('http://answers.launchpad.test')
-    >>> anon_browser.getControl(name='field.search_text').value = 'plugins'
-    >>> anon_browser.getControl('One project').selected = True
-    >>> anon_browser.getControl(name='field.scope.target').value = 'mozilla'
-    >>> anon_browser.getControl('Find Answers').click()
+    >>> anon_browser.open("http://answers.launchpad.test";)
+    >>> anon_browser.getControl(name="field.search_text").value = "plugins"
+    >>> anon_browser.getControl("One project").selected = True
+    >>> anon_browser.getControl(name="field.scope.target").value = "mozilla"
+    >>> anon_browser.getControl("Find Answers").click()
     >>> print(anon_browser.title)
     Questions : The Mozilla Project
 
 This works also with distributions:
 
-    >>> anon_browser.open('http://answers.launchpad.test')
-    >>> anon_browser.getControl(name='field.search_text').value = 'firefox'
-    >>> anon_browser.getControl('One project').selected = True
-    >>> anon_browser.getControl(name='field.scope.target').value = 'ubuntu'
-    >>> anon_browser.getControl('Find Answers').click()
+    >>> anon_browser.open("http://answers.launchpad.test";)
+    >>> anon_browser.getControl(name="field.search_text").value = "firefox"
+    >>> anon_browser.getControl("One project").selected = True
+    >>> anon_browser.getControl(name="field.scope.target").value = "ubuntu"
+    >>> anon_browser.getControl("Find Answers").click()
     >>> print(anon_browser.title)
     Questions : Ubuntu
 
 And also with products:
 
-    >>> anon_browser.open('http://answers.launchpad.test')
-    >>> anon_browser.getControl(name='field.search_text').value = 'plugins'
-    >>> anon_browser.getControl('One project').selected = True
-    >>> anon_browser.getControl(name='field.scope.target').value = 'firefox'
-    >>> anon_browser.getControl('Find Answers').click()
+    >>> anon_browser.open("http://answers.launchpad.test";)
+    >>> anon_browser.getControl(name="field.search_text").value = "plugins"
+    >>> anon_browser.getControl("One project").selected = True
+    >>> anon_browser.getControl(name="field.scope.target").value = "firefox"
+    >>> anon_browser.getControl("Find Answers").click()
     >>> print(anon_browser.title)
     Questions : Mozilla Firefox
diff --git a/lib/lp/answers/stories/question-compatibility-urls.rst b/lib/lp/answers/stories/question-compatibility-urls.rst
index 25572d9..bb886b0 100644
--- a/lib/lp/answers/stories/question-compatibility-urls.rst
+++ b/lib/lp/answers/stories/question-compatibility-urls.rst
@@ -8,74 +8,74 @@ terminology. We provide redirect from the old names to the new ones.
 Answer Contact Page
 -------------------
 
-    >>> user_browser.open('http://launchpad.test/firefox/+support-contact')
+    >>> user_browser.open("http://launchpad.test/firefox/+support-contact";)
     >>> print(user_browser.url)
     http://answers.launchpad.test/firefox/+answer-contact
 
 Add Question Page
 -----------------
 
-    >>> user_browser.open('http://launchpad.test/firefox/+addticket')
+    >>> user_browser.open("http://launchpad.test/firefox/+addticket";)
     >>> print(user_browser.url)
     http://answers.launchpad.test/firefox/+addquestion
 
-    >>> user_browser.open('http://launchpad.test/mozilla/+addticket')
+    >>> user_browser.open("http://launchpad.test/mozilla/+addticket";)
     >>> print(user_browser.url)
     http://answers.launchpad.test/mozilla/+addquestion
 
 My Questions Page
 -----------------
 
-    >>> user_browser.open('http://launchpad.test/firefox/+mytickets')
+    >>> user_browser.open("http://launchpad.test/firefox/+mytickets";)
     >>> print(user_browser.url)
     http://answers.launchpad.test/firefox/+myquestions
 
 Questions Listing
 -----------------
 
-    >>> browser.open('http://launchpad.test/firefox/+tickets')
+    >>> browser.open("http://launchpad.test/firefox/+tickets";)
     >>> print(browser.url)
     http://answers.launchpad.test/firefox/+questions
 
 Question Page
 -------------
 
-    >>> browser.open('http://launchpad.test/firefox/+ticket/1')
+    >>> browser.open("http://launchpad.test/firefox/+ticket/1";)
     >>> print(browser.url)
     http://answers.launchpad.test/firefox/+question/1
 
-    >>> browser.open('http://api.launchpad.test/devel/firefox/+ticket/1')
+    >>> browser.open("http://api.launchpad.test/devel/firefox/+ticket/1";)
     >>> print(browser.url)
     http://api.launchpad.test/devel/firefox/+question/1
 
 Person Questions Listing
 ------------------------
 
-    >>> browser.open('http://launchpad.test/~name12/+tickets')
+    >>> browser.open("http://launchpad.test/~name12/+tickets";)
     >>> print(browser.url)
     http://answers.launchpad.test/~name12/+questions
 
-    >>> browser.open('http://launchpad.test/~name12/+answeredtickets')
+    >>> browser.open("http://launchpad.test/~name12/+answeredtickets";)
     >>> print(browser.url)
     http://answers.launchpad.test/~name12/+answeredquestions
 
-    >>> browser.open('http://launchpad.test/~name12/+assignedtickets')
+    >>> browser.open("http://launchpad.test/~name12/+assignedtickets";)
     >>> print(browser.url)
     http://answers.launchpad.test/~name12/+assignedquestions
 
-    >>> browser.open('http://launchpad.test/~name12/+commentedtickets')
+    >>> browser.open("http://launchpad.test/~name12/+commentedtickets";)
     >>> print(browser.url)
     http://answers.launchpad.test/~name12/+commentedquestions
 
-    >>> browser.open('http://launchpad.test/~name12/+createdtickets')
+    >>> browser.open("http://launchpad.test/~name12/+createdtickets";)
     >>> print(browser.url)
     http://answers.launchpad.test/~name12/+createdquestions
 
-    >>> browser.open('http://launchpad.test/~name12/+needattentiontickets')
+    >>> browser.open("http://launchpad.test/~name12/+needattentiontickets";)
     >>> print(browser.url)
     http://answers.launchpad.test/~name12/+needattentionquestions
 
-    >>> browser.open('http://launchpad.test/~name12/+subscribedtickets')
+    >>> browser.open("http://launchpad.test/~name12/+subscribedtickets";)
     >>> print(browser.url)
     http://answers.launchpad.test/~name12/+subscribedquestions
 
@@ -86,7 +86,7 @@ Unsupported questions
 The Unsupported View is irrelevant. The question search page provides
 links to the +by-language pages that have unsolved questions.
 
-    >>> browser.open('http://launchpad.test/ubuntu/+unsupported')
+    >>> browser.open("http://launchpad.test/ubuntu/+unsupported";)
     >>> print(browser.url)
     http://answers.launchpad.test/ubuntu/+questions
 
@@ -101,25 +101,27 @@ became 'NEEDSINFO'. The old values are used in incoming links,
 bookmarks, and the firefox-launchpad plugin; the old values must
 continue to work.
 
-    >>> old_sort = 'by+status'
-    >>> old_status = 'Needs+information'
-    >>> url = ('http://answers.launchpad.test/ubuntu/+questions'
-    ...        '?field.sort=%s&field.sort-empty-marker=1'
-    ...        '&field.language=en&field.language-empty-marker=1'
-    ...        '&field.search_text=&field.actions.search=Search'
-    ...        '&field.status=%s&field.status-empty-marker=1')
+    >>> old_sort = "by+status"
+    >>> old_status = "Needs+information"
+    >>> url = (
+    ...     "http://answers.launchpad.test/ubuntu/+questions";
+    ...     "?field.sort=%s&field.sort-empty-marker=1"
+    ...     "&field.language=en&field.language-empty-marker=1"
+    ...     "&field.search_text=&field.actions.search=Search"
+    ...     "&field.status=%s&field.status-empty-marker=1"
+    ... )
     >>> browser.open(url % (old_sort, old_status))
     >>> print(browser.title)
     Questions : Ubuntu
-    >>> browser.getControl(name='field.sort').displayValue
+    >>> browser.getControl(name="field.sort").displayValue
     ['by status']
 
 Using the new values returns the same page.
 
-    >>> new_sort = 'STATUS'
-    >>> new_status = 'NEEDSINFO'
+    >>> new_sort = "STATUS"
+    >>> new_status = "NEEDSINFO"
     >>> browser.open(url % (new_sort, new_status))
     >>> print(browser.title)
     Questions : Ubuntu
-    >>> browser.getControl(name='field.sort').displayValue
+    >>> browser.getControl(name="field.sort").displayValue
     ['by status']
diff --git a/lib/lp/answers/stories/question-edit.rst b/lib/lp/answers/stories/question-edit.rst
index 8aeed0d..d1db512 100644
--- a/lib/lp/answers/stories/question-edit.rst
+++ b/lib/lp/answers/stories/question-edit.rst
@@ -6,32 +6,33 @@ Question' menu item. You need to be logged in to see the edit form, and
 only the question creator or an owner of the question target can change the
 title and description.
 
-    >>> anon_browser.open('http://launchpad.test/firefox/+question/2')
-    >>> anon_browser.getLink('Edit question').click()
+    >>> anon_browser.open("http://launchpad.test/firefox/+question/2";)
+    >>> anon_browser.getLink("Edit question").click()
     Traceback (most recent call last):
     ...
     zope.security.interfaces.Unauthorized: ...
 
-    >>> test_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
-    >>> test_browser.open('http://launchpad.test/firefox/+question/2')
-    >>> test_browser.getLink('Edit question').click()
+    >>> test_browser = setupBrowser(auth="Basic test@xxxxxxxxxxxxx:test")
+    >>> test_browser.open("http://launchpad.test/firefox/+question/2";)
+    >>> test_browser.getLink("Edit question").click()
     >>> print(test_browser.url)
     http://answers.launchpad.test/firefox/+question/2/+edit
 
 There is a cancel link should the user decide otherwise:
 
-    >>> print(test_browser.getLink('Cancel').url)
+    >>> print(test_browser.getLink("Cancel").url)
     http://answers.launchpad.test/firefox/+question/2
 
 When we post the form, we should be redirected back to the question page.
 
     >>> description = (
-    ...   "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")
-    >>> test_browser.getControl('Description').value = description
+    ...     "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"
+    ... )
+    >>> test_browser.getControl("Description").value = description
     >>> summary = "Problem showing the SVG demo on W3C web site"
-    >>> test_browser.getControl('Summary').value = summary
-    >>> test_browser.getControl('Save Changes').click()
+    >>> test_browser.getControl("Summary").value = summary
+    >>> test_browser.getControl("Save Changes").click()
 
     >>> print(test_browser.url)
     http://answers.launchpad.test/firefox/+question/2
@@ -39,25 +40,29 @@ 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').decode_contents().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').decode_contents())
+    >>> 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
 'Invalid' questions:
 
     >>> def print_question_status(browser):
-    ...     print(extract_text(
-    ...         find_tag_by_id(browser.contents, 'question-status')))
+    ...     print(
+    ...         extract_text(
+    ...             find_tag_by_id(browser.contents, "question-status")
+    ...         )
+    ...     )
+    ...
 
-    >>> test_browser.open('http://launchpad.test/ubuntu/+question/3')
+    >>> test_browser.open("http://launchpad.test/ubuntu/+question/3";)
     >>> print_question_status(test_browser)
     Status: Invalid
 
-    >>> test_browser.getLink('Edit question')
+    >>> test_browser.getLink("Edit question")
     <Link...>
 
 
@@ -68,19 +73,21 @@ Distribution questions can have a source package associated with them.
 Any logged in user can change the question source package on the
 'Edit Question' page.
 
-    >>> user_browser.open('http://launchpad.test/ubuntu/+question/5')
-    >>> user_browser.getLink('Edit question').click()
+    >>> user_browser.open("http://launchpad.test/ubuntu/+question/5";)
+    >>> user_browser.getLink("Edit question").click()
     >>> user_browser.getControl(
-    ...     name='field.target.package').value = 'linux-source-2.6.15'
-    >>> user_browser.getControl('Save Changes').click()
+    ...     name="field.target.package"
+    ... ).value = "linux-source-2.6.15"
+    >>> user_browser.getControl("Save Changes").click()
 
 Product questions ignore sourcepackage information if it is submitted:
 
-    >>> user_browser.open('http://launchpad.test/firefox/+question/2')
-    >>> user_browser.getLink('Edit question').click()
+    >>> user_browser.open("http://launchpad.test/firefox/+question/2";)
+    >>> user_browser.getLink("Edit question").click()
     >>> user_browser.getControl(
-    ...     name='field.target.package').value = 'linux-source-2.6.15'
-    >>> user_browser.getControl('Save Changes').click()
+    ...     name="field.target.package"
+    ... ).value = "linux-source-2.6.15"
+    >>> user_browser.getControl("Save Changes").click()
 
 
 Changing Other Metadata
@@ -91,28 +98,29 @@ distribution owner) can also change the question Assignee, and
 edit the Status Whiteboard using the 'Edit Question' page.
 
     >>> browser.addHeader(
-    ...   'Authorization', 'Basic jeff.waugh@xxxxxxxxxxxxxxx:test')
-    >>> browser.open('http://localhost/ubuntu/+question/5')
-    >>> browser.getLink('Edit question').click()
+    ...     "Authorization", "Basic jeff.waugh@xxxxxxxxxxxxxxx:test"
+    ... )
+    >>> browser.open("http://localhost/ubuntu/+question/5";)
+    >>> browser.getLink("Edit question").click()
 
-    >>> browser.getControl('Assignee').value = 'name16'
-    >>> browser.getControl('Status Whiteboard').value = 'Some note'
-    >>> browser.getControl('Save Changes').click()
+    >>> browser.getControl("Assignee").value = "name16"
+    >>> browser.getControl("Status Whiteboard").value = "Some note"
+    >>> browser.getControl("Save Changes").click()
 
     >>> soup = find_main_content(browser.contents)
-    >>> print(extract_text(find_tag_by_id(soup, 'question-whiteboard')))
+    >>> print(extract_text(find_tag_by_id(soup, "question-whiteboard")))
     Whiteboard: Some note
-    >>> portlet_details = find_tag_by_id(browser.contents, 'portlet-details')
+    >>> portlet_details = find_tag_by_id(browser.contents, "portlet-details")
 
 These fields cannot be modified by a non-privileged user:
 
-    >>> user_browser.open('http://localhost/ubuntu/+question/5')
-    >>> user_browser.getLink('Edit question').click()
-    >>> user_browser.getControl('Assignee')
+    >>> user_browser.open("http://localhost/ubuntu/+question/5";)
+    >>> user_browser.getLink("Edit question").click()
+    >>> user_browser.getControl("Assignee")
     Traceback (most recent call last):
     ...
     LookupError...
-    >>> user_browser.getControl('Status Whiteboard')
+    >>> user_browser.getControl("Status Whiteboard")
     Traceback (most recent call last):
     ...
     LookupError...
diff --git a/lib/lp/answers/stories/question-message.rst b/lib/lp/answers/stories/question-message.rst
index 45cd8f3..c6ca281 100644
--- a/lib/lp/answers/stories/question-message.rst
+++ b/lib/lp/answers/stories/question-message.rst
@@ -11,11 +11,11 @@ Let's have an authenticated user create a message in the style of
 an email post to examine the markup rules. This message contains a
 quoted passage, and a signature with an email address in it.
 
-    >>> user_browser.open('http://answers.launchpad.test/ubuntu/+question/11')
+    >>> user_browser.open("http://answers.launchpad.test/ubuntu/+question/11";)
     >>> print(user_browser.title)
     Question #11 : ...
 
-    >>> user_browser.getControl('Message').value = (
+    >>> user_browser.getControl("Message").value = (
     ...     "Top quoting is bad netiquette.\n"
     ...     "The leading text will be displayed\n"
     ...     "normally--no markup to hide it from view.\n"
@@ -28,8 +28,9 @@ quoted passage, and a signature with an email address in it.
     ...     "--\n"
     ...     "______________________\n"
     ...     "human@xxxxxxxxxxxxx\n"
-    ...     "Witty signatures rock!\n")
-    >>> user_browser.getControl('Add Information Request').click()
+    ...     "Witty signatures rock!\n"
+    ... )
+    >>> user_browser.getControl("Add Information Request").click()
 
 
 Email addresses are only shown to authenticated users
@@ -40,9 +41,10 @@ authenticated already, so they will see 'human@xxxxxxxxxxxxx'.
 
     >>> print(user_browser.title)
     Question #11 :  ...
-    >>> text = find_tags_by_class(
-    ...     user_browser.contents, 'boardCommentBody')[-1]
-    >>> print(extract_text(text.find_all('p')[-1]))
+    >>> text = find_tags_by_class(user_browser.contents, "boardCommentBody")[
+    ...     -1
+    ... ]
+    >>> print(extract_text(text.find_all("p")[-1]))
     --
     ______________________
     human@xxxxxxxxxxxxx
@@ -52,13 +54,14 @@ Unauthenticated users, such as a bot will see the mock email address
 of 'person@xxxxxxxxxx'. The anonymous user is unauthenticated, so they will
 see the obfuscated email address (<email address hidden>).
 
-    >>> anon_browser.open('http://answers.launchpad.test/ubuntu/+question/11')
+    >>> anon_browser.open("http://answers.launchpad.test/ubuntu/+question/11";)
     >>> print(anon_browser.title)
     Question #11 : ...
 
-    >>> text = find_tags_by_class(
-    ...     anon_browser.contents, 'boardCommentBody')[-1]
-    >>> print(extract_text(text.find_all('p')[-1]))
+    >>> text = find_tags_by_class(anon_browser.contents, "boardCommentBody")[
+    ...     -1
+    ... ]
+    >>> print(extract_text(text.find_all("p")[-1]))
     --
     ______________________
     &lt;email address hidden&gt;
@@ -77,7 +80,7 @@ Signatures are identified by paragraphs with a starting line like '--'.
 The entire content of the paragraph is wrapped by a tag of 'foldable'
 class.
 
-    >>> print(text.find_all('p')[-1])
+    >>> print(text.find_all("p")[-1])
     <p><span class="foldable">--...
     &lt;email address hidden&gt;<br/>
     Witty signatures rock!
@@ -90,7 +93,7 @@ preceded by a citation line. Only the quoted lines are wrapped with a
 tag of 'foldable' class, citation lines are always displayed. Again
 we can continue with the anonymous user to see the markup.
 
-    >>> print(text.find_all('p')[-2])
+    >>> print(text.find_all("p")[-2])
     <p>Somebody said sometime ago:<br/>
     <span class="foldable-quoted">
     &gt; 1. Remove the letters  c, j, q, x, w<br/>
diff --git a/lib/lp/answers/stories/question-obfuscation.rst b/lib/lp/answers/stories/question-obfuscation.rst
index 0ba7144..791728c 100644
--- a/lib/lp/answers/stories/question-obfuscation.rst
+++ b/lib/lp/answers/stories/question-obfuscation.rst
@@ -14,29 +14,33 @@ No Privileges Person can see the email address in the tooltip of the
 questions in the Latest questions solved portlet on the Answers
 front page.
 
-    >>> user_browser.open('http://answers.launchpad.test/')
+    >>> user_browser.open("http://answers.launchpad.test/";)
     >>> question_portlet = find_tag_by_id(
-    ...     user_browser.contents, 'latest-questions-solved')
-    >>> for li in question_portlet.find_all('li'):
-    ...     print(li['title'])
+    ...     user_browser.contents, "latest-questions-solved"
+    ... )
+    >>> for li in question_portlet.find_all("li"):
+    ...     print(li["title"])
+    ...
     I am not able to ... if i click on a mailto:user@xxxxxxxxxx link ...
 
 They can also see the email address in the tooltip for the question in the
 project's questions. When they view the question, they can see the address
 in the question's description.
 
-    >>> user_browser.getControl(name='field.search_text').value = 'mailto'
-    >>> user_browser.getControl('Find Answers').click()
+    >>> user_browser.getControl(name="field.search_text").value = "mailto"
+    >>> user_browser.getControl("Find Answers").click()
     >>> user_browser.title
     'Questions matching "mailto"'
 
     >>> question_listing = find_tag_by_id(
-    ...     user_browser.contents, 'question-listing')
-    >>> for li in question_portlet.find_all('li'):
-    ...     print(li['title'])
+    ...     user_browser.contents, "question-listing"
+    ... )
+    >>> for li in question_portlet.find_all("li"):
+    ...     print(li["title"])
+    ...
     I am not able to ... if i click on a mailto:user@xxxxxxxxxx link ...
 
-    >>> user_browser.getLink('mailto: problem in webpage').click()
+    >>> user_browser.getLink("mailto: problem in webpage").click()
     >>> description = find_main_content(user_browser.contents).p
     >>> print(description.decode_contents())
     I am not able to open my email client if i click on a
@@ -46,20 +50,20 @@ in the question's description.
 No Privileges Person can see email addresses in the FAQ's
 Related question's portlet.
 
-    >>> user_browser.getLink('Link to a FAQ').click()
+    >>> user_browser.getLink("Link to a FAQ").click()
     >>> user_browser.title
     'Is question #9 a FAQ...
 
-    >>> user_browser.getControl(name='field.faq-query').value = 'voip'
-    >>> user_browser.getControl('Search', index=0).click()
-    >>> user_browser.getControl('4').selected = True
-    >>> user_browser.getControl('Link to FAQ').click()
-    >>> user_browser.getLink('How can I make VOIP calls?').click()
+    >>> user_browser.getControl(name="field.faq-query").value = "voip"
+    >>> user_browser.getControl("Search", index=0).click()
+    >>> user_browser.getControl("4").selected = True
+    >>> user_browser.getControl("Link to FAQ").click()
+    >>> user_browser.getLink("How can I make VOIP calls?").click()
     >>> print(user_browser.title)
     FAQ #4 : Questions : Ubuntu
 
-    >>> portlet = find_portlet(user_browser.contents, 'Related questions')
-    >>> print(portlet.a['title'])
+    >>> portlet = find_portlet(user_browser.contents, "Related questions")
+    >>> print(portlet.a["title"])
     I am not able to open my email client if i click on a
     mailto:user@xxxxxxxxxx link in a webpage in...
 
@@ -67,24 +71,27 @@ No Privileges Person creates a question with an email address in the
 description. They can then see the email address in the tooltip in the
 'Latest questions asked' portlet for Answers front page.
 
-    >>> user_browser.getLink('#9 mailto: problem in webpage').click()
-    >>> user_browser.getLink('Ask a question').click()
+    >>> user_browser.getLink("#9 mailto: problem in webpage").click()
+    >>> user_browser.getLink("Ask a question").click()
     >>> user_browser.title
     'Ask a question about...
 
-    >>> user_browser.getControl('Summary').value = 'email address test'
-    >>> user_browser.getControl('Continue').click()
-    >>> user_browser.getControl('Description').value = (
-    ...     'The clicking mailto:user@xxxxxxxxxx crashes the browser.')
-    >>> user_browser.getControl('Post Question').click()
+    >>> user_browser.getControl("Summary").value = "email address test"
+    >>> user_browser.getControl("Continue").click()
+    >>> user_browser.getControl(
+    ...     "Description"
+    ... ).value = "The clicking mailto:user@xxxxxxxxxx crashes the browser."
+    >>> user_browser.getControl("Post Question").click()
     >>> print(user_browser.title)
     Question #... : ...
 
-    >>> user_browser.open('http://answers.launchpad.test/')
+    >>> user_browser.open("http://answers.launchpad.test/";)
     >>> question_portlet = find_tag_by_id(
-    ...     user_browser.contents, 'latest-questions-asked')
-    >>> for li in question_portlet.find_all('li'):
-    ...     print(li['title'])
+    ...     user_browser.contents, "latest-questions-asked"
+    ... )
+    >>> for li in question_portlet.find_all("li"):
+    ...     print(li["title"])
+    ...
     The clicking mailto:user@xxxxxxxxxx crashes the browser.
     ...
 
@@ -95,44 +102,50 @@ Anonymous users cannot see email addresses
 Anonymous cannot see the email address anywhere on the Answers front
 page.
 
-    >>> anon_browser.open('http://answers.launchpad.test/')
-    >>> 'user@xxxxxxxxxx' in anon_browser.contents
+    >>> anon_browser.open("http://answers.launchpad.test/";)
+    >>> "user@xxxxxxxxxx" in anon_browser.contents
     False
 
     >>> question_portlet = find_tag_by_id(
-    ...     anon_browser.contents, 'latest-questions-solved')
-    >>> for li in question_portlet.find_all('li'):
-    ...     print(li['title'])
+    ...     anon_browser.contents, "latest-questions-solved"
+    ... )
+    >>> for li in question_portlet.find_all("li"):
+    ...     print(li["title"])
+    ...
     I am not able to ... if i click on a
     mailto:<email address hidden> ...
 
     >>> question_portlet = find_tag_by_id(
-    ...     anon_browser.contents, 'latest-questions-asked')
-    >>> for li in question_portlet.find_all('li'):
-    ...     print(li['title'])
+    ...     anon_browser.contents, "latest-questions-asked"
+    ... )
+    >>> for li in question_portlet.find_all("li"):
+    ...     print(li["title"])
+    ...
     The clicking mailto:<email address hidden> crashes the browser.
     ...
 
 Nor can they see it in the question listings for the project.
 They cannot see the address reading the question either.
 
-    >>> anon_browser.getControl(name='field.search_text').value = 'mailto'
-    >>> anon_browser.getControl('Find Answers').click()
+    >>> anon_browser.getControl(name="field.search_text").value = "mailto"
+    >>> anon_browser.getControl("Find Answers").click()
     >>> anon_browser.title
     'Questions matching "mailto"'
 
-    >>> 'user@xxxxxxxxxx' in anon_browser.contents
+    >>> "user@xxxxxxxxxx" in anon_browser.contents
     False
 
     >>> question_listing = find_tag_by_id(
-    ...     anon_browser.contents, 'question-listing')
-    >>> for tr in question_listing.tbody.find_all('tr'):
-    ...     print(tr['title'])
+    ...     anon_browser.contents, "question-listing"
+    ... )
+    >>> for tr in question_listing.tbody.find_all("tr"):
+    ...     print(tr["title"])
+    ...
     I am not able to ... if i click on a mailto:<email address hidden>
     link ...
 
-    >>> anon_browser.getLink('mailto: problem in webpage').click()
-    >>> 'user@xxxxxxxxxx' in anon_browser.contents
+    >>> anon_browser.getLink("mailto: problem in webpage").click()
+    >>> "user@xxxxxxxxxx" in anon_browser.contents
     False
 
     >>> description = find_main_content(anon_browser.contents).p
@@ -143,11 +156,11 @@ They cannot see the address reading the question either.
 Anonymous users cannot see the email addresses in the Related
 questions portlet on a FAQ page.
 
-    >>> anon_browser.getLink('How can I make VOIP calls?').click()
+    >>> anon_browser.getLink("How can I make VOIP calls?").click()
     >>> print(anon_browser.title)
     FAQ #4 : Questions : Ubuntu
 
-    >>> portlet = find_portlet(anon_browser.contents, 'Related questions')
-    >>> print(portlet.a['title'])
+    >>> portlet = find_portlet(anon_browser.contents, "Related questions")
+    >>> print(portlet.a["title"])
     I am not able to open my email client if i click on a
     mailto:<email address hidden> link in a web...
diff --git a/lib/lp/answers/stories/question-overview.rst b/lib/lp/answers/stories/question-overview.rst
index 9005d90..612ee94 100644
--- a/lib/lp/answers/stories/question-overview.rst
+++ b/lib/lp/answers/stories/question-overview.rst
@@ -12,57 +12,58 @@ Products
 Creating a new question is possible directly from the main page of a
 product. Users simply click the 'Ask a question' buttion.
 
-    >>> user_browser.open('http://launchpad.test/firefox')
-    >>> user_browser.getLink('Ask a question').click()
+    >>> user_browser.open("http://launchpad.test/firefox";)
+    >>> user_browser.getLink("Ask a question").click()
     >>> print(user_browser.title)
     Ask a question about...
 
 There is also an 'Ask a question' link on the Bugs home page of the
 product:
 
-    >>> user_browser.open('http://bugs.launchpad.test/firefox')
-    >>> user_browser.getLink('Ask a question').click()
+    >>> user_browser.open("http://bugs.launchpad.test/firefox";)
+    >>> user_browser.getLink("Ask a question").click()
     >>> print(user_browser.title)
     Ask a question about...
 
 The list of all the currently active questions for the product is
 available from the 'Answers' facet.
 
-    >>> browser.open('http://launchpad.test/firefox')
-    >>> browser.getLink('Answers').click()
+    >>> browser.open("http://launchpad.test/firefox";)
+    >>> browser.getLink("Answers").click()
 
     >>> soup = find_main_content(browser.contents)
-    >>> print(soup.find('h1').decode_contents())
+    >>> print(soup.find("h1").decode_contents())
     Questions for Mozilla Firefox
 
-    >>> browser.getLink('Firefox loses focus and gets stuck').url
+    >>> browser.getLink("Firefox loses focus and gets stuck").url
     '.../firefox/+question/4'
 
 There is also an 'Ask a question' link to ask a new question.
 
-    >>> browser.getLink('Ask a question').url
+    >>> browser.getLink("Ask a question").url
     'http://answers.launchpad.test/firefox/+addquestion'
 
 There are links to some common listing of questions:
 
-    >>> browser.getLink('Open').url
+    >>> browser.getLink("Open").url
     '.../firefox/+questions?...'
 
-    >>> browser.getLink('Answered').url
+    >>> browser.getLink("Answered").url
     '.../firefox/+questions?...'
 
-    >>> browser.getLink('My questions').url
+    >>> browser.getLink("My questions").url
     '.../firefox/+myquestions'
 
 Information on a particular question can be seen by browsing to the
 question page.
 
-    >>> browser.getLink('Problem showing the SVG demo on W3C site').click()
+    >>> browser.getLink("Problem showing the SVG demo on W3C site").click()
     >>> print(browser.title)
     Question #2 : ...
 
-    >>> print(find_main_content(
-    ...     browser.contents).find('h1').decode_contents())
+    >>> print(
+    ...     find_main_content(browser.contents).find("h1").decode_contents()
+    ... )
     Problem showing the SVG demo on W3C site
 
 
@@ -71,40 +72,40 @@ Distributions
 
 Distributions have an 'Ask a question' link on the front page:
 
-    >>> user_browser.open('http://launchpad.test/ubuntu')
-    >>> user_browser.getLink('Ask a question').click()
+    >>> user_browser.open("http://launchpad.test/ubuntu";)
+    >>> user_browser.getLink("Ask a question").click()
     >>> print(user_browser.title)
     Ask a question about...
 
 As well as on the Bugs facet home page:
 
-    >>> user_browser.open('http://bugs.launchpad.test/ubuntu')
-    >>> user_browser.getLink('Ask a question').click()
+    >>> user_browser.open("http://bugs.launchpad.test/ubuntu";)
+    >>> user_browser.getLink("Ask a question").click()
     >>> print(user_browser.title)
     Ask a question about...
 
 The distribution home page also has a link to the 'Answers' facet:
 
-    >>> browser.open('http://launchpad.test/ubuntu')
-    >>> browser.getLink('Answers').url
+    >>> browser.open("http://launchpad.test/ubuntu";)
+    >>> browser.getLink("Answers").url
     'http://answers.launchpad.test/ubuntu'
 
-    >>> browser.getLink('Answers').click()
+    >>> browser.getLink("Answers").click()
     >>> print(browser.title)
     Questions : Ubuntu
 
-    >>> browser.getLink('Ask a question')
+    >>> browser.getLink("Ask a question")
     <Link...>
 
 On that facet, we will find the same common listings:
 
-    >>> browser.getLink('Open').url
+    >>> browser.getLink("Open").url
     '.../ubuntu/+questions?...'
 
-    >>> browser.getLink('Answered').url
+    >>> browser.getLink("Answered").url
     '.../ubuntu/+questions?...'
 
-    >>> browser.getLink('My questions').url
+    >>> browser.getLink("My questions").url
     '.../ubuntu/+myquestions'
 
 
@@ -114,49 +115,49 @@ Distribution Source packages
 On a source package, the 'Ask a question' link is accessible through the
 Answers facet.
 
-    >>> browser.open('http://launchpad.test/ubuntu/+source/evolution')
-    >>> browser.getLink('Answers').url
+    >>> browser.open("http://launchpad.test/ubuntu/+source/evolution";)
+    >>> browser.getLink("Answers").url
     'http://answers.launchpad.test/ubuntu/+source/evolution'
 
-    >>> browser.getLink('Answers').click()
+    >>> browser.getLink("Answers").click()
     >>> print(browser.title)
     Questions : evolution package : Ubuntu
 
-    >>> browser.getLink('Ask a question').url
+    >>> browser.getLink("Ask a question").url
     '.../ubuntu/+source/evolution/+addquestion'
 
 As are the common listings:
 
-    >>> browser.getLink('Open').url
+    >>> browser.getLink("Open").url
     '.../ubuntu/+source/evolution/+questions?...'
 
-    >>> browser.getLink('Answered').url
+    >>> browser.getLink("Answered").url
     '.../ubuntu/+source/evolution/+questions?...'
 
-    >>> browser.getLink('My questions').url
+    >>> browser.getLink("My questions").url
     '.../ubuntu/+source/evolution/+myquestions'
 
 The 'Answers' facet is also available on the distribution source package
 page:
 
-    >>> browser.open('http://launchpad.test/ubuntu/+source/mozilla-firefox')
-    >>> browser.getLink('Answers').url
+    >>> browser.open("http://launchpad.test/ubuntu/+source/mozilla-firefox";)
+    >>> browser.getLink("Answers").url
     'http://answers.launchpad.test/ubuntu/+source/mozilla-firefox'
 
-    >>> browser.getLink('Answers').click()
+    >>> browser.getLink("Answers").click()
     >>> browser.title
     'Questions : mozilla-firefox package : Ubuntu'
 
-    >>> browser.getLink('Ask a question').url
+    >>> browser.getLink("Ask a question").url
     '.../ubuntu/+source/mozilla-firefox/+addquestion'
 
-    >>> browser.getLink('Open').url
+    >>> browser.getLink("Open").url
     '.../ubuntu/+source/mozilla-firefox/+questions?...'
 
-    >>> browser.getLink('Answered').url
+    >>> browser.getLink("Answered").url
     '.../ubuntu/+source/mozilla-firefox/+questions?...'
 
-    >>> browser.getLink('My questions').url
+    >>> browser.getLink("My questions").url
     '.../ubuntu/+source/mozilla-firefox/+myquestions'
 
 
@@ -166,10 +167,11 @@ ProjectGroups
 ProjectGroups also have the 'Latest questions' portlet and the 'Ask a
 question' button on their overview page.
 
-    >>> user_browser.open('http://launchpad.test/mozilla')
+    >>> user_browser.open("http://launchpad.test/mozilla";)
 
     >>> questions = find_tag_by_id(
-    ...     user_browser.contents, 'portlet-latest-questions')
+    ...     user_browser.contents, "portlet-latest-questions"
+    ... )
     >>> print(backslashreplace(extract_text(questions)))
     All questions
     Latest questions
@@ -179,7 +181,7 @@ question' button on their overview page.
     Problem showing the SVG demo on W3C site ...
     Firefox cannot render Bank Site ...
 
-    >>> user_browser.getLink('Ask a question').click()
+    >>> user_browser.getLink("Ask a question").click()
     >>> print(user_browser.title)
     Ask a question about...
 
@@ -190,26 +192,27 @@ Persons
 The 'Answers' facet link will display a page listing all the questions
 involving a person.
 
-    >>> browser.open('http://launchpad.test/~name16')
-    >>> browser.getLink('Answers').url
+    >>> browser.open("http://launchpad.test/~name16";)
+    >>> browser.getLink("Answers").url
     'http://answers.launchpad.test/~name16'
 
-    >>> browser.getLink('Answers').click()
+    >>> browser.getLink("Answers").click()
     >>> print(browser.title)
     Questions : Foo Bar
 
-    >>> print(find_main_content(
-    ...     browser.contents).find('h1').decode_contents())
+    >>> print(
+    ...     find_main_content(browser.contents).find("h1").decode_contents()
+    ... )
     Questions for Foo Bar
 
-    >>> browser.getLink('Slow system').url
+    >>> browser.getLink("Slow system").url
     '.../ubuntu/+question/7'
 
     # One of them is not on this batch, so we'll have to first go to the next
     # batch.
 
-    >>> browser.getLink('Next').click()
-    >>> browser.getLink('Firefox loses focus').url
+    >>> browser.getLink("Next").click()
+    >>> browser.getLink("Firefox loses focus").url
     '.../firefox/+question/4'
 
 
@@ -220,29 +223,30 @@ You can access any question by its ID using the URL
 http://answers.launchpad.test/questions/<id>. This URL will redirect to
 the proper context where the question can be found:
 
-    >>> browser.open('http://answers.launchpad.test/questions/1')
+    >>> browser.open("http://answers.launchpad.test/questions/1";)
     >>> print(browser.url)
     http://answers.launchpad.test/firefox/+question/1
 
-    >>> print(find_main_content(
-    ...     browser.contents).find('h1').decode_contents())
+    >>> print(
+    ...     find_main_content(browser.contents).find("h1").decode_contents()
+    ... )
     Firefox cannot render Bank Site
 
 This also works on the webservice.
 
-    >>> browser.open('http://api.launchpad.test/devel/questions/1')
+    >>> browser.open("http://api.launchpad.test/devel/questions/1";)
     >>> print(browser.url)
     http://api.launchpad.test/devel/firefox/+question/1
 
 Asking for a non-existent question or an invalid ID will still raise a
 404 though:
 
-    >>> browser.open('http://answers.launchpad.test/questions/255')
+    >>> browser.open("http://answers.launchpad.test/questions/255";)
     Traceback (most recent call last):
       ...
     zope.publisher.interfaces.NotFound: ...
 
-    >>> browser.open('http://answers.launchpad.test/questions/bad_id')
+    >>> browser.open("http://answers.launchpad.test/questions/bad_id";)
     Traceback (most recent call last):
       ...
     zope.publisher.interfaces.NotFound: ...
@@ -251,30 +255,30 @@ Also If you access a question through the wrong context, you'll be
 redirected to the question in the proper context. (For example, this is
 useful after a question was retargeted.)
 
-    >>> browser.open('http://answers.launchpad.test/ubuntu/+question/1')
+    >>> browser.open("http://answers.launchpad.test/ubuntu/+question/1";)
     >>> print(browser.url)
     http://answers.launchpad.test/firefox/+question/1
 
-    >>> browser.open('http://api.launchpad.test/devel/ubuntu/+question/1')
+    >>> browser.open("http://api.launchpad.test/devel/ubuntu/+question/1";)
     >>> print(browser.url)
     http://api.launchpad.test/devel/firefox/+question/1
 
 It also works with pages below that URL:
 
     >>> browser.open(
-    ...     'http://answers.launchpad.test/ubuntu/+question/1/+history')
+    ...     "http://answers.launchpad.test/ubuntu/+question/1/+history";
+    ... )
     >>> print(browser.url)
     http://answers.launchpad.test/firefox/+question/1/+history
 
 But again, an invalid ID still raises a 404:
 
-    >>> browser.open('http://answers.launchpad.test/ubuntu/+question/255')
+    >>> browser.open("http://answers.launchpad.test/ubuntu/+question/255";)
     Traceback (most recent call last):
       ...
     zope.publisher.interfaces.NotFound: ...
 
-    >>> browser.open(
-    ...     'http://answers.launchpad.test/ubuntu/+question/bad_id')
+    >>> browser.open("http://answers.launchpad.test/ubuntu/+question/bad_id";)
     Traceback (most recent call last):
       ...
     zope.publisher.interfaces.NotFound: ...
diff --git a/lib/lp/answers/stories/question-reject-and-change-status.rst b/lib/lp/answers/stories/question-reject-and-change-status.rst
index b7b615b..7cc0805 100644
--- a/lib/lp/answers/stories/question-reject-and-change-status.rst
+++ b/lib/lp/answers/stories/question-reject-and-change-status.rst
@@ -8,8 +8,8 @@ for the product. For example, if the question is a duplicate or spam.
 No Privileges Person isn't an answer contact or administrator, so they
 don't have access to that feature.
 
-    >>> user_browser.open('http://launchpad.test/firefox/+question/2')
-    >>> user_browser.getLink('Reject question')
+    >>> user_browser.open("http://launchpad.test/firefox/+question/2";)
+    >>> user_browser.getLink("Reject question")
     Traceback (most recent call last):
       ...
     zope.testbrowser.browser.LinkNotFoundError
@@ -17,8 +17,7 @@ don't have access to that feature.
 Even when trying to access the page directly, they will get an unauthorized
 error.
 
-    >>> user_browser.open(
-    ...     'http://launchpad.test/firefox/+question/2/+reject')
+    >>> user_browser.open("http://launchpad.test/firefox/+question/2/+reject";)
     Traceback (most recent call last):
       ...
     zope.security.interfaces.Unauthorized: ...
@@ -26,41 +25,49 @@ error.
 To reject the question, the user clicks on the 'Reject Question' link.
 
     >>> admin_browser.open(
-    ...     'http://answers.launchpad.test/firefox/+question/2')
-    >>> admin_browser.getLink('Reject question').click()
-    >>> admin_browser.getControl('Reject').click()
+    ...     "http://answers.launchpad.test/firefox/+question/2";
+    ... )
+    >>> admin_browser.getLink("Reject question").click()
+    >>> admin_browser.getControl("Reject").click()
 
 They need to enter a message explaining the rejection:
 
-    >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
+    >>> for message in find_tags_by_class(admin_browser.contents, "message"):
     ...     print(message.decode_contents())
+    ...
     There is 1 error.
     You must provide an explanation message.
 
 At this point the user might decide this is a bad idea, so there is a
 cancel link to take them back to the question:
 
-    >>> print(admin_browser.getLink('Cancel').url)
+    >>> print(admin_browser.getLink("Cancel").url)
     http://answers.launchpad.test/firefox/+question/2
 
 Entering an explanation message and clicking the 'Reject' button,
 will reject the question.
 
-    >>> admin_browser.getControl('Message').value = (
-    ...     """Rejecting because it's a duplicate of bug #1.""")
-    >>> admin_browser.getControl('Reject').click()
+    >>> admin_browser.getControl(
+    ...     "Message"
+    ... ).value = """Rejecting because it's a duplicate of bug #1."""
+    >>> admin_browser.getControl("Reject").click()
 
 Once the question is rejected, a confirmation message is shown;
 
-    >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
+    >>> for message in find_tags_by_class(admin_browser.contents, "message"):
     ...     print(message.decode_contents())
+    ...
     You have rejected this question.
 
 its status is changed to 'Invalid';
 
     >>> def print_question_status(browser):
-    ...     print(extract_text(
-    ...         find_tag_by_id(browser.contents, 'question-status')))
+    ...     print(
+    ...         extract_text(
+    ...             find_tag_by_id(browser.contents, "question-status")
+    ...         )
+    ...     )
+    ...
 
     >>> print_question_status(admin_browser)
     Status: Invalid ...
@@ -69,22 +76,24 @@ and the rejection message is added to the question board.
 
     >>> content = find_main_content(admin_browser.contents)
     >>> print(
-    ...     content.find_all('div', 'boardCommentBody')[-1].decode_contents())
+    ...     content.find_all("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.
 
-    >>> print(content.find(id='can-you-help-with-this-problem'))
+    >>> print(content.find(id="can-you-help-with-this-problem"))
     None
 
 Selecting the 'Reject' action again, will simply display a message
 stating that the question is already rejected:
 
-    >>> admin_browser.getLink('Reject question').click()
+    >>> admin_browser.getLink("Reject question").click()
     >>> print(admin_browser.url)
     http://answers.launchpad.test/firefox/+question/2
-    >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
+    >>> for message in find_tags_by_class(admin_browser.contents, "message"):
     ...     print(message.decode_contents())
+    ...
     The question is already rejected.
 
 Changing the Question Status
@@ -101,8 +110,8 @@ set the question status to an arbitrary value without workflow constraint.
 
 That action isn't available to a non-privileged user:
 
-    >>> browser.open('http://launchpad.test/firefox/+question/2')
-    >>> browser.getLink('Change status')
+    >>> browser.open("http://launchpad.test/firefox/+question/2";)
+    >>> browser.getLink("Change status")
     Traceback (most recent call last):
       ...
     zope.testbrowser.browser.LinkNotFoundError
@@ -110,25 +119,26 @@ That action isn't available to a non-privileged user:
 The change status form is available to an administrator through the
 'Change status' link.
 
-    >>> admin_browser.open('http://launchpad.test/firefox/+question/2')
-    >>> admin_browser.getLink('Change status').click()
+    >>> admin_browser.open("http://launchpad.test/firefox/+question/2";)
+    >>> admin_browser.getLink("Change status").click()
 
 The form has a select widget displaying the current status.
 
-    >>> admin_browser.getControl('Status', index=0).displayValue
+    >>> admin_browser.getControl("Status", index=0).displayValue
     ['Invalid']
 
 There is also a cancel link should the user decide otherwise:
 
-    >>> print(admin_browser.getLink('Cancel').url)
+    >>> print(admin_browser.getLink("Cancel").url)
     http://answers.launchpad.test/firefox/+question/2
 
 The user needs to select a status and enter a message explaining the
 status change:
 
-    >>> admin_browser.getControl('Change Status').click()
-    >>> for error in find_tags_by_class(admin_browser.contents, 'message'):
+    >>> admin_browser.getControl("Change Status").click()
+    >>> for error in find_tags_by_class(admin_browser.contents, "message"):
     ...     print(error.decode_contents())
+    ...
     There are 2 errors.
     You didn't change the status.
     You must provide an explanation message.
@@ -136,16 +146,18 @@ status change:
 To correct the mistake of the previous example, the administrator would
 select back the 'Open' status and provide an appropriate message:
 
-    >>> admin_browser.getControl('Status', index=0).displayValue = ['Open']
-    >>> admin_browser.getControl('Message').value = (
+    >>> admin_browser.getControl("Status", index=0).displayValue = ["Open"]
+    >>> admin_browser.getControl("Message").value = (
     ...     "Setting status back to 'Open'. Questions similar to a bug "
-    ...     "report should be linked to it, not rejected.")
-    >>> admin_browser.getControl('Change Status').click()
+    ...     "report should be linked to it, not rejected."
+    ... )
+    >>> admin_browser.getControl("Change Status").click()
 
 Once the operation is completed, a confirmation message is displayed;
 
-    >>> for message in find_tags_by_class(admin_browser.contents, 'message'):
+    >>> for message in find_tags_by_class(admin_browser.contents, "message"):
     ...     print(message.decode_contents())
+    ...
     Question status updated.
 
 its status is updated;
@@ -157,6 +169,7 @@ and the explanation message is added to the question discussion:
 
     >>> content = find_main_content(admin_browser.contents)
     >>> print(
-    ...     content.find_all('div', 'boardCommentBody')[-1].decode_contents())
+    ...     content.find_all("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.rst b/lib/lp/answers/stories/question-search-multiple-languages.rst
index 3668977..1aac165 100644
--- a/lib/lp/answers/stories/question-search-multiple-languages.rst
+++ b/lib/lp/answers/stories/question-search-multiple-languages.rst
@@ -7,8 +7,8 @@ preferred languages are listed and searched.
 Make the test browsers look like they're coming from an arbitrary South
 African IP address, since we'll use that later.
 
-    >>> anon_browser.addHeader('X_FORWARDED_FOR', '196.36.161.227')
-    >>> user_browser.addHeader('X_FORWARDED_FOR', '196.36.161.227')
+    >>> anon_browser.addHeader("X_FORWARDED_FOR", "196.36.161.227")
+    >>> user_browser.addHeader("X_FORWARDED_FOR", "196.36.161.227")
 
 
 Anonymous searching
@@ -20,10 +20,11 @@ or inferred from GeoIP information. In this example, only English
 questions will be shown when the user has not provided language
 information.
 
-    >>> anon_browser.open('http://launchpad.test/distros/ubuntu/+questions')
+    >>> anon_browser.open("http://launchpad.test/distros/ubuntu/+questions";)
     >>> soup = find_main_content(anon_browser.contents)
-    >>> for question in soup.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> for question in soup.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Continue playing after shutdown
     Play DVDs in Totem
     mailto: problem in webpage
@@ -31,10 +32,11 @@ information.
     Slow system
 
     # Since we have more than 5 results, some of them are in the second batch.
-    >>> anon_browser.getLink('Next').click()
+    >>> anon_browser.getLink("Next").click()
     >>> soup = find_main_content(anon_browser.contents)
-    >>> for question in soup.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> for question in soup.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Installation failed
 
 The questions match the languages inferred from GeoIP (South Africa in
@@ -42,18 +44,19 @@ this case). The language control shows the intersection of the user's
 languages and the languages of the target's questions. In this example:
 set(['af', 'en', 'st', 'xh', 'zu']) & set(['en', 'es']) == set(en).
 
-    >>> sorted(anon_browser.getControl(name='field.language').options)
+    >>> sorted(anon_browser.getControl(name="field.language").options)
     ['en']
 
 If the user unselects all the language options, then all questions,
 whatever the language they are written in are displayed. Doing so,
 the anonymous user will see a Spanish question.
 
-    >>> anon_browser.getControl('English (en)').selected = False
-    >>> anon_browser.getControl('Search', index=0).click()
-    >>> table = find_tag_by_id(anon_browser.contents, 'question-listing')
-    >>> for question in table.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> anon_browser.getControl("English (en)").selected = False
+    >>> anon_browser.getControl("Search", index=0).click()
+    >>> table = find_tag_by_id(anon_browser.contents, "question-listing")
+    >>> for question in table.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Problema al recompilar kernel con soporte smp (doble-núcleo)
     Continue playing after shutdown
     Play DVDs in Totem
@@ -64,8 +67,9 @@ While the user might recognize the first question above is in Spanish,
 browsers and search engine robots need help. Each row in the list
 of questions declares its language and text direction.
 
-    >>> for question in table.find_all('tr', lang=True):
-    ...     print('lang="%s" dir="%s"' % (question['lang'], question['dir']))
+    >>> for question in table.find_all("tr", lang=True):
+    ...     print('lang="%s" dir="%s"' % (question["lang"], question["dir"]))
+    ...
     lang="es" dir="ltr"
     lang="en" dir="ltr"
     lang="en" dir="ltr"
@@ -76,16 +80,18 @@ Following the next link to the second page, the anonymous person or
 robot will see the remaining questions. The last question is in
 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.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> anon_browser.getLink("Next").click()
+    >>> table = find_tag_by_id(anon_browser.contents, "question-listing")
+    >>> for question in table.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Slow system
     Installation failed
     عكس ...
 
-    >>> for question in table.find_all('tr', lang=True):
-    ...     print('lang="%s" dir="%s"' % (question['lang'], question['dir']))
+    >>> for question in table.find_all("tr", lang=True):
+    ...     print('lang="%s" dir="%s"' % (question["lang"], question["dir"]))
+    ...
     lang="en" dir="ltr"
     lang="en" dir="ltr"
     lang="ar" dir="rtl"
@@ -99,19 +105,20 @@ Kubuntu must enable answers to access questions.
     >>> from lp.app.enums import ServiceUsage
     >>> from lp.registry.interfaces.distribution import IDistributionSet
 
-    >>> login('admin@xxxxxxxxxxxxx')
-    >>> getUtility(IDistributionSet)['kubuntu'].answers_usage = (
-    ...     ServiceUsage.LAUNCHPAD)
+    >>> login("admin@xxxxxxxxxxxxx")
+    >>> getUtility(IDistributionSet)[
+    ...     "kubuntu"
+    ... ].answers_usage = ServiceUsage.LAUNCHPAD
     >>> transaction.commit()
     >>> logout()
 
-    >>> anon_browser.open('http://launchpad.test/kubuntu/+questions')
-    >>> anon_browser.getControl(name='field.language')
+    >>> anon_browser.open("http://launchpad.test/kubuntu/+questions";)
+    >>> anon_browser.getControl(name="field.language")
     Traceback (most recent call last):
       ...
     LookupError: name ...'field.language...
 
-    >>> content = find_main_content(anon_browser.contents).find('p')
+    >>> content = find_main_content(anon_browser.contents).find("p")
     >>> print(content.decode_contents())
     There are no questions for Kubuntu with the requested statuses.
 
@@ -121,10 +128,11 @@ The mozilla-firefox sourcepackage only has English questions. When the
 anonymous user makes a request from a GeoIP that has no languages
 mapped, we assume they speak the default language of English.
 
-    >>> anon_browser.addHeader('X_FORWARDED_FOR', '172.16.1.1')
+    >>> anon_browser.addHeader("X_FORWARDED_FOR", "172.16.1.1")
     >>> anon_browser.open(
-    ...     'http://launchpad.test/ubuntu/+source/mozilla-firefox/+questions')
-    >>> anon_browser.getControl(name='field.language')
+    ...     "http://launchpad.test/ubuntu/+source/mozilla-firefox/+questions";
+    ... )
+    >>> anon_browser.getControl(name="field.language")
     Traceback (most recent call last):
       ...
     LookupError: name ...'field.language...
@@ -132,16 +140,17 @@ mapped, we assume they speak the default language of English.
 But if the user configures their browser to accept Spanish and English
 then questions with those language will be displayed:
 
-    >>> anon_browser.addHeader('Accept-Language', 'es, en')
-    >>> anon_browser.open('http://launchpad.test/distros/ubuntu/+questions')
-    >>> anon_browser.getControl('English (en)').selected
+    >>> anon_browser.addHeader("Accept-Language", "es, en")
+    >>> anon_browser.open("http://launchpad.test/distros/ubuntu/+questions";)
+    >>> anon_browser.getControl("English (en)").selected
     True
-    >>> anon_browser.getControl('Spanish (es)').selected
+    >>> anon_browser.getControl("Spanish (es)").selected
     True
 
     >>> soup = find_main_content(anon_browser.contents)
-    >>> for question in soup.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> for question in soup.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Problema al recompilar kernel con soporte smp (doble-núcleo)
     Continue playing after shutdown
     Play DVDs in Totem
@@ -149,10 +158,11 @@ then questions with those language will be displayed:
     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()
+    >>> anon_browser.getLink("Next").click()
     >>> soup = find_main_content(anon_browser.contents)
-    >>> for question in soup.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> for question in soup.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Slow system
     Installation failed
 
@@ -168,17 +178,18 @@ languages, nor is their browser configured with a language; we use GeoIP
 rules. As with the anonymous user, the intersection of the GeoIP
 languages and the target's question languages is 'en'.
 
-    >>> user_browser.open('http://launchpad.test/distros/ubuntu/+questions')
-    >>> sorted(user_browser.getControl(name='field.language').options)
+    >>> user_browser.open("http://launchpad.test/distros/ubuntu/+questions";)
+    >>> sorted(user_browser.getControl(name="field.language").options)
     ['en']
 
 When the project languages are just English, and the user speaks
 that language, we do not show the language controls.
 
-    >>> user_browser.addHeader('X_FORWARDED_FOR', '172.16.1.1')
+    >>> user_browser.addHeader("X_FORWARDED_FOR", "172.16.1.1")
     >>> user_browser.open(
-    ...     'http://launchpad.test/ubuntu/+source/mozilla-firefox/+questions')
-    >>> user_browser.getControl(name='field.language')
+    ...     "http://launchpad.test/ubuntu/+source/mozilla-firefox/+questions";
+    ... )
+    >>> user_browser.getControl(name="field.language")
     Traceback (most recent call last):
       ...
     LookupError: name ...'field.language...
@@ -186,9 +197,9 @@ that language, we do not show the language controls.
 When No Privileges Person adds Spanish and English to their browser, they
 are added to the language controls.
 
-    >>> user_browser.addHeader('Accept-Language', 'es, en')
-    >>> user_browser.open('http://launchpad.test/distros/ubuntu/+questions')
-    >>> sorted(user_browser.getControl(name='field.language').options)
+    >>> user_browser.addHeader("Accept-Language", "es, en")
+    >>> user_browser.open("http://launchpad.test/distros/ubuntu/+questions";)
+    >>> sorted(user_browser.getControl(name="field.language").options)
     ['en', 'es']
 
 Users that have configured their preferred languages may choose to
@@ -200,24 +211,26 @@ Catalan questions, so no Catalan checkbox is displayed.
 
     >>> from lp.testing.pages import strip_label
 
-    >>> browser.addHeader('Authorization', 'Basic carlos@xxxxxxxxxxxxx:test')
-    >>> browser.open('http://launchpad.test/distros/ubuntu/+questions')
-    >>> language_control = browser.getControl(name='field.language')
+    >>> browser.addHeader("Authorization", "Basic carlos@xxxxxxxxxxxxx:test")
+    >>> browser.open("http://launchpad.test/distros/ubuntu/+questions";)
+    >>> language_control = browser.getControl(name="field.language")
     >>> for label in sorted(language_control.displayOptions):
     ...     strip_label(label)
+    ...
     'English (en)'
     'Spanish (es)'
-    >>> sorted(browser.getControl(name='field.language').options)
+    >>> sorted(browser.getControl(name="field.language").options)
     ['en', 'es']
 
 By unchecking a checkbox, Carlos can exclude English questions from
 the search results.
 
-    >>> browser.getControl('English (en)').selected = False
-    >>> browser.getControl('Search', index=0).click()
+    >>> browser.getControl("English (en)").selected = False
+    >>> browser.getControl("Search", index=0).click()
     >>> content = find_main_content(browser.contents)
-    >>> for question in content.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> for question in content.find_all("td", "questionTITLE"):
+    ...     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.
@@ -228,15 +241,15 @@ English questions. As there are no Japanese or Welsh questions, there
 will not be any controls present for those two languages when
 searching Ubuntu.
 
-    >>> daf_browser = setupBrowser(auth='Basic daf@xxxxxxxxxxxxx:test')
-    >>> daf_browser.open('http://launchpad.test/~daf/+editlanguages')
-    >>> daf_browser.getControl('English (United Kingdom)').selected
+    >>> daf_browser = setupBrowser(auth="Basic daf@xxxxxxxxxxxxx:test")
+    >>> daf_browser.open("http://launchpad.test/~daf/+editlanguages";)
+    >>> daf_browser.getControl("English (United Kingdom)").selected
     True
-    >>> daf_browser.getControl('Japanese').selected
+    >>> daf_browser.getControl("Japanese").selected
     True
-    >>> daf_browser.getControl('Welsh').selected
+    >>> daf_browser.getControl("Welsh").selected
     True
-    >>> daf_browser.getControl('English', index=1).selected
+    >>> daf_browser.getControl("English", index=1).selected
     False
 
 The user's languages are presented as controls in the question form.
@@ -244,20 +257,22 @@ The controls are filters that allow the user to see questions in
 their languages. Daf, in this example, can see a language filter for
 English, and can use it to locate English questions.
 
-    >>> daf_browser.open('http://launchpad.test/distros/ubuntu/+questions')
-    >>> language_control = daf_browser.getControl(name='field.language')
+    >>> daf_browser.open("http://launchpad.test/distros/ubuntu/+questions";)
+    >>> language_control = daf_browser.getControl(name="field.language")
     >>> for label in language_control.displayOptions:
     ...     strip_label(label)
+    ...
     'English (en)'
 
-    >>> daf_browser.getControl(name='field.language').options
+    >>> daf_browser.getControl(name="field.language").options
     ['en']
 
-    >>> daf_browser.getControl('English (en)').selected = True
-    >>> daf_browser.getControl('Search', index=0).click()
+    >>> daf_browser.getControl("English (en)").selected = True
+    >>> daf_browser.getControl("Search", index=0).click()
     >>> content = find_main_content(daf_browser.contents)
-    >>> for question in content.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> for question in content.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Continue playing after shutdown
     Play DVDs in Totem
     mailto: problem in webpage
@@ -276,13 +291,13 @@ project that needs help. They visit Kubuntu and does not see a need,
 but when they visit Mozilla Firefox, they are informed that there are
 questions going unanswered.
 
-    >>> user_browser.open('http://answers.launchpad.test/kubuntu')
-    >>> paragraph = find_main_content(user_browser.contents).find('p')
+    >>> user_browser.open("http://answers.launchpad.test/kubuntu";)
+    >>> paragraph = find_main_content(user_browser.contents).find("p")
     >>> print(extract_text(paragraph))
     There are no questions for Kubuntu with the requested statuses.
 
-    >>> user_browser.open('http://answers.launchpad.test/firefox')
-    >>> paragraph = find_main_content(user_browser.contents).find('p')
+    >>> user_browser.open("http://answers.launchpad.test/firefox";)
+    >>> paragraph = find_main_content(user_browser.contents).find("p")
     >>> print(extract_text(paragraph))
     Mozilla Firefox has unanswered questions in the following languages:
     1 in Portuguese (Brazil). Can you help?
@@ -293,22 +308,23 @@ The user cannot change the language. The link presets the STATUS to
 OPEN. No Privileges Person sees a single question as indicated by
 the preceding page.
 
-    >>> user_browser.getLink('Portuguese (Brazil)').click()
+    >>> user_browser.getLink("Portuguese (Brazil)").click()
     >>> print(user_browser.title)
     Portuguese (Brazil) questions in Mozilla Firefox : Questions : Mozilla
     Firefox
 
-    >>> language_field = user_browser.getControl(name='field.language')
+    >>> language_field = user_browser.getControl(name="field.language")
     >>> print(language_field.type)
     hidden
 
-    >>> labels = user_browser.getControl(name='field.status').displayValue
+    >>> labels = user_browser.getControl(name="field.status").displayValue
     >>> [strip_label(label) for label in labels]
     ['Open']
 
     >>> content = find_main_content(user_browser.contents)
-    >>> for question in content.find_all('td', 'questionTITLE'):
-    ...     print(question.find('a').decode_contents())
+    >>> for question in content.find_all("td", "questionTITLE"):
+    ...     print(question.find("a").decode_contents())
+    ...
     Problemas de Impressão no Firefox
 
 The page in all other respects behaves like a question search page.
@@ -323,30 +339,34 @@ example, Sample Person has asked a question in Arabic in the Ubuntu
 project. They can see the question on the second page of "My questions"...
 
     >>> sample_person_browser = setupBrowser(
-    ...     auth='Basic test@xxxxxxxxxxxxx:test')
-    >>> sample_person_browser.open('http://answers.launchpad.test/ubuntu')
-    >>> sample_person_browser.getLink('My questions').click()
-    >>> sample_person_browser.getLink('Next').click()
+    ...     auth="Basic test@xxxxxxxxxxxxx:test"
+    ... )
+    >>> sample_person_browser.open("http://answers.launchpad.test/ubuntu";)
+    >>> sample_person_browser.getLink("My questions").click()
+    >>> sample_person_browser.getLink("Next").click()
     >>> print(sample_person_browser.title)
     Questions you asked about Ubuntu : Questions : Ubuntu
 
     >>> questions = find_tag_by_id(
-    ...     sample_person_browser.contents, 'question-listing')
-    >>> for key in ('lang', 'dir'):
-    ...     print('%s: %s ' % (key, questions.tbody.tr[key]))
+    ...     sample_person_browser.contents, "question-listing"
+    ... )
+    >>> for key in ("lang", "dir"):
+    ...     print("%s: %s " % (key, questions.tbody.tr[key]))
+    ...
     lang: ar
     dir: rtl
 
-    >>> for question in questions.find_all('td', {'class': 'questionTITLE'}):
-    ...     print(question.find('a').decode_contents())
+    >>> for question in questions.find_all("td", {"class": "questionTITLE"}):
+    ...     print(question.find("a").decode_contents())
     عكس التغ...
 
 ...even though they have not set Arabic as one of their preferred languages.
 
     >>> sample_person_browser.getLink(
-    ...     'Change your preferred languages').click()
+    ...     "Change your preferred languages"
+    ... ).click()
     >>> print(sample_person_browser.title)
     Language preferences...
 
-    >>> sample_person_browser.getControl('Arabic', index=0).selected
+    >>> sample_person_browser.getControl("Arabic", index=0).selected
     False
diff --git a/lib/lp/answers/stories/question-subscriptions.rst b/lib/lp/answers/stories/question-subscriptions.rst
index e4e6b2e..545dda0 100644
--- a/lib/lp/answers/stories/question-subscriptions.rst
+++ b/lib/lp/answers/stories/question-subscriptions.rst
@@ -12,16 +12,15 @@ To subscribe, users use the 'Subscribe' link and then confirm that
 they want to subscribe by clicking on the 'Subscribe' button. The user
 sees a link to their subscribed questions.
 
-    >>> user_browser.open(
-    ...     'http://launchpad.test/firefox/+question/2')
-    >>> user_browser.getLink('Subscribe').click()
+    >>> user_browser.open("http://launchpad.test/firefox/+question/2";)
+    >>> user_browser.getLink("Subscribe").click()
     >>> print(user_browser.title)
     Subscription : Question #2 ...
 
-    >>> print(user_browser.getLink('your subscribed questions page'))
+    >>> print(user_browser.getLink("your subscribed questions page"))
     <Link ...'http://answers.launchpad.test/~no-priv/+subscribedquestions'>
 
-    >>> user_browser.getControl('Subscribe').click()
+    >>> user_browser.getControl("Subscribe").click()
 
 A message confirming that they were subscribed is displayed:
 
@@ -36,15 +35,16 @@ When the user is subscribed to the question, the 'Subscribe' link
 becomes an 'Unsubscribe' link. To unsubscribe, the user follows that
 link and then click on the 'Unsubscribe' button.
 
-    >>> link = user_browser.getLink('Unsubscribe').click()
+    >>> link = user_browser.getLink("Unsubscribe").click()
     >>> print(user_browser.title)
     Subscription : Question #2 ...
 
-    >>> print(extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'unsubscribe')))
+    >>> print(
+    ...     extract_text(find_tag_by_id(user_browser.contents, "unsubscribe"))
+    ... )
     Unsubscribing from this question ...
 
-    >>> user_browser.getControl('Unsubscribe').click()
+    >>> user_browser.getControl("Unsubscribe").click()
 
 A confirmation is displayed:
 
@@ -59,12 +59,14 @@ It is also possible to subscribe at the same time than posting a message
 on an existing question. The user can simply check the 'Email me future
 discussion about this question' checkbox:
 
-    >>> user_browser.open('http://launchpad.test/firefox/+question/6')
-    >>> user_browser.getControl('Message').value = (
+    >>> user_browser.open("http://launchpad.test/firefox/+question/6";)
+    >>> user_browser.getControl("Message").value = (
     ...     "Try starting firefox from the command-line. Are there any "
-    ...     "messages appearing?")
+    ...     "messages appearing?"
+    ... )
     >>> user_browser.getControl(
-    ...     'Email me future discussion about this question').selected = True
+    ...     "Email me future discussion about this question"
+    ... ).selected = True
     >>> user_browser.getControl("Add Information Request").click()
 
 A notification message is displayed notifying of the subscription:
diff --git a/lib/lp/answers/stories/question-workflow.rst b/lib/lp/answers/stories/question-workflow.rst
index 5addf78..73f604f 100755
--- a/lib/lp/answers/stories/question-workflow.rst
+++ b/lib/lp/answers/stories/question-workflow.rst
@@ -8,24 +8,27 @@ the Firefox product which was filed by 'Sample Person'.
     # We will use one browser objects for the owner, and one for the user
     # providing support, 'No Privileges Person' here.
 
-    >>> owner_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
+    >>> owner_browser = setupBrowser(auth="Basic test@xxxxxxxxxxxxx:test")
 
     >>> support_browser = setupBrowser(
-    ...     auth='Basic no-priv@xxxxxxxxxxxxx:test')
+    ...     auth="Basic no-priv@xxxxxxxxxxxxx:test"
+    ... )
 
     # Define some utility functions to retrieve easily the last comment
     # added and the status of the question.
 
     >>> def find_request_status(contents):
-    ...     print(extract_text(
-    ...         find_tag_by_id(contents, 'question-status')))
+    ...     print(extract_text(find_tag_by_id(contents, "question-status")))
+    ...
 
-    >>> def  find_last_comment(contents):
+    >>> def find_last_comment(contents):
     ...     soup = find_main_content(contents)
-    ...     return soup.find_all('div', 'boardCommentBody')[-1]
+    ...     return soup.find_all("div", "boardCommentBody")[-1]
+    ...
 
     >>> def print_last_comment(contents):
     ...     print(extract_text(find_last_comment(contents)))
+    ...
 
 
 Logging In
@@ -33,7 +36,7 @@ Logging In
 
 To participate in a question, the user must be logged in.
 
-    >>> anon_browser.open('http://launchpad.test/firefox/+question/2')
+    >>> anon_browser.open("http://launchpad.test/firefox/+question/2";)
     >>> print(anon_browser.contents)
     <!DOCTYPE...
     ...
@@ -54,23 +57,29 @@ information. To request for more information from the question owner, No
 Privileges Person enters their question in the 'Message' field and clicks
 on the 'Add Information Request' button.
 
-    >>> support_browser.open(
-    ...     'http://launchpad.test/firefox/+question/2')
+    >>> support_browser.open("http://launchpad.test/firefox/+question/2";)
     >>> content = find_tag_by_id(
-    ...     support_browser.contents, 'can-you-help-with-this-problem')
+    ...     support_browser.contents, "can-you-help-with-this-problem"
+    ... )
     >>> print(content.h2.decode_contents())
     Can you help with this problem?
 
-    >>> print(extract_text(
-    ...     find_tag_by_id(support_browser.contents, 'horizontal-menu')))
+    >>> print(
+    ...     extract_text(
+    ...         find_tag_by_id(support_browser.contents, "horizontal-menu")
+    ...     )
+    ... )
     Link existing bug
     Create bug report
     Link to a FAQ
     Create a new FAQ
 
-    >>> support_browser.getControl('Message').value = (
-    ...     "Can you provide an example of an URL displaying the problem?")
-    >>> support_browser.getControl('Add Information Request').click()
+    >>> support_browser.getControl(
+    ...     "Message"
+    ... ).value = (
+    ...     "Can you provide an example of an URL displaying the problem?"
+    ... )
+    >>> support_browser.getControl("Add Information Request").click()
 
 The message was added to the question and its status was changed to
 'Needs information':
@@ -84,9 +93,9 @@ The message was added to the question and its status was changed to
 Of course, if you don't add a message, clicking on the button will give
 you an error.
 
-    >>> support_browser.getControl('Add Information Request').click()
+    >>> support_browser.getControl("Add Information Request").click()
     >>> soup = find_main_content(support_browser.contents)
-    >>> print(soup.find('div', 'message').decode_contents())
+    >>> print(soup.find("div", "message").decode_contents())
     Please enter a message.
 
 
@@ -97,9 +106,12 @@ A comment can be added at any point without altering the status. The
 user simply enters the comment in the 'Message' box and clicks the 'Just
 Add a Comment' button.
 
-    >>> support_browser.getControl('Message').value = (
-    ...     "I forgot to mention, in the meantime here is a workaround...")
-    >>> support_browser.getControl('Just Add a Comment').click()
+    >>> support_browser.getControl(
+    ...     "Message"
+    ... ).value = (
+    ...     "I forgot to mention, in the meantime here is a workaround..."
+    ... )
+    >>> support_browser.getControl("Just Add a Comment").click()
 
 This appends the comment to the question and it doesn't change its
 status:
@@ -121,16 +133,17 @@ Providing More Information" button. Note that the question owner cannot
 see the 'Can you help with this problem?' heading because it is not
 relevant to their tasks.
 
-    >>> owner_browser.open(
-    ...     'http://launchpad.test/firefox/+question/2')
+    >>> owner_browser.open("http://launchpad.test/firefox/+question/2";)
     >>> content = find_tag_by_id(
-    ...     owner_browser.contents, 'can-you-help-with-this-problem')
+    ...     owner_browser.contents, "can-you-help-with-this-problem"
+    ... )
     >>> content is None
     True
 
-    >>> owner_browser.getControl('Message').value = (
+    >>> owner_browser.getControl("Message").value = (
     ...     "The following SVG doesn't display properly:\n"
-    ...     "http://www.w3.org/2001/08/rdfweb/rdfweb-chaals-and-dan.svg";)
+    ...     "http://www.w3.org/2001/08/rdfweb/rdfweb-chaals-and-dan.svg";
+    ... )
     >>> owner_browser.getControl("I'm Providing More Information").click()
 
 Once the owner replied with the, hopefully, requested information, the
@@ -152,12 +165,12 @@ Once the question is clarified, it is easier for a user to give an
 answer. This is done by entering the answer in the 'Message' box and
 clicking the 'Propose Answer' button.
 
-    >>> support_browser.open(
-    ...     'http://launchpad.test/firefox/+question/2')
-    >>> support_browser.getControl('Message').value = (
+    >>> support_browser.open("http://launchpad.test/firefox/+question/2";)
+    >>> support_browser.getControl("Message").value = (
     ...     "New version of the firefox package are available with SVG "
-    ...     "support enabled. You can use apt to upgrade.")
-    >>> support_browser.getControl('Propose Answer').click()
+    ...     "support enabled. You can use apt to upgrade."
+    ... )
+    >>> support_browser.getControl("Propose Answer").click()
 
 This moves the question to the Answered state and adds the answer to
 the end of the discussion:
@@ -176,10 +189,9 @@ Confirming an Answer
 When the owner comes back on the question page, they will now see a new
 'This Solved My Problem' button near the answer.
 
-    >>> owner_browser.open(
-    ...     'http://launchpad.test/firefox/+question/2')
+    >>> owner_browser.open("http://launchpad.test/firefox/+question/2";)
     >>> soup = find_main_content(owner_browser.contents)
-    >>> soup.find_all('div', 'boardComment')[-1].find('input', type='submit')
+    >>> soup.find_all("div", "boardComment")[-1].find("input", type="submit")
     <input name="field.actions.confirm" type="submit"
      value="This Solved My Problem"/>
 
@@ -190,14 +202,15 @@ There is also a hint below the form to the question owner about using
 the 'This Solved My Problem' button.
 
     >>> answer_button_paragraph = find_tag_by_id(
-    ...     owner_browser.contents, 'answer-button-hint')
+    ...     owner_browser.contents, "answer-button-hint"
+    ... )
     >>> print(extract_text(answer_button_paragraph))
     To confirm an answer, use the 'This Solved My Problem' button located at
     the bottom of the answer.
 
 Clicking that button will confirm that the answer solved the problem.
 
-    >>> owner_browser.getControl('This Solved My Problem').click()
+    >>> owner_browser.getControl("This Solved My Problem").click()
 
 This changes the status of the question to 'Solved' and mark 'No
 Privileges Person' as the solver.
@@ -214,20 +227,25 @@ confirmation message was appended to the question discussion:
 The confirmed answer is also highlighted.
 
     >>> soup = find_main_content(owner_browser.contents)
-    >>> bestAnswer = soup.find_all('div', 'boardComment')[-2]
-    >>> print(bestAnswer.find_all('img')[1])
+    >>> bestAnswer = soup.find_all("div", "boardComment")[-2]
+    >>> print(bestAnswer.find_all("img")[1])
     <img ... src="/@@/favourite-yes" ... title="Marked as best answer"/>
 
-    >>> print(soup.find(
-    ...     'div', 'boardCommentBody highlighted editable-message-text'
-    ... ).decode_contents())
+    >>> print(
+    ...     soup.find(
+    ...         "div", "boardCommentBody highlighted editable-message-text"
+    ...     ).decode_contents()
+    ... )
     <p>New version of the firefox package are available with SVG support
     enabled. You can use apt to upgrade.</p>
 
 The History link should now show up.
 
-    >>> print(extract_text(
-    ...     find_tag_by_id(support_browser.contents, 'horizontal-menu')))
+    >>> print(
+    ...     extract_text(
+    ...         find_tag_by_id(support_browser.contents, "horizontal-menu")
+    ...     )
+    ... )
     History
     Link existing bug
     Create bug report
@@ -242,9 +260,10 @@ When the question is Solved, it is still possible to add comments to it.
 The user simply enters the comment in the 'Message' box and clicks the
 'Just Add a Comment' button.
 
-    >>> owner_browser.getControl('Message').value = (
-    ...     "The example now displays correctly. Thanks.")
-    >>> owner_browser.getControl('Just Add a Comment').click()
+    >>> owner_browser.getControl(
+    ...     "Message"
+    ... ).value = "The example now displays correctly. Thanks."
+    >>> owner_browser.getControl("Just Add a Comment").click()
 
 This appends the comment to the question and it doesn't change its
 status:
@@ -264,11 +283,12 @@ the original problem reappears. In this case, they can reopen the question
 by entering a new message and clicking the "I Still Need an Answer"
 button.
 
-    >>> owner_browser.getControl('Message').value = (
+    >>> owner_browser.getControl("Message").value = (
     ...     "Actually, there are still SVGs that do not display correctly. "
     ...     "For example, the following\n"
     ...     "http://people.w3.org/maxf/ChessGML/immortal.svg doesn't display "
-    ...     "correctly.")
+    ...     "correctly."
+    ... )
     >>> owner_browser.getControl("I Still Need an Answer").click()
 
 This appends the new information to the question discussion and changes
@@ -286,11 +306,11 @@ This also removes the highlighting from the previous answer and sets the
 answerer back to None.
 
     >>> soup = find_main_content(owner_browser.contents)
-    >>> bestAnswer = soup.find_all('div', 'boardComment')[-4]
-    >>> bestAnswer.find('strong') is None
+    >>> bestAnswer = soup.find_all("div", "boardComment")[-4]
+    >>> bestAnswer.find("strong") is None
     True
 
-    >>> bestAnswer.find('div', 'boardCommentBody editable-message-text')
+    >>> bestAnswer.find("div", "boardCommentBody editable-message-text")
     <div class="boardCommentBody editable-message-text"
     itemprop="commentText"><p>New version of the firefox package
     are available with SVG support enabled. You can use apt to
@@ -299,8 +319,11 @@ answerer back to None.
 In addition, this creates a reopening record that is displayed in the
 reopening portlet.
 
-    >>> print(extract_text(
-    ...     find_tag_by_id(owner_browser.contents, 'portlet-reopenings')))
+    >>> print(
+    ...     extract_text(
+    ...         find_tag_by_id(owner_browser.contents, "portlet-reopenings")
+    ...     )
+    ... )
     This question was reopened ... Sample Person
 
 
@@ -311,9 +334,10 @@ The owner can also give the solution to their own question. They simply have
 to enter their solution in the 'Message' box and click the 'Problem
 Solved' button.
 
-    >>> owner_browser.getControl('Message').value = (
+    >>> owner_browser.getControl("Message").value = (
     ...     "OK, this example requires some SVG features that will only be "
-    ...     "available in Firefox 2.0.")
+    ...     "available in Firefox 2.0."
+    ... )
     >>> owner_browser.getControl("Problem Solved").click()
 
 This appends the message to the question and sets its status to
@@ -323,9 +347,9 @@ message as the "Best answer".
     >>> find_request_status(owner_browser.contents)
     Status: Solved ...
 
-    >>> soup = find_tag_by_id(owner_browser.contents, 'portlet-details')
+    >>> soup = find_tag_by_id(owner_browser.contents, "portlet-details")
     >>> soup = find_main_content(owner_browser.contents)
-    >>> bestAnswer = soup.find('img', {'title': 'Marked as best answer'})
+    >>> bestAnswer = soup.find("img", {"title": "Marked as best answer"})
     >>> None == bestAnswer
     True
 
@@ -333,31 +357,33 @@ A message is displayed to the user confirming that the question is
 solved and suggesting that the user choose an answer that helped the
 question owner to solve their problem.
 
-    >>> for message in soup.find_all('div', 'informational message'):
+    >>> for message in soup.find_all("div", "informational message"):
     ...     print(extract_text(message))
+    ...
     Your question is solved. If a particular message helped you solve the
     problem, use the 'This solved my problem' button.
 
 If the user chooses a best answer, the author of that answer is
 attributed as the answerer.
 
-    >>> owner_browser.getControl('This Solved My Problem').click()
+    >>> owner_browser.getControl("This Solved My Problem").click()
     >>> find_request_status(owner_browser.contents)
     Status: Solved ...
 
 The answer's message is also highlighted as the best answer.
 
     >>> soup = find_main_content(owner_browser.contents)
-    >>> bestAnswer = soup.find('img', {'title' : 'Marked as best answer'})
+    >>> bestAnswer = soup.find("img", {"title": "Marked as best answer"})
     >>> print(bestAnswer)
     <img ... src="/@@/favourite-yes" ... title="Marked as best answer"/>
 
-    >>> answerer = bestAnswer.parent.find('a')
+    >>> answerer = bestAnswer.parent.find("a")
     >>> print(extract_text(answerer))
     No Privileges Person (no-priv)
 
     >>> message = soup.find(
-    ...     'div', 'boardCommentBody highlighted editable-message-text')
+    ...     "div", "boardCommentBody highlighted editable-message-text"
+    ... )
     >>> print(message)
     <div class="boardCommentBody highlighted editable-message-text"
     itemprop="commentText"><p>New version of the firefox package are
@@ -373,29 +399,30 @@ History
 
 The history of the question is available on the 'History' page.
 
-    >>> anon_browser.open(
-    ...     'http://launchpad.test/firefox/+question/2')
-    >>> anon_browser.getLink('History').click()
+    >>> anon_browser.open("http://launchpad.test/firefox/+question/2";)
+    >>> anon_browser.getLink("History").click()
     >>> print(anon_browser.title)
     History of question #2...
 
 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.find_all('th'):
+    >>> action_listing = soup.find("table", "listing")
+    >>> for header in action_listing.find_all("th"):
     ...     print(header.decode_contents())
+    ...
     When
     Who
     Action
     New State
 
-    >>> for row in action_listing.find('tbody').find_all('tr'):
-    ...     cells = row.find_all('td')
-    ...     who = extract_text(cells[1].find('a'))
+    >>> for row in action_listing.find("tbody").find_all("tr"):
+    ...     cells = row.find_all("td")
+    ...     who = extract_text(cells[1].find("a"))
     ...     action = cells[2].decode_contents()
     ...     new_status = cells[3].decode_contents()
-    ...     print(who.lstrip('&nbsp;'), action, new_status)
+    ...     print(who.lstrip("&nbsp;"), action, new_status)
+    ...
     No Privileges Person Request for more information Needs information
     No Privileges Person Comment Needs information
     Sample Person        Give more information        Open
@@ -419,24 +446,28 @@ is able to solve the problem on his own, and submits the solution for
 other users with similar problems. He does not see a notice about
 choosing an answer that helped him solve his problem.
 
-    >>> carlos_browser = setupBrowser(auth='Basic carlos@xxxxxxxxxxxxx:test')
-    >>> carlos_browser.open('http://launchpad.test/firefox/+question/12')
+    >>> carlos_browser = setupBrowser(auth="Basic carlos@xxxxxxxxxxxxx:test")
+    >>> carlos_browser.open("http://launchpad.test/firefox/+question/12";)
     >>> print(find_request_status(carlos_browser.contents))
     Status: Open ...
 
     >>> answer_button_paragraph = find_tag_by_id(
-    ...     carlos_browser.contents, 'answer-button-hint')
+    ...     carlos_browser.contents, "answer-button-hint"
+    ... )
     >>> answer_button_paragraph is None
     True
 
-    >>> carlos_browser.getControl('Message').value = (
-    ...     "There is a bug in that version. SMP is fine after upgrading.")
+    >>> carlos_browser.getControl(
+    ...     "Message"
+    ... ).value = (
+    ...     "There is a bug in that version. SMP is fine after upgrading."
+    ... )
     >>> carlos_browser.getControl("Problem Solved").click()
     >>> print(find_request_status(carlos_browser.contents))
     Status: Solved ...
 
     >>> content = find_main_content(carlos_browser.contents)
-    >>> messages = content.find_all('div', 'informational message')
+    >>> messages = content.find_all("div", "informational message")
     >>> messages
     []
 
@@ -453,12 +484,12 @@ No Privileges Person (a different user from the one above) discovers the
 Firefox question. The solution does not work, but they think they have a
 similar problem so they ask their own question.
 
-    >>> user_browser.open('http://launchpad.test/firefox/+question/2')
+    >>> user_browser.open("http://launchpad.test/firefox/+question/2";)
 
     >>> content = find_main_content(user_browser.contents)
-    >>> print(content.find(id='can-you-help-with-this-problem'))
+    >>> print(content.find(id="can-you-help-with-this-problem"))
     None
 
-    >>> user_browser.getLink('Ask a question').click()
+    >>> user_browser.getLink("Ask a question").click()
     >>> print(user_browser.title)
     Ask a question about...
diff --git a/lib/lp/answers/stories/questions-index.rst b/lib/lp/answers/stories/questions-index.rst
index 99969d7..7aee544 100644
--- a/lib/lp/answers/stories/questions-index.rst
+++ b/lib/lp/answers/stories/questions-index.rst
@@ -8,26 +8,28 @@ First, we need to set some values for the later tests.
     >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> from lp.registry.interfaces.product import IProductSet
     >>> from zope.component import getUtility
-    >>> login('admin@xxxxxxxxxxxxx')
-    >>> firefox = getUtility(IProductSet).getByName('firefox')
-    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
+    >>> login("admin@xxxxxxxxxxxxx")
+    >>> firefox = getUtility(IProductSet).getByName("firefox")
+    >>> ubuntu = getUtility(IDistributionSet).getByName("ubuntu")
     >>> firefox.answers_usage = ServiceUsage.LAUNCHPAD
     >>> ubuntu.answers_usage = ServiceUsage.LAUNCHPAD
     >>> logout()
     >>> transaction.commit()
 
-    >>> anon_browser.open('http://answers.launchpad.test/')
+    >>> anon_browser.open("http://answers.launchpad.test/";)
     >>> print(anon_browser.title)
     Launchpad Answers
 
 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').decode_contents())
+    ...     anon_browser.contents, "latest-questions-asked"
+    ... )
+    >>> print(latest_questions_asked.find("h2").decode_contents())
     Latest questions asked
-    >>> for row in latest_questions_asked.find_all('li'):
-    ...     print(row.find('a').decode_contents())
+    >>> for row in latest_questions_asked.find_all("li"):
+    ...     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
@@ -37,19 +39,30 @@ It shows the 5 latest questions asked:
 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').decode_contents())
+    ...     anon_browser.contents, "latest-questions-solved"
+    ... )
+    >>> print(latest_questions_solved.find("h2").decode_contents())
     Latest questions solved
-    >>> for row in latest_questions_solved.find_all('li'):
-    ...     print(row.find('a').decode_contents())
+    >>> for row in latest_questions_solved.find_all("li"):
+    ...     print(row.find("a").decode_contents())
+    ...
     9: mailto: problem in webpage
 
 The application footer also contains a sample of stats for the application:
 
     # Replace numbers with X in output.
     >>> import re
-    >>> print(re.sub('\d+', 'X', extract_text(find_tag_by_id(
-    ...     anon_browser.contents, 'application-footer'))))
+    >>> print(
+    ...     re.sub(
+    ...         "\d+",
+    ...         "X",
+    ...         extract_text(
+    ...             find_tag_by_id(
+    ...                 anon_browser.contents, "application-footer"
+    ...             )
+    ...         ),
+    ...     )
+    ... )
     X questions answered and X questions solved out of
     X questions asked across X projects
 
@@ -57,23 +70,27 @@ The page also contains the projects actively using the Answer tracker.
 (Since sample data contains no projects with a question asked in the
 last 60 days, this list is empty):
 
-    >>> print(extract_text(find_tag_by_id(
-    ...     anon_browser.contents, 'most-active-projects')))
+    >>> print(
+    ...     extract_text(
+    ...         find_tag_by_id(anon_browser.contents, "most-active-projects")
+    ...     )
+    ... )
     Most active projects
 
 Add some recent questions so that this listing contains something.
 
     >>> from lp.answers.testing import QuestionFactory
     >>> from lp.testing import login, logout
-    >>> login('no-priv@xxxxxxxxxxxxx')
-    >>> QuestionFactory.createManyByProject([
-    ...    ('ubuntu', 2),
-    ...    ('firefox', 1)])
+    >>> login("no-priv@xxxxxxxxxxxxx")
+    >>> QuestionFactory.createManyByProject([("ubuntu", 2), ("firefox", 1)])
     >>> logout()
 
-    >>> anon_browser.open('http://answers.launchpad.test/')
-    >>> print(extract_text(find_tag_by_id(
-    ...     anon_browser.contents, 'most-active-projects')))
+    >>> anon_browser.open("http://answers.launchpad.test/";)
+    >>> print(
+    ...     extract_text(
+    ...         find_tag_by_id(anon_browser.contents, "most-active-projects")
+    ...     )
+    ... )
     Most active projects
     Ubuntu
     Mozilla Firefox
@@ -81,7 +98,7 @@ Add some recent questions so that this listing contains something.
 Clicking on these project links will bring the user to the project
 Answers front page:
 
-    >>> anon_browser.getLink('Ubuntu').click()
+    >>> anon_browser.getLink("Ubuntu").click()
     >>> print(anon_browser.url)
     http://answers.launchpad.test/ubuntu
     >>> print(anon_browser.title)
diff --git a/lib/lp/answers/stories/this-is-a-faq.rst b/lib/lp/answers/stories/this-is-a-faq.rst
index acb4ebb..948c5fa 100644
--- a/lib/lp/answers/stories/this-is-a-faq.rst
+++ b/lib/lp/answers/stories/this-is-a-faq.rst
@@ -18,10 +18,10 @@ less prose permits better matching. See bug 612384.
     >>> from zope.component import getUtility
     >>> from lp.registry.interfaces.product import IProductSet
     >>> from lp.testing import login, logout
-    >>> login('test@xxxxxxxxxxxxx')
-    >>> firefox = getUtility(IProductSet)['firefox']
+    >>> login("test@xxxxxxxxxxxxx")
+    >>> firefox = getUtility(IProductSet)["firefox"]
     >>> svg_question = firefox.getQuestion(2)
-    >>> svg_question.title = 'SVG extension'
+    >>> svg_question.title = "SVG extension"
     >>> logout()
 
 
@@ -37,12 +37,11 @@ on 'Link to a FAQ' to answer the question:
 
     # We use backslashreplace because the page title includes smart quotes.
     >>> from lp.services.helpers import backslashreplace
-    >>> user_browser.open(
-    ...     'http://answers.launchpad.test/firefox/+question/2')
+    >>> user_browser.open("http://answers.launchpad.test/firefox/+question/2";)
     >>> print(backslashreplace(user_browser.title))
     Question #2 : ...
 
-    >>> user_browser.getLink('Link to a FAQ').click()
+    >>> user_browser.getLink("Link to a FAQ").click()
     >>> print(backslashreplace(user_browser.title))
     Is question #2 a FAQ...
 
@@ -54,31 +53,33 @@ no FAQ is associated to the question, the 'No existing FAQs are
 relevant' option is selected.
 
     >>> def printFAQOptions(contents):
-    ...     buttons =  find_main_content(contents).find_all(
-    ...         'input', {'name': 'field.faq'})
+    ...     buttons = find_main_content(contents).find_all(
+    ...         "input", {"name": "field.faq"}
+    ...     )
     ...     for button in buttons:
     ...         label = extract_text(button.parent)
-    ...         if button.get('checked', None):
-    ...             radio = '(*)'
+    ...         if button.get("checked", None):
+    ...             radio = "(*)"
     ...         else:
-    ...             radio = '( )'
-    ...         if button['value']:
-    ...             link = button.find_next('a').decode_contents()
+    ...             radio = "( )"
+    ...         if button["value"]:
+    ...             link = button.find_next("a").decode_contents()
     ...         else:
-    ...             link = ''
+    ...             link = ""
     ...         print(radio, label, link)
+    ...
     >>> printFAQOptions(user_browser.contents)
     (*) No existing FAQs are relevant
     ( ) 8: How do I install Extensions?
     ( ) 9: How do I troubleshoot problems with extensions/themes?
 
-    >>> print(user_browser.getLink('How do I troubleshoot problems').url)
+    >>> print(user_browser.getLink("How do I troubleshoot problems").url)
     http://answers.launchpad.test/firefox/+faq/9
 
 The query used to find these results is displayed in the search field
 under the radio widgets. That query defaults to the question's title.
 
-    >>> search_field = user_browser.getControl(name='field.faq-query')
+    >>> search_field = user_browser.getControl(name="field.faq-query")
     >>> print(search_field.value)
     SVG extension
 
@@ -86,8 +87,8 @@ From the titles, it doesn't seem like one of these FAQs would be
 appropriate. The user can modify the search query and hit the 'Search'
 button to update the list of available choices:
 
-    >>> search_field.value = 'SVG plugin'
-    >>> user_browser.getControl('Search', index=0).click()
+    >>> search_field.value = "SVG plugin"
+    >>> user_browser.getControl("Search", index=0).click()
 
 The page is updated with a new list of FAQs:
 
@@ -101,44 +102,56 @@ The page is updated with a new list of FAQs:
 The most relevant result seems like the good answer, so the user selects
 it.
 
-    >>> user_browser.getControl('10').selected = True
+    >>> user_browser.getControl("10").selected = True
 
 There is a 'Message' field that will be used to answer the question.
 It is pre-filled, but they can change its value. The FAQ reference will
 be appended to the message.
 
-    >>> print(user_browser.getControl('Message').value)
+    >>> print(user_browser.getControl("Message").value)
     No Privileges Person suggests this article as an answer to your question:
 
 They can then click 'Link to FAQ' to answer the question with the selected
 FAQ. After clicking the button, the user is redirected to the question
 page.
 
-    >>> user_browser.getControl('Link to FAQ').click()
+    >>> user_browser.getControl("Link to FAQ").click()
     >>> print(user_browser.url)
     http://answers.launchpad.test/firefox/+question/2
 
 They see that the question's status was changed to 'Answered':
 
     >>> def print_question_status(browser):
-    ...     print(extract_text(
-    ...         find_tag_by_id(browser.contents, 'question-status')))
+    ...     print(
+    ...         extract_text(
+    ...             find_tag_by_id(browser.contents, "question-status")
+    ...         )
+    ...     )
+    ...
 
     >>> print_question_status(user_browser)
     Status: Answered
 
 A link to the FAQ appears under the question's description:
 
-    >>> print(extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'related-faq')))
+    >>> print(
+    ...     extract_text(find_tag_by_id(user_browser.contents, "related-faq"))
+    ... )
     Related FAQ: How do I install plugins (Shockwave, QuickTime, etc.)? ...
-    >>> print(user_browser.getLink('How do I install plugins').url)
+    >>> print(user_browser.getLink("How do I install plugins").url)
     http://answers.launchpad.test/firefox/+faq/10
 
 The answer message was added to the question's discussion:
 
-    >>> print(backslashreplace(extract_text(find_tags_by_class(
-    ...     user_browser.contents, 'boardCommentBody')[-1])))
+    >>> print(
+    ...     backslashreplace(
+    ...         extract_text(
+    ...             find_tags_by_class(
+    ...                 user_browser.contents, "boardCommentBody"
+    ...             )[-1]
+    ...         )
+    ...     )
+    ... )
     No Privileges Person suggests this article as an answer to your question:
     FAQ #10: \u201cHow do I install plugins...
 
@@ -152,7 +165,7 @@ the FAQ that they just linked and found that it doesn't really answer
 the question. To correct the mistake, they use the same 'Link to a FAQ'
 action.
 
-    >>> user_browser.getLink('Link to a FAQ').click()
+    >>> user_browser.getLink("Link to a FAQ").click()
 
 The existing linked FAQ is selected and the other FAQs matching the
 question's title are displayed:
@@ -165,9 +178,10 @@ question's title are displayed:
 
 They change the message and click 'Link to FAQ'.
 
-    >>> user_browser.getControl('Message').value = (
-    ...     "Sorry, this document doesn't really answer your question.")
-    >>> user_browser.getControl('Link to FAQ').click()
+    >>> user_browser.getControl(
+    ...     "Message"
+    ... ).value = "Sorry, this document doesn't really answer your question."
+    >>> user_browser.getControl("Link to FAQ").click()
 
 But since they forgot to change the link, the form is displayed again
 with an error message.
@@ -181,19 +195,27 @@ with an error message.
 To remove the FAQ, the user selects the 'No existing...' option and
 submit the form again.
 
-    >>> user_browser.getControl('No existing FAQs').selected = True
-    >>> user_browser.getControl('Link to FAQ').click()
+    >>> user_browser.getControl("No existing FAQs").selected = True
+    >>> user_browser.getControl("Link to FAQ").click()
 
 The new message was added to the question:
 
-    >>> print(backslashreplace(extract_text(find_tags_by_class(
-    ...     user_browser.contents, 'boardCommentBody')[-1])))
+    >>> print(
+    ...     backslashreplace(
+    ...         extract_text(
+    ...             find_tags_by_class(
+    ...                 user_browser.contents, "boardCommentBody"
+    ...             )[-1]
+    ...         )
+    ...     )
+    ... )
     Sorry, this document doesn't really answer your question.
 
 The link was also removed from the details portlet:
 
-    >>> print(extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'related-faq')))
+    >>> print(
+    ...     extract_text(find_tag_by_id(user_browser.contents, "related-faq"))
+    ... )
     Related FAQ: None ...
 
 
@@ -208,33 +230,35 @@ answer contacts and the project's owner).
 Since No Privileges Person isn't an answer contact for the project nor
 the project owner, they don't have the possibility to create a new FAQ.
 
-    >>> user_browser.getLink('Create a FAQ')
+    >>> user_browser.getLink("Create a FAQ")
     Traceback (most recent call last):
       ...
     zope.testbrowser.browser.LinkNotFoundError
 
-    >>> user_browser.getLink('Link to a FAQ').click()
-    >>> user_browser.getLink('create a new FAQ')
+    >>> user_browser.getLink("Link to a FAQ").click()
+    >>> user_browser.getLink("create a new FAQ")
     Traceback (most recent call last):
       ...
     zope.testbrowser.browser.LinkNotFoundError
 
     >>> user_browser.open(
-    ...     'http://answers.launchpad.test/firefox/+question/2/+createfaq')
+    ...     "http://answers.launchpad.test/firefox/+question/2/+createfaq";
+    ... )
     Traceback (most recent call last):
       ...
     zope.security.interfaces.Unauthorized: ...
 
 Sample Person who is the project owner does have that ability.
 
-    >>> owner_browser = setupBrowser(auth='Basic test@xxxxxxxxxxxxx:test')
+    >>> owner_browser = setupBrowser(auth="Basic test@xxxxxxxxxxxxx:test")
     >>> owner_browser.open(
-    ...     'http://answers.launchpad.test/firefox/+question/2')
-    >>> owner_browser.getLink('Create a new FAQ')
+    ...     "http://answers.launchpad.test/firefox/+question/2";
+    ... )
+    >>> owner_browser.getLink("Create a new FAQ")
     <Link text='Create a new FAQ'
           url='http://.../firefox/+question/2/+createfaq'>
-    >>> owner_browser.getLink('Link to a FAQ').click()
-    >>> owner_browser.getLink('create a new FAQ').click()
+    >>> owner_browser.getLink("Link to a FAQ").click()
+    >>> owner_browser.getLink("create a new FAQ").click()
     >>> print(owner_browser.url)
     http://answers.launchpad.test/firefox/+question/2/+createfaq
     >>> print(owner_browser.title)
@@ -243,52 +267,65 @@ Sample Person who is the project owner does have that ability.
 The FAQ title and content are pre-filled with the target question. They
 edit them to be more appropriate:
 
-    >>> print(owner_browser.getControl('Title').value)
+    >>> print(owner_browser.getControl("Title").value)
     SVG extension
-    >>> owner_browser.getControl('Title').value = 'Displaying SVG in Firefox'
+    >>> owner_browser.getControl("Title").value = "Displaying SVG in Firefox"
 
-    >>> print(owner_browser.getControl('Content').value)
+    >>> print(owner_browser.getControl("Content").value)
     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.
 
-    >>> owner_browser.getControl('Content').value = (
-    ...     'Upgrade your browser to Firefox 2.0.')
+    >>> owner_browser.getControl(
+    ...     "Content"
+    ... ).value = "Upgrade your browser to Firefox 2.0."
 
 They can also enter keywords describing the FAQ:
 
-    >>> owner_browser.getControl('Keywords').value = (
-    ...     'scalable vector graphic')
+    >>> owner_browser.getControl("Keywords").value = "scalable vector graphic"
 
 There is a 'Message' field that will be used to answer the question.
 It is pre-filled, but they can change its value:
 
-    >>> print(owner_browser.getControl(
-    ...     'Additional comment for question #2').value)
+    >>> print(
+    ...     owner_browser.getControl(
+    ...         "Additional comment for question #2"
+    ...     ).value
+    ... )
     Sample Person suggests this article as an answer to your question:
 
     >>> owner_browser.getControl(
-    ...     'Additional comment for question #2').value = (
-    ...     'Read the Fine Answer:')
+    ...     "Additional comment for question #2"
+    ... ).value = "Read the Fine Answer:"
 
 After clicking the 'Create' button, the FAQ is created and the user is
 returned to the question page.
 
-    >>> owner_browser.getControl('Create and Link').click()
+    >>> owner_browser.getControl("Create and Link").click()
     >>> print(owner_browser.url)
     http://answers.launchpad.test/firefox/+question/2
 
 The answer message was added to the question's discussion:
 
-    >>> print(backslashreplace(extract_text(find_tags_by_class(
-    ...     owner_browser.contents, 'boardCommentBody')[-1])))
+    >>> print(
+    ...     backslashreplace(
+    ...         extract_text(
+    ...             find_tags_by_class(
+    ...                 owner_browser.contents, "boardCommentBody"
+    ...             )[-1]
+    ...         )
+    ...     )
+    ... )
     Read the Fine Answer:
     FAQ...: \u201cDisplaying SVG in Firefox\u201d.
 
 And the link to the created FAQ is displayed under the question's
 description:
 
-    >>> print(extract_text(
-    ...     find_tag_by_id(owner_browser.contents, 'related-faq')))
+    >>> print(
+    ...     extract_text(
+    ...         find_tag_by_id(owner_browser.contents, "related-faq")
+    ...     )
+    ... )
     Related FAQ: Displaying SVG in Firefox ...
 
 
@@ -298,7 +335,7 @@ Viewing a FAQ
 From a question page which has a related FAQ, the user can click on the
 FAQ title to display the FAQ content.
 
-    >>> owner_browser.getLink('Displaying SVG in Firefox').click()
+    >>> owner_browser.getLink("Displaying SVG in Firefox").click()
     >>> print(owner_browser.url)
     http://answers.launchpad.test/firefox/+faq/...
     >>> print(backslashreplace(owner_browser.title))
@@ -306,29 +343,41 @@ FAQ title to display the FAQ content.
 
 The FAQ keywords and content appears just below:
 
-    >>> print(extract_text(find_tag_by_id(
-    ...     owner_browser.contents, 'faq-keywords')))
+    >>> print(
+    ...     extract_text(
+    ...         find_tag_by_id(owner_browser.contents, "faq-keywords")
+    ...     )
+    ... )
     Keywords: scalable vector graphic
 
-    >>> print(extract_text(find_tag_by_id(
-    ...     owner_browser.contents, 'faq-content')))
+    >>> print(
+    ...     extract_text(
+    ...         find_tag_by_id(owner_browser.contents, "faq-content")
+    ...     )
+    ... )
     Upgrade your browser to Firefox 2.0.
 
 The FAQ's original author and creation date appears in the header:
 
-    >>> print(extract_text(
-    ...     find_tag_by_id(owner_browser.contents, 'registration')))
+    >>> print(
+    ...     extract_text(
+    ...         find_tag_by_id(owner_browser.contents, "registration")
+    ...     )
+    ... )
     Created by Sample Person ...
 
 A 'Related questions' portlet contains links to the question answered by
 the FAQ:
 
-    >>> print(extract_text(find_portlet(
-    ...     owner_browser.contents, 'Related questions')))
+    >>> print(
+    ...     extract_text(
+    ...         find_portlet(owner_browser.contents, "Related questions")
+    ...     )
+    ... )
     Related questions
     #2 SVG extension
 
-    >>> print(owner_browser.getLink('SVG extension').url)
+    >>> print(owner_browser.getLink("SVG extension").url)
     http://answers.launchpad.test/firefox/+question/2
 
 
@@ -338,20 +387,20 @@ Distribution and Source Packages
 Questions asked about a distribution or distribution source package
 can also be linked to FAQs.
 
-    >>> user_browser.open(
-    ...     'http://answers.launchpad.test/ubuntu/+question/11')
+    >>> user_browser.open("http://answers.launchpad.test/ubuntu/+question/11";)
     >>> print(user_browser.title)
     Question #11 : ...
-    >>> user_browser.getLink('Link to a FAQ').click()
+    >>> user_browser.getLink("Link to a FAQ").click()
     >>> print(user_browser.title)
     Is question #11 a FAQ...
 
     >>> user_browser.open(
-    ...     'http://answers.launchpad.test/ubuntu/+source/mozilla-firefox'
-    ...     '/+question/8')
+    ...     "http://answers.launchpad.test/ubuntu/+source/mozilla-firefox";
+    ...     "/+question/8"
+    ... )
     >>> print(user_browser.title)
     Question #8 : ...
-    >>> user_browser.getLink('Link to a FAQ').click()
+    >>> user_browser.getLink("Link to a FAQ").click()
     >>> user_browser.title
     'Is question #8 a FAQ...
 
@@ -367,21 +416,23 @@ FAQ. They decided to add it to the question to provide additional
 information.
 
     >>> user_browser.open(
-    ...     'http://answers.launchpad.test/ubuntu/+source/mozilla-firefox/'
-    ...     '+question/9')
+    ...     "http://answers.launchpad.test/ubuntu/+source/mozilla-firefox/";
+    ...     "+question/9"
+    ... )
     >>> details_portlet = find_portlet(
-    ...     user_browser.contents, 'mozilla-firefox in ubuntu question #9')
+    ...     user_browser.contents, "mozilla-firefox in ubuntu question #9"
+    ... )
     >>> print_question_status(user_browser)
     Status: Solved
-    >>> user_browser.getLink('Link to a FAQ').click()
+    >>> user_browser.getLink("Link to a FAQ").click()
 
     >>> print(user_browser.title)
     Is question #9 a FAQ...
-    >>> user_browser.getControl(name='field.faq-query').value = 'flash'
-    >>> user_browser.getControl('Search', index=0).click()
-    >>> user_browser.getControl('6').selected = True
-    >>> user_browser.getControl('Message').value = "The FAQ mentions this:"
-    >>> user_browser.getControl('Link to FAQ').click()
+    >>> user_browser.getControl(name="field.faq-query").value = "flash"
+    >>> user_browser.getControl("Search", index=0).click()
+    >>> user_browser.getControl("6").selected = True
+    >>> user_browser.getControl("Message").value = "The FAQ mentions this:"
+    >>> user_browser.getControl("Link to FAQ").click()
 
 The question is still solved. No Privileges Person sees the FAQ was
 added to the question, and their message was added to the question's
@@ -392,13 +443,21 @@ discussion.
     >>> print_question_status(user_browser)
     Status: Solved
 
-    >>> print(extract_text(
-    ...     find_tag_by_id(user_browser.contents, 'related-faq')))
+    >>> print(
+    ...     extract_text(find_tag_by_id(user_browser.contents, "related-faq"))
+    ... )
     Related FAQ:
     How can I play MP3/Divx/DVDs/Quicktime/Realmedia files ...
 
-    >>> print(backslashreplace(extract_text(find_tags_by_class(
-    ...     user_browser.contents, 'boardCommentBody')[-1])))
+    >>> print(
+    ...     backslashreplace(
+    ...         extract_text(
+    ...             find_tags_by_class(
+    ...                 user_browser.contents, "boardCommentBody"
+    ...             )[-1]
+    ...         )
+    ...     )
+    ... )
     The FAQ mentions this:
     FAQ #6: ...How can I play MP3/Divx/DVDs/Quicktime/Realmedia files...
 
@@ -410,13 +469,13 @@ You can respond to a question by pointing people to a FAQ. FAQs are
 linkified as you would expect! You can use the "this is a FAQ" menu
 item, as above:
 
-    >>> user_browser.getLink('FAQ #6').url
+    >>> user_browser.getLink("FAQ #6").url
     'http://answers.launchpad.test/ubuntu/+faq/6'
 
 Or you can just refer to FAQs in comments:
 
-    >>> user_browser.getControl('Message').value = 'No, this is FAQ #2'
-    >>> user_browser.getControl('Just Add a Comment').click()
+    >>> user_browser.getControl("Message").value = "No, this is FAQ #2"
+    >>> user_browser.getControl("Just Add a Comment").click()
     >>> user_browser.getLink("FAQ #2").url
     'http://answers.launchpad.test/ubuntu/+faq/2'
 
diff --git a/lib/lp/answers/stories/webservice.rst b/lib/lp/answers/stories/webservice.rst
index 0954ced..54e0e2a 100644
--- a/lib/lp/answers/stories/webservice.rst
+++ b/lib/lp/answers/stories/webservice.rst
@@ -15,28 +15,33 @@ contact, and asker, and three questions.
     >>> lang_set = getUtility(ILanguageSet)
 
     >>> login(ADMIN_EMAIL)
-    >>> _contact = factory.makePerson(name='contact')
-    >>> _project = factory.makeProduct(name='my-project', owner=_contact)
-    >>> _contact.addLanguage(lang_set['en'])
+    >>> _contact = factory.makePerson(name="contact")
+    >>> _project = factory.makeProduct(name="my-project", owner=_contact)
+    >>> _contact.addLanguage(lang_set["en"])
     >>> _project.answers_usage = ServiceUsage.LAUNCHPAD
     >>> success = _project.addAnswerContact(_contact, _contact)
     >>> _team = factory.makeTeam(
     ...     owner=_contact,
-    ...     name='my-team',
-    ...     membership_policy=TeamMembershipPolicy.RESTRICTED)
-    >>> _team_project = factory.makeProduct(name='team-project', owner=_team)
-    >>> _asker = factory.makePerson(name='asker')
+    ...     name="my-team",
+    ...     membership_policy=TeamMembershipPolicy.RESTRICTED,
+    ... )
+    >>> _team_project = factory.makeProduct(name="team-project", owner=_team)
+    >>> _asker = factory.makePerson(name="asker")
     >>> _question_1 = factory.makeQuestion(
-    ...     target=_project, title="Q 1 great", owner=_asker)
+    ...     target=_project, title="Q 1 great", owner=_asker
+    ... )
     >>> _question_2 = factory.makeQuestion(
-    ...     target=_project, title="Q 2 greater", owner=_asker)
+    ...     target=_project, title="Q 2 greater", owner=_asker
+    ... )
     >>> _question_3 = factory.makeQuestion(
-    ...     target=_team_project, title="Q 3 greatest", owner=_asker)
-    >>> _message = _question_1.giveAnswer(_contact, 'This is the answer')
+    ...     target=_team_project, title="Q 3 greatest", owner=_asker
+    ... )
+    >>> _message = _question_1.giveAnswer(_contact, "This is the answer")
     >>> logout()
 
     >>> contact_webservice = webservice_for_person(
-    ...     _contact, permission=OAuthPermission.WRITE_PUBLIC)
+    ...     _contact, permission=OAuthPermission.WRITE_PUBLIC
+    ... )
 
 
 Answer contacts
@@ -48,50 +53,74 @@ language. Scripts should call the canUserAlterAnswerContact method first to
 verify that the person can changed.
 
     >>> project = contact_webservice.get(
-    ...     '/my-project', api_version='devel').jsonBody()
+    ...     "/my-project", api_version="devel"
+    ... ).jsonBody()
     >>> contact = contact_webservice.get(
-    ...     '/~contact', api_version='devel').jsonBody()
+    ...     "/~contact", api_version="devel"
+    ... ).jsonBody()
     >>> contact_webservice.named_get(
-    ...     project['self_link'], 'canUserAlterAnswerContact',
-    ...     person=contact['self_link'], api_version='devel').jsonBody()
+    ...     project["self_link"],
+    ...     "canUserAlterAnswerContact",
+    ...     person=contact["self_link"],
+    ...     api_version="devel",
+    ... ).jsonBody()
     True
 
     >>> contact_webservice.named_post(
-    ...     project['self_link'], 'removeAnswerContact',
-    ...     person=contact['self_link'], api_version='devel').jsonBody()
+    ...     project["self_link"],
+    ...     "removeAnswerContact",
+    ...     person=contact["self_link"],
+    ...     api_version="devel",
+    ... ).jsonBody()
     True
 
     >>> contact_webservice.named_post(
-    ...     project['self_link'], 'addAnswerContact',
-    ...     person=contact['self_link'], api_version='devel').jsonBody()
+    ...     project["self_link"],
+    ...     "addAnswerContact",
+    ...     person=contact["self_link"],
+    ...     api_version="devel",
+    ... ).jsonBody()
     True
 
 Users can also make the teams they administer answer contacts using
 addAnswerContact and removeAnswerContact if the team has a preferred language.
 
     >>> team = contact_webservice.get(
-    ...     '/~my-team', api_version='devel').jsonBody()
+    ...     "/~my-team", api_version="devel"
+    ... ).jsonBody()
     >>> team_project = contact_webservice.get(
-    ...     '/team-project', api_version='devel').jsonBody()
+    ...     "/team-project", api_version="devel"
+    ... ).jsonBody()
     >>> contact_webservice.named_get(
-    ...     team_project['self_link'], 'canUserAlterAnswerContact',
-    ...     person=team['self_link'], api_version='devel').jsonBody()
+    ...     team_project["self_link"],
+    ...     "canUserAlterAnswerContact",
+    ...     person=team["self_link"],
+    ...     api_version="devel",
+    ... ).jsonBody()
     True
 
     >>> contact_webservice.named_post(
-    ...     team['self_link'], 'addLanguage',
-    ...     language='/+languages/fr', api_version='devel').jsonBody()
+    ...     team["self_link"],
+    ...     "addLanguage",
+    ...     language="/+languages/fr",
+    ...     api_version="devel",
+    ... ).jsonBody()
     >>> contact_webservice.named_post(
-    ...     team_project['self_link'], 'addAnswerContact',
-    ...     person=team['self_link'], api_version='devel').jsonBody()
+    ...     team_project["self_link"],
+    ...     "addAnswerContact",
+    ...     person=team["self_link"],
+    ...     api_version="devel",
+    ... ).jsonBody()
     True
 
 Anyone can get the collection of languages spoken by at least one
 answer contact by calling getSupportedLanguages.
 
     >>> languages = anon_webservice.named_get(
-    ...     team_project['self_link'], 'getSupportedLanguages',
-    ...     api_version='devel').jsonBody()
+    ...     team_project["self_link"],
+    ...     "getSupportedLanguages",
+    ...     api_version="devel",
+    ... ).jsonBody()
     >>> print_self_link_of_entries(languages)
     http://.../+languages/en
     http://.../+languages/fr
@@ -100,11 +129,15 @@ Anyone can retrieve the collection of answer contacts for a language using
 getAnswerContactsForLanguage.
 
     >>> english = anon_webservice.get(
-    ...     '/+languages/en', api_version='devel').jsonBody()
+    ...     "/+languages/en", api_version="devel"
+    ... ).jsonBody()
 
     >>> contacts = anon_webservice.named_get(
-    ...     project['self_link'], 'getAnswerContactsForLanguage',
-    ...     language=english['self_link'], api_version='devel').jsonBody()
+    ...     project["self_link"],
+    ...     "getAnswerContactsForLanguage",
+    ...     language=english["self_link"],
+    ...     api_version="devel",
+    ... ).jsonBody()
     >>> print_self_link_of_entries(contacts)
     http://.../~contact
 
@@ -112,8 +145,10 @@ Anyone can retrieve the collection of `IQuestionTarget`s that a person
 is an answer contact for using getDirectAnswerQuestionTargets.
 
     >>> targets = anon_webservice.named_get(
-    ...     contact['self_link'], 'getDirectAnswerQuestionTargets',
-    ...     api_version='devel').jsonBody()
+    ...     contact["self_link"],
+    ...     "getDirectAnswerQuestionTargets",
+    ...     api_version="devel",
+    ... ).jsonBody()
     >>> print_self_link_of_entries(targets)
     http://api.launchpad.test/devel/my-project
 
@@ -121,8 +156,10 @@ Anyone can retrieve the collection of `IQuestionTarget`s that a person's
 teams is an answer contact for using getTeamAnswerQuestionTargets.
 
     >>> targets = anon_webservice.named_get(
-    ...     contact['self_link'], 'getTeamAnswerQuestionTargets',
-    ...     api_version='devel').jsonBody()
+    ...     contact["self_link"],
+    ...     "getTeamAnswerQuestionTargets",
+    ...     api_version="devel",
+    ... ).jsonBody()
     >>> print_self_link_of_entries(targets)
     http://api.launchpad.test/devel/team-project
 
@@ -135,17 +172,20 @@ searchQuestions. The question will that match the precise search criteria
 called with searchQuestions.
 
     >>> questions = anon_webservice.named_get(
-    ...     project['self_link'], 'searchQuestions',
-    ...     search_text='q great',
-    ...     status=['Open', 'Needs information', 'Answered'],
-    ...     language=[english['self_link']],
-    ...     sort='oldest first',
-    ...     api_version='devel').jsonBody()
-    >>> for question in questions['entries']:
-    ...     print(question['title'])
+    ...     project["self_link"],
+    ...     "searchQuestions",
+    ...     search_text="q great",
+    ...     status=["Open", "Needs information", "Answered"],
+    ...     language=[english["self_link"]],
+    ...     sort="oldest first",
+    ...     api_version="devel",
+    ... ).jsonBody()
+    >>> for question in questions["entries"]:
+    ...     print(question["title"])
+    ...
     Q 1 great
 
-    >>> print(questions['total_size'])
+    >>> print(questions["total_size"])
     1
 
 Anyone can retrieve a collection of questions from an `IQuestionTarget` that
@@ -154,11 +194,14 @@ words that might appear in a question's title or description.
 findSimilarQuestions uses natural language techniques to match the question.
 
     >>> questions = anon_webservice.named_get(
-    ...     project['self_link'], 'findSimilarQuestions',
-    ...     phrase='q great',
-    ...     api_version='devel').jsonBody()
-    >>> for question in questions['entries']:
-    ...     print(question['title'])
+    ...     project["self_link"],
+    ...     "findSimilarQuestions",
+    ...     phrase="q great",
+    ...     api_version="devel",
+    ... ).jsonBody()
+    >>> for question in questions["entries"]:
+    ...     print(question["title"])
+    ...
     Q 1 great
     Q 2 greater
 
@@ -166,9 +209,12 @@ Anyone can retrieve a specific question from an `IQuestionTarget` calling
 getQuestion with the question Id.
 
     >>> question_1 = anon_webservice.named_get(
-    ...     project['self_link'], 'getQuestion', question_id=_question_1.id,
-    ...     api_version='devel').jsonBody()
-    >>> print(question_1['title'])
+    ...     project["self_link"],
+    ...     "getQuestion",
+    ...     question_id=_question_1.id,
+    ...     api_version="devel",
+    ... ).jsonBody()
+    >>> print(question_1["title"])
     Q 1 great
 
 
@@ -177,15 +223,18 @@ searchQuestions. The question will that match the precise search criteria
 called with searchQuestions.
 
     >>> questions = anon_webservice.named_get(
-    ...     contact['self_link'], 'searchQuestions',
-    ...     search_text='q great',
-    ...     status=['Open', 'Needs information', 'Answered'],
-    ...     language=[english['self_link']],
+    ...     contact["self_link"],
+    ...     "searchQuestions",
+    ...     search_text="q great",
+    ...     status=["Open", "Needs information", "Answered"],
+    ...     language=[english["self_link"]],
     ...     needs_attention=False,
-    ...     sort='oldest first',
-    ...     api_version='devel').jsonBody()
-    >>> for question in questions['entries']:
-    ...     print(question['title'])
+    ...     sort="oldest first",
+    ...     api_version="devel",
+    ... ).jsonBody()
+    >>> for question in questions["entries"]:
+    ...     print(question["title"])
+    ...
     Q 1 great
 
 
@@ -227,9 +276,9 @@ An `IQuestionMessage` provides the IMessage fields and additional fields
 that indicate how the message changed the question.
 
     >>> messages = anon_webservice.get(
-    ...     question_1['messages_collection_link'],
-    ...     api_version='devel').jsonBody()
-    >>> pprint_entry(messages['entries'][0])
+    ...     question_1["messages_collection_link"], api_version="devel"
+    ... ).jsonBody()
+    >>> pprint_entry(messages["entries"][0])
     action: 'Answer'
     bug_attachments_collection_link: '...'
     content: 'This is the answer'
diff --git a/lib/lp/answers/tests/emailinterface.rst b/lib/lp/answers/tests/emailinterface.rst
index cab2739..311f696 100644
--- a/lib/lp/answers/tests/emailinterface.rst
+++ b/lib/lp/answers/tests/emailinterface.rst
@@ -23,6 +23,7 @@ AnswerTrackerHandler.
     ...     while True:
     ...         yield now
     ...         now += timedelta(seconds=1)
+    ...
 
     # We are using a date in the past because MessageSet disallows the
     # creation of email message with a future date.
@@ -35,25 +36,26 @@ AnswerTrackerHandler.
     >>> handler = AnswerTrackerHandler()
     >>> def send_question_email(question_id, from_addr, subject, body):
     ...     login(from_addr)
-    ...     lines = ['From: %s' % from_addr]
-    ...     to_addr = 'question%s@xxxxxxxxxxxxxxxxxxxxx' % question_id
-    ...     lines.append('To: %s' % to_addr)
-    ...     date = mktime_tz(next(now).utctimetuple() + (0, ))
-    ...     lines.append('Date: %s' % formatdate(date))
+    ...     lines = ["From: %s" % from_addr]
+    ...     to_addr = "question%s@xxxxxxxxxxxxxxxxxxxxx" % question_id
+    ...     lines.append("To: %s" % to_addr)
+    ...     date = mktime_tz(next(now).utctimetuple() + (0,))
+    ...     lines.append("Date: %s" % formatdate(date))
     ...     msgid = make_msgid()
-    ...     lines.append('Message-Id: %s' % msgid)
-    ...     lines.append('Subject: %s' % subject)
-    ...     lines.append('')
+    ...     lines.append("Message-Id: %s" % msgid)
+    ...     lines.append("Subject: %s" % subject)
+    ...     lines.append("")
     ...     lines.append(body)
-    ...     raw_msg = '\n'.join(lines)
-    ...     msg = signed_message_from_bytes(raw_msg.encode('UTF-8'))
-    ...     if handler.process(msg, msg['To']):
+    ...     raw_msg = "\n".join(lines)
+    ...     msg = signed_message_from_bytes(raw_msg.encode("UTF-8"))
+    ...     if handler.process(msg, msg["To"]):
     ...         # Ensures that the DB user has the correct permission to \
     ...         # saves the changes.
     ...         flush_database_updates()
     ...         return msgid
     ...     else:
     ...         return None
+    ...
 
 It only processes emails which are sent to an address of the form
 'question<ID>@answers.launchpad.net', where <ID> is the question id. (The
@@ -68,7 +70,7 @@ All other email addresses are ignored:
     ...
     ... Hello there."""
     >>> msg = signed_message_from_bytes(raw_msg)
-    >>> handler.process(msg, msg['To'])
+    >>> handler.process(msg, msg["To"])
     False
 
 
@@ -76,7 +78,8 @@ The message will also be ignored if no question with the addressed ID
 can be found:
 
     >>> comment_msgid = send_question_email(
-    ...     1234, 'foo.bar@xxxxxxxxxxxxx', 'Hey', 'This is another comment.')
+    ...     1234, "foo.bar@xxxxxxxxxxxxx", "Hey", "This is another comment."
+    ... )
     >>> comment_msgid is None
     True
 
@@ -102,22 +105,26 @@ possibilities for the user.
     # question.
     >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> from lp.registry.interfaces.person import IPersonSet
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
     >>> personset = getUtility(IPersonSet)
-    >>> sample_person = personset.getByEmail('test@xxxxxxxxxxxxx')
-    >>> no_priv = personset.getByEmail('no-priv@xxxxxxxxxxxxx')
-    >>> foo_bar = personset.getByEmail('foo.bar@xxxxxxxxxxxxx')
+    >>> sample_person = personset.getByEmail("test@xxxxxxxxxxxxx")
+    >>> no_priv = personset.getByEmail("no-priv@xxxxxxxxxxxxx")
+    >>> foo_bar = personset.getByEmail("foo.bar@xxxxxxxxxxxxx")
 
     >>> import transaction
     >>> from lp.testing.dbuser import lp_dbuser
 
     >>> with lp_dbuser():
-    ...     ubuntu = getUtility(IDistributionSet)['ubuntu']
+    ...     ubuntu = getUtility(IDistributionSet)["ubuntu"]
     ...     question = ubuntu.newQuestion(
-    ...         no_priv, 'Unable to boot installer',
+    ...         no_priv,
+    ...         "Unable to boot installer",
     ...         "I've tried installing Ubuntu on a Mac. But the installer "
-    ...         "never boots.", datecreated=next(now))
+    ...         "never boots.",
+    ...         datecreated=next(now),
+    ...     )
     ...     question_id = question.id
+    ...
 
     # We need to refetch the question, since a new transaction was started.
     >>> from lp.answers.interfaces.questioncollection import IQuestionSet
@@ -125,10 +132,12 @@ possibilities for the user.
 
     # Define an helper to change the question status easily.
     >>> def setQuestionStatus(question, new_status):
-    ...     login('foo.bar@xxxxxxxxxxxxx')
-    ...     question.setStatus(foo_bar, new_status, 'Status Change',
-    ...                            datecreated=next(now))
-    ...     login('no-priv@xxxxxxxxxxxxx')
+    ...     login("foo.bar@xxxxxxxxxxxxx")
+    ...     question.setStatus(
+    ...         foo_bar, new_status, "Status Change", datecreated=next(now)
+    ...     )
+    ...     login("no-priv@xxxxxxxxxxxxx")
+    ...
 
 Message From the Question Owner
 -------------------------------
@@ -146,8 +155,11 @@ more information on the problem.
 For example, from the Open state:
 
     >>> msgid = send_question_email(
-    ...     question.id, 'no-priv@xxxxxxxxxxxxx', 'PowerMac 7200',
-    ...     "I forgot to specify that I'm installing on a PowerMac 7200.")
+    ...     question.id,
+    ...     "no-priv@xxxxxxxxxxxxx",
+    ...     "PowerMac 7200",
+    ...     "I forgot to specify that I'm installing on a PowerMac 7200.",
+    ... )
     >>> message = question.messages[-1]
     >>> message.rfc822msgid == msgid
     True
@@ -165,9 +177,12 @@ And from the Needs information state:
     >>> from lp.answers.enums import QuestionStatus
     >>> setQuestionStatus(question, QuestionStatus.NEEDSINFO)
 
-    >>> msgid =  send_question_email(
-    ...     question.id, 'no-priv@xxxxxxxxxxxxx', 'Re: What model?',
-    ...     'A PowerMac 7200.')
+    >>> msgid = send_question_email(
+    ...     question.id,
+    ...     "no-priv@xxxxxxxxxxxxx",
+    ...     "Re: What model?",
+    ...     "A PowerMac 7200.",
+    ... )
     >>> message = question.messages[-1]
     >>> message.rfc822msgid == msgid
     True
@@ -190,10 +205,13 @@ the email is reopening the question with more information.
     >>> setQuestionStatus(question, QuestionStatus.ANSWERED)
 
     >>> msgid = send_question_email(
-    ...     question.id, 'no-priv@xxxxxxxxxxxxx', 'Re: BootX',
+    ...     question.id,
+    ...     "no-priv@xxxxxxxxxxxxx",
+    ...     "Re: BootX",
     ...     "I installed BootX, but I must have made a mistake somewhere "
     ...     "because it still doesn't boot. I have a dialog which says "
-    ...     "cannot find any kernel images.")
+    ...     "cannot find any kernel images.",
+    ... )
     >>> message = question.messages[-1]
     >>> message.rfc822msgid == msgid
     True
@@ -211,9 +229,12 @@ From the Expired state:
 
     >>> setQuestionStatus(question, QuestionStatus.EXPIRED)
 
-    >>> msgid =  send_question_email(
-    ...     question.id, 'no-priv@xxxxxxxxxxxxx', 'Need Help',
-    ...     "I still cannot install on my PowerMac.")
+    >>> msgid = send_question_email(
+    ...     question.id,
+    ...     "no-priv@xxxxxxxxxxxxx",
+    ...     "Need Help",
+    ...     "I still cannot install on my PowerMac.",
+    ... )
     >>> message = question.messages[-1]
     >>> message.rfc822msgid == msgid
     True
@@ -233,9 +254,12 @@ message as a comment.
 
     >>> setQuestionStatus(question, QuestionStatus.SOLVED)
 
-    >>> msgid =  send_question_email(
-    ...     question.id, 'no-priv@xxxxxxxxxxxxx', "Thanks",
-    ...     "Thanks for helping me make BootX work.")
+    >>> msgid = send_question_email(
+    ...     question.id,
+    ...     "no-priv@xxxxxxxxxxxxx",
+    ...     "Thanks",
+    ...     "Thanks for helping me make BootX work.",
+    ... )
     >>> message = question.messages[-1]
     >>> message.rfc822msgid == msgid
     True
@@ -251,10 +275,13 @@ And from the Invalid:
 
     >>> setQuestionStatus(question, QuestionStatus.INVALID)
 
-    >>> msgid =  send_question_email(
-    ...     question.id, 'no-priv@xxxxxxxxxxxxx', 'Come on!',
+    >>> msgid = send_question_email(
+    ...     question.id,
+    ...     "no-priv@xxxxxxxxxxxxx",
+    ...     "Come on!",
     ...     "Trying to install on an old machine shouldn't be considered "
-    ...     "an invalid question!")
+    ...     "an invalid question!",
+    ... )
     >>> message = question.messages[-1]
     >>> message.rfc822msgid == msgid
     True
@@ -278,10 +305,13 @@ fine. So it is the safest thing to assume.
 
     >>> setQuestionStatus(question, QuestionStatus.OPEN)
 
-    >>> msgid =  send_question_email(
-    ...     question.id, 'test@xxxxxxxxxxxxx', 'BootX',
+    >>> msgid = send_question_email(
+    ...     question.id,
+    ...     "test@xxxxxxxxxxxxx",
+    ...     "BootX",
     ...     "You need to install and configure BootX to boot the installer "
-    ...     "CD.")
+    ...     "CD.",
+    ... )
     >>> message = question.messages[-1]
     >>> message.rfc822msgid == msgid
     True
@@ -294,9 +324,12 @@ Needs information example:
 
     >>> setQuestionStatus(question, QuestionStatus.NEEDSINFO)
 
-    >>> msgid =  send_question_email(
-    ...     question.id, 'test@xxxxxxxxxxxxx', 'What model?',
-    ...     "What Mac model are you trying to install on?")
+    >>> msgid = send_question_email(
+    ...     question.id,
+    ...     "test@xxxxxxxxxxxxx",
+    ...     "What model?",
+    ...     "What Mac model are you trying to install on?",
+    ... )
     >>> message = question.messages[-1]
     >>> message.rfc822msgid == msgid
     True
@@ -308,10 +341,13 @@ Answered example:
     >>> print(question.status.title)
     Answered
 
-    >>> msgid =  send_question_email(
-    ...     question.id, 'test@xxxxxxxxxxxxx', 'More info on BootX',
+    >>> msgid = send_question_email(
+    ...     question.id,
+    ...     "test@xxxxxxxxxxxxx",
+    ...     "More info on BootX",
     ...     "You can find instructions on BootX installation at that URL: "
-    ...     "https://help.ubuntu.com/community/Installation/OldWorldMacs";)
+    ...     "https://help.ubuntu.com/community/Installation/OldWorldMacs";,
+    ... )
     >>> message = question.messages[-1]
     >>> message.rfc822msgid == msgid
     True
@@ -328,10 +364,13 @@ interpretation is that it is a comment.
 
     >>> setQuestionStatus(question, QuestionStatus.SOLVED)
 
-    >>> msgid =  send_question_email(
-    ...     question.id, 'test@xxxxxxxxxxxxx', 'RAM',
+    >>> msgid = send_question_email(
+    ...     question.id,
+    ...     "test@xxxxxxxxxxxxx",
+    ...     "RAM",
     ...     "You will probably need to install some RAM to make this usable "
-    ...     "though.")
+    ...     "though.",
+    ... )
     >>> message = question.messages[-1]
     >>> message.rfc822msgid == msgid
     True
@@ -340,10 +379,13 @@ interpretation is that it is a comment.
 
     >>> setQuestionStatus(question, QuestionStatus.EXPIRED)
 
-    >>> msgid =  send_question_email(
-    ...     question.id, 'test@xxxxxxxxxxxxx', 'How weird',
+    >>> msgid = send_question_email(
+    ...     question.id,
+    ...     "test@xxxxxxxxxxxxx",
+    ...     "How weird",
     ...     "Is somebody really trying to install Ubuntu on such obsolete "
-    ...     "hardware?")
+    ...     "hardware?",
+    ... )
     >>> message = question.messages[-1]
     >>> message.rfc822msgid == msgid
     True
@@ -352,9 +394,12 @@ interpretation is that it is a comment.
 
     >>> setQuestionStatus(question, QuestionStatus.INVALID)
 
-    >>> msgid =  send_question_email(
-    ...     question.id, 'test@xxxxxxxxxxxxx', 'Error?',
-    ...     "I think the rejection was an error.")
+    >>> msgid = send_question_email(
+    ...     question.id,
+    ...     "test@xxxxxxxxxxxxx",
+    ...     "Error?",
+    ...     "I think the rejection was an error.",
+    ... )
     >>> message = question.messages[-1]
     >>> message.rfc822msgid == msgid
     True
@@ -371,20 +416,26 @@ Answers may also be linked to FAQ questions.
     >>> from zope.security.proxy import removeSecurityProxy
 
     >>> with lp_dbuser():
-    ...     login('foo.bar@xxxxxxxxxxxxx')
+    ...     login("foo.bar@xxxxxxxxxxxxx")
     ...     faq = question.target.newFAQ(
-    ...         no_priv, 'Why everyone think this is weird.',
-    ...         "That's an easy one. It's because it is!")
+    ...         no_priv,
+    ...         "Why everyone think this is weird.",
+    ...         "That's an easy one. It's because it is!",
+    ...     )
     ...     removeSecurityProxy(question).faq = faq
+    ...
 
-    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> login("no-priv@xxxxxxxxxxxxx")
 
     # Make sure that the database security and permissions are set up
     # correctly for answers that link to FAQs.  If they are not, then
     # this will raise an error; See bug #196661.
-    >>> msgid =  send_question_email(
-    ...     question.id, 'test@xxxxxxxxxxxxx', 'Fnord',
-    ...     "You will probably need to install some RAM to see the fnords.")
+    >>> msgid = send_question_email(
+    ...     question.id,
+    ...     "test@xxxxxxxxxxxxx",
+    ...     "Fnord",
+    ...     "You will probably need to install some RAM to see the fnords.",
+    ... )
     >>> message = question.messages[-1]
     >>> message.rfc822msgid == msgid
     True
@@ -410,16 +461,21 @@ config.answertracker.email_domain to the AnswerTrackerHandler.
 
     # Clear email queue of outgoing notifications.
     >>> stub.test_emails = []
-    >>> stub.test_emails.append((
-    ...     'test@xxxxxxxxxxxxx', ['question1@xxxxxxxxxxxxxxxxxxxxx'],
-    ...     raw_msg))
+    >>> stub.test_emails.append(
+    ...     (
+    ...         "test@xxxxxxxxxxxxx",
+    ...         ["question1@xxxxxxxxxxxxxxxxxxxxx"],
+    ...         raw_msg,
+    ...     )
+    ... )
 
     >>> from lp.services.mail.incoming import handleMail
     >>> handleMail()
 
     >>> question_one = getUtility(IQuestionSet).get(1)
-    >>> '<comment1@localhost>' in [
-    ...     comment.rfc822msgid for comment in question_one.messages]
+    >>> "<comment1@localhost>" in [
+    ...     comment.rfc822msgid for comment in question_one.messages
+    ... ]
     True
 
 For backward compatibility with notifications sent before the support
@@ -434,12 +490,17 @@ to the old ticket<ID>@support.launchpad.net address:
     ...
     ... This is another comment.
     ... """
-    >>> stub.test_emails.append((
-    ...     'test@xxxxxxxxxxxxx', ['ticket11@xxxxxxxxxxxxxxxxxxxxx'],
-    ...     raw_msg))
+    >>> stub.test_emails.append(
+    ...     (
+    ...         "test@xxxxxxxxxxxxx",
+    ...         ["ticket11@xxxxxxxxxxxxxxxxxxxxx"],
+    ...         raw_msg,
+    ...     )
+    ... )
     >>> handleMail()
 
     >>> question_11 = getUtility(IQuestionSet).get(11)
-    >>> '<comment2@localhost>' in [
-    ...     comment.rfc822msgid for comment in question_11.messages]
+    >>> "<comment2@localhost>" in [
+    ...     comment.rfc822msgid for comment in question_11.messages
+    ... ]
     True
diff --git a/lib/lp/app/browser/doc/base-layout.rst b/lib/lp/app/browser/doc/base-layout.rst
index f755353..c9d4758 100644
--- a/lib/lp/app/browser/doc/base-layout.rst
+++ b/lib/lp/app/browser/doc/base-layout.rst
@@ -14,19 +14,21 @@ YUI grid classes for positioning.
     >>> from lp.services.webapp.servers import LaunchpadTestRequest
     >>> from zope.browserpage import ViewPageTemplateFile
 
-    >>> user = factory.makePerson(name='waffles')
+    >>> user = factory.makePerson(name="waffles")
     >>> request = LaunchpadTestRequest(
-    ...     SERVER_URL='http://launchpad.test',
-    ...     PATH_INFO='/~waffles/+layout')
+    ...     SERVER_URL="http://launchpad.test";, PATH_INFO="/~waffles/+layout"
+    ... )
     >>> request.setPrincipal(user)
 
 The view can define the template's title in the page_title property.
 
     >>> class MainSideView(LaunchpadView):
     ...     """A simple view to test base-layout."""
-    ...     __launchpad_facetname__ = 'overview'
-    ...     template = ViewPageTemplateFile('../tests/testfiles/main-side.pt')
-    ...     page_title = 'Test base-layout: main_side'
+    ...
+    ...     __launchpad_facetname__ = "overview"
+    ...     template = ViewPageTemplateFile("../tests/testfiles/main-side.pt")
+    ...     page_title = "Test base-layout: main_side"
+    ...
 
 The main_side layout uses all the defined features of the base-layout
 template. The example template uses the epilogue, main, and side
@@ -56,9 +58,11 @@ content to take up all the horizontal space.
 
     >>> class MainOnlyView(LaunchpadView):
     ...     """A simple view to test base-layout."""
-    ...     __launchpad_facetname__ = 'overview'
-    ...     template = ViewPageTemplateFile('../tests/testfiles/main-only.pt')
-    ...     page_title = 'Test base-layout: main_only'
+    ...
+    ...     __launchpad_facetname__ = "overview"
+    ...     template = ViewPageTemplateFile("../tests/testfiles/main-only.pt")
+    ...     page_title = "Test base-layout: main_only"
+    ...
 
     >>> view = MainOnlyView(user, request)
     >>> html = view.render()
@@ -79,10 +83,13 @@ slots are rendered, as are the application tabs.
 
     >>> class SearchlessView(LaunchpadView):
     ...     """A simple view to test base-layout."""
-    ...     __launchpad_facetname__ = 'overview'
+    ...
+    ...     __launchpad_facetname__ = "overview"
     ...     template = ViewPageTemplateFile(
-    ...         '../tests/testfiles/searchless.pt')
-    ...     page_title = 'Test base-layout: searchless'
+    ...         "../tests/testfiles/searchless.pt"
+    ...     )
+    ...     page_title = "Test base-layout: searchless"
+    ...
 
     >>> view = SearchlessView(user, request)
     >>> html = view.render()
@@ -104,8 +111,7 @@ Page Diagnostics
 
 The page includes a comment after the body with diagnostic information.
 
-    >>> print(html[html.index('</body>') + 7:])
-    ...
+    >>> print(html[html.index("</body>") + 7 :])
     <!--
       Facet name: overview
       Page type: searchless
@@ -124,7 +130,7 @@ Page Headings
 The example layouts all used the heading slot to define a heading for their
 test. The template controlled the heading.
 
-    >>> content = find_tag_by_id(view.render(), 'maincontent')
+    >>> content = find_tag_by_id(view.render(), "maincontent")
     >>> print(content.h1)
     <h1>Heading</h1>
 
@@ -134,15 +140,19 @@ Page Footers
 
     >>> class BugsMainSideView(MainSideView):
     ...     """A simple view to test base-layout."""
-    ...     __launchpad_facetname__ = 'bugs'
+    ...
+    ...     __launchpad_facetname__ = "bugs"
+    ...
     >>> bugs_request = LaunchpadTestRequest(
-    ...     SERVER_URL='http://bugs.launchpad.test',
-    ...     PATH_INFO='/~waffles/+layout')
+    ...     SERVER_URL="http://bugs.launchpad.test";,
+    ...     PATH_INFO="/~waffles/+layout",
+    ... )
     >>> bugs_request.setPrincipal(user)
     >>> view = BugsMainSideView(user, bugs_request)
-    >>> footer = find_tag_by_id(html, 'footer')
-    >>> for tag in footer.find_all('a'):
-    ...     print(tag.string, tag['href'])
+    >>> footer = find_tag_by_id(html, "footer")
+    >>> for tag in footer.find_all("a"):
+    ...     print(tag.string, tag["href"])
+    ...
     None http://launchpad.test/
     Take the tour http://launchpad.test/+tour
     Read the guide https://help.launchpad.net/
@@ -167,13 +177,15 @@ attribute.
 
     >>> from lp.registry.interfaces.person import PersonVisibility
 
-    >>> login('admin@xxxxxxxxxxxxx')
+    >>> login("admin@xxxxxxxxxxxxx")
     >>> team = factory.makeTeam(
-    ...     owner=user, name='a-private-team',
-    ...     visibility=PersonVisibility.PRIVATE)
+    ...     owner=user,
+    ...     name="a-private-team",
+    ...     visibility=PersonVisibility.PRIVATE,
+    ... )
     >>> view = MainOnlyView(team, request)
-    >>> body = find_tag_by_id(view.render(), 'document')
-    >>> print(' '.join(body['class']))
+    >>> body = find_tag_by_id(view.render(), "document")
+    >>> print(" ".join(body["class"]))
     tab-overview
         main_only
         private
@@ -182,10 +194,10 @@ attribute.
 When the context is public, the 'public' class is in the class attribute.
 
     >>> login(ANONYMOUS)
-    >>> team = factory.makeTeam(owner=user, name='a-public-team')
+    >>> team = factory.makeTeam(owner=user, name="a-public-team")
     >>> view = MainOnlyView(team, request)
-    >>> body = find_tag_by_id(view.render(), 'document')
-    >>> print(' '.join(body['class']))
+    >>> body = find_tag_by_id(view.render(), "document")
+    >>> print(" ".join(body["class"]))
     tab-overview main_only public yui3-skin-sam
 
 
@@ -194,9 +206,9 @@ Notifications
 
 Notifications are displayed between the breadcrumbs and the page content.
 
-    >>> request.response.addInfoNotification('I cannot do that Dave.')
+    >>> request.response.addInfoNotification("I cannot do that Dave.")
     >>> view = MainOnlyView(user, request)
-    >>> body_tag = find_tag_by_id(view.render(), 'maincontent')
+    >>> body_tag = find_tag_by_id(view.render(), "maincontent")
     >>> print(str(body_tag))
     <div ... id="maincontent">
       ...
@@ -213,17 +225,21 @@ headers.
     >>> from zope.interface import Interface
     >>> class FormView(LaunchpadFormView):
     ...     """A simple view to test notifications."""
+    ...
     ...     class schema(Interface):
     ...         """A default schema."""
-    ...     @action('Test', name='test')
+    ...
+    ...     @action("Test", name="test")
     ...     def test_action(self, action, data):
     ...         pass
+    ...
 
-    >>> extra = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
+    >>> extra = {"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"}
     >>> request = LaunchpadTestRequest(
-    ...     method='POST', form={'field.actions.test': 'Test'}, **extra)
-    >>> request.response.addInfoNotification('I cannot do that Dave.')
+    ...     method="POST", form={"field.actions.test": "Test"}, **extra
+    ... )
+    >>> request.response.addInfoNotification("I cannot do that Dave.")
     >>> view = FormView(user, request)
     >>> view.initialize()
-    >>> print(request.response.getHeader('X-Lazr-Notifications'))
+    >>> print(request.response.getHeader("X-Lazr-Notifications"))
     [[20, "I cannot do that Dave."]]
diff --git a/lib/lp/app/browser/doc/launchpad-search-pages.rst b/lib/lp/app/browser/doc/launchpad-search-pages.rst
index 83413f3..2ce13d6 100644
--- a/lib/lp/app/browser/doc/launchpad-search-pages.rst
+++ b/lib/lp/app/browser/doc/launchpad-search-pages.rst
@@ -39,18 +39,23 @@ When text is not None, the title indicates what was searched.
     ...     for name in sorted(form):
     ...         value = form[name]
     ...         search_param_list.append(
-    ...             wsgi_native_string(name) + wsgi_native_string('=') +
-    ...             wsgi_native_string(value))
-    ...     query_string = wsgi_native_string('&').join(search_param_list)
+    ...             wsgi_native_string(name)
+    ...             + wsgi_native_string("=")
+    ...             + wsgi_native_string(value)
+    ...         )
+    ...     query_string = wsgi_native_string("&").join(search_param_list)
     ...     request = LaunchpadTestRequest(
-    ...         SERVER_URL='https://launchpad.test/+search',
-    ...         QUERY_STRING=query_string, form=form, PATH_INFO='/+search')
+    ...         SERVER_URL="https://launchpad.test/+search";,
+    ...         QUERY_STRING=query_string,
+    ...         form=form,
+    ...         PATH_INFO="/+search",
+    ...     )
     ...     search_view = getMultiAdapter((root, request), name="+search")
     ...     search_view.initialize()
     ...     return search_view
+    ...
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': 'albatross'})
+    >>> search_view = getSearchView(form={"field.text": "albatross"})
 
     >>> print(search_view.text)
     albatross
@@ -88,8 +93,7 @@ When a numeric token can be extracted from the submitted search text,
 the view tries to match a bug and question. Bugs and questions are
 matched by their id.
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': '5'})
+    >>> search_view = getSearchView(form={"field.text": "5"})
     >>> print(search_view._getNumericToken(search_view.text))
     5
     >>> search_view.has_matches
@@ -103,8 +107,7 @@ Bugs and questions are matched independent of each other. The number
 extracted may only match one kind of object. For example, there are
 more bugs than questions.
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': '15'})
+    >>> search_view = getSearchView(form={"field.text": "15"})
     >>> print(search_view._getNumericToken(search_view.text))
     15
     >>> search_view.has_matches
@@ -121,21 +124,20 @@ created because they are the owner.
     >>> from lp.services.webapp.interfaces import ILaunchBag
     >>> from lp.app.enums import InformationType
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> sample_person = getUtility(ILaunchBag).user
     >>> private_bug = factory.makeBug(
-    ...     owner=sample_person, information_type=InformationType.USERDATA)
+    ...     owner=sample_person, information_type=InformationType.USERDATA
+    ... )
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': str(private_bug.id)})
+    >>> search_view = getSearchView(form={"field.text": str(private_bug.id)})
     >>> search_view.bug.private
     True
 
 But anonymous and unprivileged users cannot see the private bug.
 
     >>> login(ANONYMOUS)
-    >>> search_view = getSearchView(
-    ...     form={'field.text': str(private_bug.id)})
+    >>> search_view = getSearchView(form={"field.text": str(private_bug.id)})
     >>> print(search_view.bug)
     None
 
@@ -146,7 +148,8 @@ is used, and it matches a bug, not a question. The second and third
 numbers do match questions, but they are not used.
 
     >>> search_view = getSearchView(
-    ...     form={'field.text': 'Question #15, #7, and 5.'})
+    ...     form={"field.text": "Question #15, #7, and 5."}
+    ... )
     >>> print(search_view._getNumericToken(search_view.text))
     15
     >>> search_view.has_matches
@@ -158,8 +161,7 @@ numbers do match questions, but they are not used.
 
 It is not an error to search for a non-existent bug or question.
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': '55555'})
+    >>> search_view = getSearchView(form={"field.text": "55555"})
     >>> print(search_view._getNumericToken(search_view.text))
     55555
     >>> search_view.has_matches
@@ -171,8 +173,7 @@ It is not an error to search for a non-existent bug or question.
 
 There is no error if a number cannot be extracted from the search text.
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': 'fifteen'})
+    >>> search_view = getSearchView(form={"field.text": "fifteen"})
     >>> print(search_view._getNumericToken(search_view.text))
     None
     >>> search_view.has_matches
@@ -185,9 +186,7 @@ There is no error if a number cannot be extracted from the search text.
 Bugs and questions are only returned for the first page of search,
 when the start param is 0.
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': '5',
-    ...           'start': '20'})
+    >>> search_view = getSearchView(form={"field.text": "5", "start": "20"})
     >>> search_view.has_matches
     False
     >>> print(search_view.bug)
@@ -204,8 +203,7 @@ When a Launchpad name can be made from the search text, the view tries
 to match the name to a pillar or person. a pillar is a distribution,
 product, or project group. A person is a person or a team.
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': 'launchpad'})
+    >>> search_view = getSearchView(form={"field.text": "launchpad"})
     >>> print(search_view._getNameToken(search_view.text))
     launchpad
     >>> search_view.has_matches
@@ -219,8 +217,7 @@ A launchpad name is constructed from the search text. The letters are
 converted to lowercase. groups of spaces and punctuation are replaced
 with a hyphen.
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': 'Gnome Terminal'})
+    >>> search_view = getSearchView(form={"field.text": "Gnome Terminal"})
     >>> print(search_view._getNameToken(search_view.text))
     gnome-terminal
     >>> search_view.has_matches
@@ -234,12 +231,11 @@ Since our pillars can have aliases, it's also possible to look up a pillar
 by any of its aliases.
 
     >>> from lp.registry.interfaces.product import IProductSet
-    >>> firefox = getUtility(IProductSet)['firefox']
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> firefox.setAliases(['iceweasel'])
+    >>> firefox = getUtility(IProductSet)["firefox"]
+    >>> login("foo.bar@xxxxxxxxxxxxx")
+    >>> firefox.setAliases(["iceweasel"])
     >>> login(ANONYMOUS)
-    >>> search_view = getSearchView(
-    ...     form={'field.text': 'iceweasel'})
+    >>> search_view = getSearchView(form={"field.text": "iceweasel"})
     >>> print(search_view._getNameToken(search_view.text))
     iceweasel
     >>> search_view.has_matches
@@ -252,7 +248,8 @@ the name of a pillar will none-the-less be tried. See the `Page searches`
 section for how this kind of search can return matches.
 
     >>> search_view = getSearchView(
-    ...     form={'field.text': "YAHOO! webservice's Python API."})
+    ...     form={"field.text": "YAHOO! webservice's Python API."}
+    ... )
     >>> print(search_view._getNameToken(search_view.text))
     yahoo-webservices-python-api.
     >>> search_view.has_matches
@@ -264,8 +261,7 @@ section for how this kind of search can return matches.
 
 Leading and trailing punctuation and whitespace are stripped.
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': "~name12"})
+    >>> search_view = getSearchView(form={"field.text": "~name12"})
     >>> print(search_view._getNameToken(search_view.text))
     name12
     >>> search_view.has_matches
@@ -279,8 +275,8 @@ Pillars, persons and teams are only returned for the first page of
 search, when the start param is 0.
 
     >>> search_view = getSearchView(
-    ...     form={'field.text': 'launchpad',
-    ...           'start': '20'})
+    ...     form={"field.text": "launchpad", "start": "20"}
+    ... )
     >>> search_view.has_matches
     True
     >>> print(search_view.bug)
@@ -296,27 +292,25 @@ pillar, nor will nsv match Nicolas Velin's unclaimed account.
 
     >>> from lp.registry.interfaces.person import IPersonSet
 
-    >>> python_gnome2 = getUtility(IProductSet).getByName('python-gnome2-dev')
+    >>> python_gnome2 = getUtility(IProductSet).getByName("python-gnome2-dev")
     >>> python_gnome2.active
     False
 
     >>> search_view = getSearchView(
-    ...     form={'field.text': 'python-gnome2-dev',
-    ...           'start': '0'})
+    ...     form={"field.text": "python-gnome2-dev", "start": "0"}
+    ... )
     >>> print(search_view._getNameToken(search_view.text))
     python-gnome2-dev
     >>> print(search_view.pillar)
     None
 
-    >>> nsv = getUtility(IPersonSet).getByName('nsv')
+    >>> nsv = getUtility(IPersonSet).getByName("nsv")
     >>> print(nsv.displayname)
     Nicolas Velin
     >>> nsv.is_valid_person_or_team
     False
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': 'nsv',
-    ...           'start': '0'})
+    >>> search_view = getSearchView(form={"field.text": "nsv", "start": "0"})
     >>> print(search_view._getNameToken(search_view.text))
     nsv
     >>> print(search_view.person_or_team)
@@ -328,20 +322,22 @@ because they are the owner.
 
     >>> from lp.registry.interfaces.product import License
 
-    >>> login('test@xxxxxxxxxxxxx')
+    >>> login("test@xxxxxxxxxxxxx")
     >>> private_product = factory.makeProduct(
-    ...     owner=sample_person, information_type=InformationType.PROPRIETARY,
-    ...     licenses=[License.OTHER_PROPRIETARY])
+    ...     owner=sample_person,
+    ...     information_type=InformationType.PROPRIETARY,
+    ...     licenses=[License.OTHER_PROPRIETARY],
+    ... )
     >>> private_product_name = private_product.name
 
-    >>> search_view = getSearchView(form={'field.text': private_product_name})
+    >>> search_view = getSearchView(form={"field.text": private_product_name})
     >>> search_view.pillar.private
     True
 
 But anonymous and unprivileged users cannot see the private project.
 
     >>> login(ANONYMOUS)
-    >>> search_view = getSearchView(form={'field.text': private_product_name})
+    >>> search_view = getSearchView(form={"field.text": private_product_name})
     >>> print(search_view.pillar)
     None
 
@@ -362,20 +358,20 @@ is True when 2 or more words match.
     ['cd', 'cds', 'disc', 'dvd', 'dvds', 'edubuntu', 'free', 'get', 'kubuntu',
      'mail', 'send', 'ship', 'shipit', 'ubuntu']
     >>> search_view = getSearchView(
-    ...     form={'field.text': 'ubuntu CDs',
-    ...           'start': '0'})
+    ...     form={"field.text": "ubuntu CDs", "start": "0"}
+    ... )
     >>> search_view.has_shipit
     True
 
     >>> search_view = getSearchView(
-    ...     form={'field.text': 'shipit',
-    ...           'start': '0'})
+    ...     form={"field.text": "shipit", "start": "0"}
+    ... )
     >>> search_view.has_shipit
     False
 
     >>> search_view = getSearchView(
-    ...     form={'field.text': 'get Kubuntu cds',
-    ...           'start': '0'})
+    ...     form={"field.text": "get Kubuntu cds", "start": "0"}
+    ... )
     >>> search_view.has_shipit
     True
 
@@ -388,14 +384,14 @@ set has_shipit to False.
      'read', 'rip', 'write']
 
     >>> search_view = getSearchView(
-    ...     form={'field.text': 'ubuntu CD write',
-    ...           'start': '0'})
+    ...     form={"field.text": "ubuntu CD write", "start": "0"}
+    ... )
     >>> search_view.has_shipit
     False
 
     >>> search_view = getSearchView(
-    ...     form={'field.text': 'shipit error',
-    ...           'start': '0'})
+    ...     form={"field.text": "shipit error", "start": "0"}
+    ... )
     >>> search_view.has_shipit
     False
 
@@ -412,8 +408,7 @@ Page searches
 The view uses the Search Service to locate pages that match the
 search terms.
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': " bug"})
+    >>> search_view = getSearchView(form={"field.text": " bug"})
     >>> print(search_view.text)
     bug
     >>> search_view.has_matches
@@ -436,18 +431,19 @@ is a heading when there are only Search Service page matches...
     False
     >>> for heading in search_view.batch_heading:
     ...     print(heading)
+    ...
     page matching "bug"
     pages matching "bug"
 
 ...and a heading for when there are exact matches and Search Service page
 matches.
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': " launchpad"})
+    >>> search_view = getSearchView(form={"field.text": " launchpad"})
     >>> search_view.has_exact_matches
     True
     >>> for heading in search_view.batch_heading:
     ...     print(heading)
+    ...
     other page matching "launchpad"
     other pages matching "launchpad"
 
@@ -483,9 +479,7 @@ batch.
 The second batch has only five matches in it, even though the batch size
 is 20. That is because there were only 25 matching pages.
 
-    >>> search_view = getSearchView(
-    ...     form={'field.text': "bug",
-    ...           'start': '20'})
+    >>> search_view = getSearchView(form={"field.text": "bug", "start": "20"})
     >>> search_view.start
     20
     >>> print(search_view.text)
@@ -500,6 +494,7 @@ is 20. That is because there were only 25 matching pages.
     5
     >>> for page in pages:
     ...     print("'%s'" % page.title)
+    ...
     '...Bug... #2 in Ubuntu Hoary: “Blackhole Trash folder”'
     '...Bug... #2 in mozilla-firefox (Debian): ...Blackhole Trash folder...'
     '...Bug... #3 in mozilla-firefox (Debian): “Bug Title Test”'
@@ -532,7 +527,7 @@ No page matches
 When an empty PageMatches object is returned by the Search Service to
 the view, there are no matches to show.
 
-    >>> search_view = getSearchView(form={'field.text': 'no-meaningful'})
+    >>> search_view = getSearchView(form={"field.text": "no-meaningful"})
     >>> search_view.has_matches
     False
 
@@ -545,11 +540,13 @@ error. Also disable warnings, since we are tossing around malformed Unicode.
 
     >>> import warnings
     >>> with warnings.catch_warnings():
-    ...     warnings.simplefilter('ignore')
+    ...     warnings.simplefilter("ignore")
     ...     search_view = getSearchView(
-    ...         form={'field.text': b'\xfe\xfckr\xfc'})
+    ...         form={"field.text": b"\xfe\xfckr\xfc"}
+    ...     )
+    ...
     >>> html = search_view()
-    >>> 'Can not convert your search term' in html
+    >>> "Can not convert your search term" in html
     True
 
 
@@ -561,7 +558,7 @@ the site search engine. The LaunchpadSearchView will display the other
 searches and show a message explaining that the user can search again to
 find matching pages.
 
-    >>> search_view = getSearchView(form={'field.text': 'gnomebaker'})
+    >>> search_view = getSearchView(form={"field.text": "gnomebaker"})
     >>> search_view.has_matches
     True
     >>> print(search_view.pillar.displayname)
@@ -591,7 +588,8 @@ default value of the text field; 'bug' was submitted above, but is not
 present in the rendered form.
 
     >>> search_form_view = getMultiAdapter(
-    ...     (search_view, request), name='+search-form')
+    ...     (search_view, request), name="+search-form"
+    ... )
     >>> search_form_view.initialize()
     >>> search_form_view.id_suffix
     '-secondary'
@@ -613,7 +611,8 @@ view does not append a suffix to the form and input ids. The search
 field's value is 'bug', as was submitted above.
 
     >>> search_form_view = getMultiAdapter(
-    ...     (search_view, request), name='+primary-search-form')
+    ...     (search_view, request), name="+primary-search-form"
+    ... )
     >>> search_form_view.initialize()
     >>> search_form_view.id_suffix
     ''
@@ -651,9 +650,10 @@ the first 20 items are None. Only the last 5 items are PageMatches.
 
     >>> site_search = active_search_service()
     >>> naked_site_search = removeSecurityProxy(site_search)
-    >>> page_matches = naked_site_search.search(terms='bug', start=20)
+    >>> page_matches = naked_site_search.search(terms="bug", start=20)
     >>> results = WindowedList(
-    ...     page_matches, page_matches.start, page_matches.total)
+    ...     page_matches, page_matches.start, page_matches.total
+    ... )
     >>> len(results)
     25
     >>> print(results[0])
@@ -674,9 +674,12 @@ or on the navigator object.
     'batch'
 
     >>> search_view = getSearchView(
-    ...     form={'field.text': "bug",
-    ...           'start': '0',
-    ...           'batch': '100',})
+    ...     form={
+    ...         "field.text": "bug",
+    ...         "start": "0",
+    ...         "batch": "100",
+    ...     }
+    ... )
 
     >>> navigator = search_view.pages
     >>> navigator.currentBatch().size
@@ -694,7 +697,8 @@ navigator conforms to Google's maximum size of 20.
     >>> page_matches.start = 0
     >>> page_matches.total = 100
     >>> navigator = SiteSearchBatchNavigator(
-    ...     page_matches, search_view.request, page_matches.start, size=100)
+    ...     page_matches, search_view.request, page_matches.start, size=100
+    ... )
     >>> navigator.currentBatch().size
     20
     >>> len(navigator.currentBatch())
@@ -712,7 +716,8 @@ batch is 20, which is the start of the next batch from Google.
     >>> matches = list(range(0, 3))
     >>> page_matches._matches = matches
     >>> navigator = SiteSearchBatchNavigator(
-    ...     page_matches, search_view.request, page_matches.start, size=100)
+    ...     page_matches, search_view.request, page_matches.start, size=100
+    ... )
     >>> batch = navigator.currentBatch()
     >>> batch.size
     20
@@ -722,6 +727,7 @@ batch is 20, which is the start of the next batch from Google.
     3
     >>> for item in batch:
     ...     print(item)
+    ...
     0
     1
     2
diff --git a/lib/lp/app/browser/doc/launchpadform-view.rst b/lib/lp/app/browser/doc/launchpadform-view.rst
index d71fad1..f085072 100644
--- a/lib/lp/app/browser/doc/launchpadform-view.rst
+++ b/lib/lp/app/browser/doc/launchpadform-view.rst
@@ -17,25 +17,30 @@ can be used for subordinate field indentation, for example.
     >>> from lp.services.webapp.servers import LaunchpadTestRequest
 
     >>> class ITestSchema(Interface):
-    ...     displayname = TextLine(title=u"Title")
-    ...     nickname = TextLine(title=u"Nickname")
+    ...     displayname = TextLine(title="Title")
+    ...     nickname = TextLine(title="Nickname")
+    ...
 
     >>> class TestView(LaunchpadFormView):
-    ...     page_title = 'Test'
+    ...     page_title = "Test"
     ...     template = ViewPageTemplateFile(
-    ...         config.root + '/lib/lp/app/templates/generic-edit.pt')
+    ...         config.root + "/lib/lp/app/templates/generic-edit.pt"
+    ...     )
     ...     schema = ITestSchema
     ...     custom_widget_nickname = CustomWidgetFactory(
-    ...         TextWidget, widget_class='field subordinate')
+    ...         TextWidget, widget_class="field subordinate"
+    ...     )
+    ...
 
-    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> login("foo.bar@xxxxxxxxxxxxx")
     >>> person = factory.makePerson()
     >>> request = LaunchpadTestRequest()
     >>> request.setPrincipal(person)
     >>> view = TestView(person, request)
     >>> view.initialize()
-    >>> for tag in find_tags_by_class(view.render(), 'subordinate'):
+    >>> for tag in find_tags_by_class(view.render(), "subordinate"):
     ...     print(tag)
+    ...
     <div class="field subordinate">
     <label for="field.nickname">Nickname:</label>
     <div>
diff --git a/lib/lp/app/browser/doc/menu.rst b/lib/lp/app/browser/doc/menu.rst
index c5f13bd..d50a767 100644
--- a/lib/lp/app/browser/doc/menu.rst
+++ b/lib/lp/app/browser/doc/menu.rst
@@ -9,39 +9,47 @@ are usually bound to a view.
     >>> from zope.interface import implementer, Interface
     >>> from lp.services.webapp.interfaces import INavigationMenu
     >>> from lp.services.webapp.menu import (
-    ...     enabled_with_permission, Link, NavigationMenu)
+    ...     enabled_with_permission,
+    ...     Link,
+    ...     NavigationMenu,
+    ... )
     >>> from lp.services.webapp.publisher import LaunchpadView
     >>> from lp.services.webapp.servers import LaunchpadTestRequest
 
     >>> class IEditMenuMarker(Interface):
     ...     """A marker for a menu and a view."""
+    ...
 
     >>> class EditMenu(NavigationMenu):
     ...     """A simple menu."""
+    ...
     ...     usedfor = IEditMenuMarker
-    ...     facet = 'overview'
-    ...     title = 'Related pages'
-    ...     links = ('edit_thing', 'edit_people', 'admin')
+    ...     facet = "overview"
+    ...     title = "Related pages"
+    ...     links = ("edit_thing", "edit_people", "admin")
     ...
     ...     def edit_thing(self):
-    ...         return Link('+edit', 'Edit thing', icon='edit')
+    ...         return Link("+edit", "Edit thing", icon="edit")
     ...
     ...     def edit_people(self):
-    ...         return Link('+edit-people', 'Edit people related to thing')
+    ...         return Link("+edit-people", "Edit people related to thing")
     ...
-    ...     @enabled_with_permission('launchpad.Admin')
+    ...     @enabled_with_permission("launchpad.Admin")
     ...     def admin(self):
-    ...         return Link('+admin', 'Administer this user')
+    ...         return Link("+admin", "Administer this user")
+    ...
 
     >>> @implementer(IEditMenuMarker)
     ... class EditView(LaunchpadView):
     ...     """A simple view."""
+    ...
     ...     # A hack that reveals secret of how facets work.
-    ...     __launchpad_facetname__ = 'overview'
+    ...     __launchpad_facetname__ = "overview"
 
     # Menus are normally registered using the menu ZCML directive.
     >>> provideAdapter(
-    ...     EditMenu, [IEditMenuMarker], INavigationMenu, name="overview")
+    ...     EditMenu, [IEditMenuMarker], INavigationMenu, name="overview"
+    ... )
 
 
 Related pages
@@ -50,10 +58,11 @@ Related pages
 The related pages portlet is rendered using a TALES call passing the view
 to the named adapter: <tal:menu replace="structure view/@@+related-pages" />
 
-    >>> user = factory.makePerson(name='beaker')
+    >>> user = factory.makePerson(name="beaker")
     >>> view = EditView(user, LaunchpadTestRequest())
     >>> menu_view = create_initialized_view(
-    ...     view, '+related-pages', principal=user)
+    ...     view, "+related-pages", principal=user
+    ... )
     >>> print(menu_view.template.filename)
     /.../navigationmenu-related-pages.pt
 
@@ -64,6 +73,7 @@ and disabled links are included.
     Related pages
     >>> for link in menu_view.links:
     ...     print(link.enabled, link.url)
+    ...
     True   http://launchpad.test/~beaker/+edit
     True   http://launchpad.test/~beaker/+edit-people
     False  http://launchpad.test/~beaker/+admin
@@ -99,15 +109,18 @@ unset, and then only the link icon and text will be displayed, but it will not
 be clickable.
 
     >>> request = LaunchpadTestRequest(
-    ...     SERVER_URL='http://launchpad.test/~beaker/+edit')
+    ...     SERVER_URL="http://launchpad.test/~beaker/+edit";
+    ... )
     >>> print(request.getURL())
     http://launchpad.test/~beaker/+edit
 
     >>> view = EditView(user, request)
     >>> menu_view = create_initialized_view(
-    ...     view, '+related-pages', principal=user)
+    ...     view, "+related-pages", principal=user
+    ... )
     >>> for link in menu_view.links:
     ...     print(link.enabled, link.linked, link.url)
+    ...
     True  False  http://launchpad.test/~beaker/+edit
     True  True   http://launchpad.test/~beaker/+edit-people
     False True   http://launchpad.test/~beaker/+admin
@@ -131,9 +144,11 @@ The action menu uses the view's enabled_links property to get the list of
 links.
 
     >>> menu_view = create_initialized_view(
-    ...     view, '+global-actions', principal=user)
+    ...     view, "+global-actions", principal=user
+    ... )
     >>> for link in menu_view.enabled_links:
     ...     print(link.enabled, link.linked, link.url)
+    ...
     True  False  http://launchpad.test/~beaker/+edit
     True  True   http://launchpad.test/~beaker/+edit-people
 
@@ -158,10 +173,11 @@ The generated markup is for a portlet with the global-actions id.
 If there are no enabled links, no markup is rendered. For example, a menu
 may contain links that require special privileges to access.
 
-    >>> EditMenu.links = ('admin',)
+    >>> EditMenu.links = ("admin",)
 
     >>> menu_view = create_initialized_view(
-    ...     view, '+global-actions', principal=user)
+    ...     view, "+global-actions", principal=user
+    ... )
     >>> menu_view.enabled_links
     []
 
diff --git a/lib/lp/app/browser/doc/root-views.rst b/lib/lp/app/browser/doc/root-views.rst
index ad01689..8782f3f 100644
--- a/lib/lp/app/browser/doc/root-views.rst
+++ b/lib/lp/app/browser/doc/root-views.rst
@@ -9,6 +9,7 @@ special data needed for the layout.
     >>> from lp.app.browser.root import LaunchpadRootIndexView
     >>> def day():
     ...     return 4
+    ...
     >>> LaunchpadRootIndexView._get_day_of_year = staticmethod(day)
 
 The view has a provides a list of featured projects and a top project.
@@ -16,9 +17,10 @@ The view has a provides a list of featured projects and a top project.
     >>> from lp.services.webapp.interfaces import ILaunchpadRoot
 
     >>> root = getUtility(ILaunchpadRoot)
-    >>> view = create_initialized_view(root, name='index.html')
+    >>> view = create_initialized_view(root, name="index.html")
     >>> for project in view.featured_projects:
     ...     print(project.name)
+    ...
     applets bazaar firefox gentoo gnome-terminal mozilla thunderbird ubuntu
 
     >>> print(view.featured_projects_top.name)
@@ -35,6 +37,7 @@ project from the list of featured_projects.
 
     >>> for project in view.featured_projects:
     ...     print(project.name)
+    ...
     applets bazaar firefox gentoo mozilla thunderbird ubuntu
 
 If there are no featured projects, the top featured project is None.
diff --git a/lib/lp/app/browser/doc/watermark.rst b/lib/lp/app/browser/doc/watermark.rst
index 7c792bb..7746f59 100644
--- a/lib/lp/app/browser/doc/watermark.rst
+++ b/lib/lp/app/browser/doc/watermark.rst
@@ -16,19 +16,22 @@ object that implements IRootContext.
     >>> from lp.app.browser.launchpad import Hierarchy
     >>> from lp.services.webapp.servers import LaunchpadTestRequest
     >>> class TrivialView:
-    ...     __name__ = '+trivial'
+    ...     __name__ = "+trivial"
+    ...
     ...     def __init__(self, context):
     ...         self.context = context
+    ...
     >>> def get_hierarchy(obj, viewcls=TrivialView):
     ...     req = LaunchpadTestRequest()
     ...     view = viewcls(obj)
     ...     req.traversed_objects.append(view.context)
     ...     req.traversed_objects.append(view)
     ...     return Hierarchy(view, req)
+    ...
 
 Products directly implement IRootContext.
 
-    >>> widget = factory.makeProduct(displayname='Widget')
+    >>> widget = factory.makeProduct(displayname="Widget")
     >>> print(get_hierarchy(widget).heading())
     <h...><a...>Widget</a></h...>
 
@@ -40,13 +43,13 @@ A series of the product still show the product watermark.
 
 ProjectGroups also directly implement IRootContext ...
 
-    >>> kde = factory.makeProject(displayname='KDE')
+    >>> kde = factory.makeProject(displayname="KDE")
     >>> print(get_hierarchy(kde).heading())
     <h...><a...>KDE</a></h...>
 
 ... as do distributions ...
 
-    >>> mint = factory.makeDistribution(displayname='Mint Linux')
+    >>> mint = factory.makeDistribution(displayname="Mint Linux")
     >>> print(get_hierarchy(mint).heading())
     <h...><a...>Mint Linux</a></h...>
 
@@ -73,7 +76,8 @@ Launchpad.net).
 Any HTML in the context title will be escaped to avoid XSS vulnerabilities.
 
     >>> person = factory.makePerson(
-    ...     displayname="Fubar<br/><script>alert('XSS')</script>")
+    ...     displayname="Fubar<br/><script>alert('XSS')</script>"
+    ... )
     >>> print(get_hierarchy(person).heading())  # noqa
     <h...><a...>Fubar&lt;br/&gt;&lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;</a></h...>
 
diff --git a/lib/lp/app/browser/tests/test_launchpadform_doc.py b/lib/lp/app/browser/tests/test_launchpadform_doc.py
index a628ee2..15293e2 100644
--- a/lib/lp/app/browser/tests/test_launchpadform_doc.py
+++ b/lib/lp/app/browser/tests/test_launchpadform_doc.py
@@ -109,17 +109,20 @@ def doctest_custom_widget_with_setUpFields_override():
         ...     # initialization.
         ...     def __init__(self, field, request):
         ...         self.field, self.request = field, request
+        ...
         ...     def setPrefix(self, prefix):
-        ...         self.name = '.'.join((prefix, self.field.__name__))
+        ...         self.name = ".".join((prefix, self.field.__name__))
+        ...
         ...     def hasInput(self):
         ...         return False
+        ...
         ...     def setRenderedValue(self, value):
         ...         self.value = value
-        ...
         >>> class CustomView(LaunchpadFormView):
         ...     custom_widget_my_bool = CustomStubWidget
+        ...
         ...     def setUpFields(self):
-        ...         self.form_fields = form.Fields(Bool(__name__='my_bool'))
+        ...         self.form_fields = form.Fields(Bool(__name__="my_bool"))
         ...
 
     The custom setUpFields adds a field dynamically. Then setUpWidgets will
@@ -130,7 +133,7 @@ def doctest_custom_widget_with_setUpFields_override():
         >>> view = CustomView(None, TestRequest())
         >>> view.setUpFields()
         >>> view.setUpWidgets()
-        >>> isinstance(view.widgets['my_bool'], CustomStubWidget)
+        >>> isinstance(view.widgets["my_bool"], CustomStubWidget)
         True
     """
 
diff --git a/lib/lp/app/browser/tests/test_stringformatter.py b/lib/lp/app/browser/tests/test_stringformatter.py
index 78622ea..c99cdbe 100644
--- a/lib/lp/app/browser/tests/test_stringformatter.py
+++ b/lib/lp/app/browser/tests/test_stringformatter.py
@@ -31,8 +31,9 @@ def test_split_paragraphs():
     Paragraphs are yielded as a list of lines in the paragraph.
 
       >>> from lp.app.browser.stringformatter import split_paragraphs
-      >>> for paragraph in split_paragraphs('\na\nb\n\nc\nd\n\n\n'):
+      >>> for paragraph in split_paragraphs("\na\nb\n\nc\nd\n\n\n"):
       ...     print(paragraph)
+      ...
       ['a', 'b']
       ['c', 'd']
     """
@@ -49,13 +50,18 @@ def test_re_substitute():
       >>> from lp.app.browser.stringformatter import re_substitute
 
       >>> def match_func(match):
-      ...     return '[%s]' % match.group()
+      ...     return "[%s]" % match.group()
+      ...
       >>> def nomatch_func(text):
-      ...     return '{%s}' % text
-
-      >>> pat = re.compile('a{2,6}')
-      >>> print(re_substitute(pat, match_func, nomatch_func,
-      ...                     'bbaaaabbbbaaaaaaa aaaaaaaab'))
+      ...     return "{%s}" % text
+      ...
+
+      >>> pat = re.compile("a{2,6}")
+      >>> print(
+      ...     re_substitute(
+      ...         pat, match_func, nomatch_func, "bbaaaabbbbaaaaaaa aaaaaaaab"
+      ...     )
+      ... )
       {bb}[aaaa]{bbbb}[aaaaaa]{a }[aaaaaa][aa]{b}
     """
 
@@ -69,18 +75,18 @@ def test_add_word_breaks():
 
       >>> from lp.app.browser.stringformatter import add_word_breaks
 
-      >>> print(add_word_breaks('abcdefghijklmnop'))
+      >>> print(add_word_breaks("abcdefghijklmnop"))
       abcdefghijklmno<wbr />p
 
-      >>> print(add_word_breaks('abcdef/ghijklmnop'))
+      >>> print(add_word_breaks("abcdef/ghijklmnop"))
       abcdef/<wbr />ghijklmnop
 
-      >>> print(add_word_breaks('ab/cdefghijklmnop'))
+      >>> print(add_word_breaks("ab/cdefghijklmnop"))
       ab/cdefghijklmn<wbr />op
 
     The string can contain HTML entities, which do not get split:
 
-      >>> print(add_word_breaks('abcdef&anentity;hijklmnop'))
+      >>> print(add_word_breaks("abcdef&anentity;hijklmnop"))
       abcdef&anentity;<wbr />hijklmnop
     """
 
@@ -93,22 +99,24 @@ def test_break_long_words():
 
       >>> from lp.app.browser.stringformatter import break_long_words
 
-      >>> print(break_long_words('1234567890123456'))
+      >>> print(break_long_words("1234567890123456"))
       1234567890123456
 
-      >>> print(break_long_words('12345678901234567890'))
+      >>> print(break_long_words("12345678901234567890"))
       123456789012345<wbr />67890
 
       >>> print(break_long_words('<tag a12345678901234567890="foo"></tag>'))
       <tag a12345678901234567890="foo"></tag>
 
-      >>> print(break_long_words('12345678901234567890 1234567890.1234567890'))
+      >>> print(
+      ...     break_long_words("12345678901234567890 1234567890.1234567890")
+      ... )
       123456789012345<wbr />67890 1234567890.<wbr />1234567890
 
-      >>> print(break_long_words('1234567890&abcdefghi;123'))
+      >>> print(break_long_words("1234567890&abcdefghi;123"))
       1234567890&abcdefghi;123
 
-      >>> print(break_long_words('<tag>1234567890123456</tag>'))
+      >>> print(break_long_words("<tag>1234567890123456</tag>"))
       <tag>1234567890123456</tag>
     """
 
diff --git a/lib/lp/app/doc/badges.rst b/lib/lp/app/doc/badges.rst
index d2759b5..f48f84a 100644
--- a/lib/lp/app/doc/badges.rst
+++ b/lib/lp/app/doc/badges.rst
@@ -27,6 +27,7 @@ Iterating over this collection gives:
 
     >>> for name in sorted(STANDARD_BADGES):
     ...     print(name)
+    ...
     blueprint
     branch
     bug
@@ -49,8 +50,12 @@ Badge is added to the rendered heading image.
 
     >>> from lp.app.browser.badge import Badge
     >>> bug = Badge(
-    ...     icon_image='/@@/bug', heading_image='/@@/bug-large',
-    ...     alt='bug', title='Linked to a bug', id='bugbadge')
+    ...     icon_image="/@@/bug",
+    ...     heading_image="/@@/bug-large",
+    ...     alt="bug",
+    ...     title="Linked to a bug",
+    ...     id="bugbadge",
+    ... )
 
 Both `alt` and `title` default to the empty string.
 
@@ -66,10 +71,10 @@ Calling the render methods will produce the default image HTML.
 If the icon_image or heading_image are not specified, then the rendering
 the particular size results in the empty string.
 
-    >>> no_large = Badge(icon_image='/@@/bug')
+    >>> no_large = Badge(icon_image="/@@/bug")
     >>> no_large.renderHeadingImage()
     ''
-    >>> no_small = Badge(heading_image='/@@/bug')
+    >>> no_small = Badge(heading_image="/@@/bug")
     >>> no_small.renderIconImage()
     ''
 
@@ -90,6 +95,7 @@ for Interface, which just provides the privacy badge.
     >>> from lp.testing import verifyObject
     >>> class PrivateClass:
     ...     private = True
+    ...
     >>> private_object = PrivateClass()
     >>> has_badge_base = HasBadgeBase(private_object)
     >>> verifyObject(IHasBadges, has_badge_base)
@@ -112,23 +118,31 @@ badger class needs to implement the method `getBadge`.
 
     >>> class SimpleBadger(HasBadgeBase):
     ...     badges = ["bug", "fish"]
+    ...
     ...     def isBugBadgeVisible(self):
     ...         return True
+    ...
     ...     def getBugBadgeTitle(self):
-    ...         return 'Bug-Title'
+    ...         return "Bug-Title"
+    ...
     ...     def isFishBadgeVisible(self):
     ...         return True
+    ...
     ...     def getFishBadgeTitle(self):
-    ...         return 'Fish-Tooltip'
+    ...         return "Fish-Tooltip"
+    ...
     ...     def getBadge(self, badge_name):
     ...         if badge_name == "fish":
-    ...             return Badge('small-fish', 'large-fish', 'fish',
-    ...                          'Fish-Title')
+    ...             return Badge(
+    ...                 "small-fish", "large-fish", "fish", "Fish-Title"
+    ...             )
     ...         else:
     ...             return HasBadgeBase.getBadge(self, badge_name)
+    ...
 
     >>> for badge in SimpleBadger(private_object).getVisibleBadges():
     ...     print(badge.alt, "/", badge.title)
+    ...
     bug / Bug-Title
     fish / Fish-Title
 
@@ -138,6 +152,7 @@ NotImplementedError.
     >>> SimpleBadger.badges.append("blueprint")
     >>> for badge in SimpleBadger(private_object).getVisibleBadges():
     ...     print(badge.alt)
+    ...
     Traceback (most recent call last):
     ...
     AttributeError:
@@ -167,15 +182,17 @@ determination methods to use the results of an alternative query.
     >>> from zope.interface import Interface, implementer
 
     >>> class IFoo(Interface):
-    ...     bugs = Attribute('Some linked bugs')
-    ...     blueprints = Attribute('Some linked blueprints')
+    ...     bugs = Attribute("Some linked bugs")
+    ...     blueprints = Attribute("Some linked blueprints")
+    ...
 
     >>> @implementer(IFoo)
     ... class Foo:
     ...     @property
     ...     def bugs(self):
     ...         print("Foo.bugs")
-    ...         return ['a']
+    ...         return ["a"]
+    ...
     ...     @property
     ...     def blueprints(self):
     ...         print("Foo.blueprints")
@@ -185,12 +202,16 @@ Now define the adapter for the Foo content class.
 
     >>> class FooBadges(HasBadgeBase):
     ...     badges = "bug", "blueprint"
+    ...
     ...     def __init__(self, context):
     ...         self.context = context
+    ...
     ...     def isBugBadgeVisible(self):
     ...         return len(self.context.bugs) > 0
+    ...
     ...     def isBlueprintBadgeVisible(self):
     ...         return len(self.context.blueprints) > 0
+    ...
 
 Usually, one would register an adapter in ZCML from the content type to
 IHasBadges.  Here is the sample from the branch.zcml to illustrate.
@@ -221,6 +242,7 @@ as illustrated by the printed method calls.
 
     >>> for badge in badger.getVisibleBadges():
     ...     print(badge.renderIconImage())
+    ...
     Foo.bugs
     Foo.blueprints
     <img alt="bug" width="14" height="14" src="/@@/bug"
@@ -238,13 +260,15 @@ handler for branches executes a single query for the BugBranch links for the
 branches in the batch and that is used to construct the DecoratedBranch.
 
     >>> from lazr.delegates import delegate_to
-    >>> @delegate_to(IFoo, context='foo')
+    >>> @delegate_to(IFoo, context="foo")
     ... class DelegatingFoo(FooBadges):
     ...     def __init__(self, foo):
     ...         FooBadges.__init__(self, foo)
     ...         self.foo = foo
+    ...
     ...     def isBugBadgeVisible(self):
     ...         return True
+    ...
     ...     def isBlueprintBadgeVisible(self):
     ...         return False
 
@@ -266,6 +290,7 @@ content classes).
 
     >>> for badge in badger.getVisibleBadges():
     ...     print(badge.renderIconImage())
+    ...
     <img alt="bug" width="14" height="14" src="/@@/bug"
     title="Linked to a bug"/>
 
@@ -282,13 +307,13 @@ through the printed attribute accessors, uses the attributes of the
 content class.
 
     >>> from lp.testing import test_tales
-    >>> print(test_tales('context/badges:small', context=foo))
+    >>> print(test_tales("context/badges:small", context=foo))
     Foo.bugs
     Foo.blueprints
     <img alt="bug" width="14" height="14" src="/@@/bug"
          title="Linked to a bug"/>
 
-    >>> print(test_tales('context/badges:large', context=foo))
+    >>> print(test_tales("context/badges:large", context=foo))
     Foo.bugs
     Foo.blueprints
     <img alt="bug" width="32" height="32" src="/@@/bug-large"
@@ -297,9 +322,9 @@ content class.
 Using the delegating foo, we get the delegated methods called and avoid
 the content class method calls.
 
-    >>> print(test_tales('context/badges:small', context=delegating_foo))
+    >>> print(test_tales("context/badges:small", context=delegating_foo))
     <img alt="bug" width="14" height="14" src="/@@/bug"
          title="Linked to a bug"/>
-    >>> print(test_tales('context/badges:large', context=delegating_foo))
+    >>> print(test_tales("context/badges:large", context=delegating_foo))
     <img alt="bug" width="32" height="32" src="/@@/bug-large"
          title="Linked to a bug" id="bugbadge"/>
diff --git a/lib/lp/app/doc/batch-navigation.rst b/lib/lp/app/doc/batch-navigation.rst
index 36c5bfc..a93b8b8 100644
--- a/lib/lp/app/doc/batch-navigation.rst
+++ b/lib/lp/app/doc/batch-navigation.rst
@@ -27,25 +27,36 @@ Imports:
     >>> from lp.services.webapp.batching import BatchNavigator
     >>> from lp.services.webapp.servers import LaunchpadTestRequest
 
-    >>> def build_request(query_string_args=None, method='GET'):
-    ...   if query_string_args is None:
-    ...       query_string_args = {}
-    ...   query_string = "&".join(
-    ...       "%s=%s" % (k,v) for k,v in query_string_args.items())
-    ...   request = LaunchpadTestRequest(
-    ...       SERVER_URL='http://www.example.com/foo', method=method,
-    ...       environ={'QUERY_STRING': query_string})
-    ...   request.processInputs()
-    ...   return request
+    >>> def build_request(query_string_args=None, method="GET"):
+    ...     if query_string_args is None:
+    ...         query_string_args = {}
+    ...     query_string = "&".join(
+    ...         "%s=%s" % (k, v) for k, v in query_string_args.items()
+    ...     )
+    ...     request = LaunchpadTestRequest(
+    ...         SERVER_URL="http://www.example.com/foo";,
+    ...         method=method,
+    ...         environ={"QUERY_STRING": query_string},
+    ...     )
+    ...     request.processInputs()
+    ...     return request
+    ...
 
 A dummy request object:
 
 Some sample data.
 
     >>> reindeer = [
-    ...     'Dasher', 'Dancer', 'Prancer', 'Vixen', 'Comet',
-    ...     'Cupid', 'Donner', 'Blitzen', 'Rudolph',
-    ...     ]
+    ...     "Dasher",
+    ...     "Dancer",
+    ...     "Prancer",
+    ...     "Vixen",
+    ...     "Comet",
+    ...     "Cupid",
+    ...     "Donner",
+    ...     "Blitzen",
+    ...     "Rudolph",
+    ... ]
 
 
 Multiple pages
@@ -54,13 +65,14 @@ Multiple pages
 The batch navigator tells us whether multiple pages will be used.
 
     >>> from lp.services.identity.model.emailaddress import EmailAddress
-    >>> select_results = EmailAddress.select(orderBy='id')
+    >>> select_results = EmailAddress.select(orderBy="id")
     >>> batch_nav = BatchNavigator(select_results, build_request(), size=50)
     >>> batch_nav.has_multiple_pages
     True
 
     >>> one_page_nav = BatchNavigator(
-    ...     select_results, build_request(), size=200)
+    ...     select_results, build_request(), size=200
+    ... )
     >>> one_page_nav.has_multiple_pages
     False
 
@@ -75,18 +87,23 @@ InvalidBatchSizeError is raised.
 
     >>> from lp.services.config import config
     >>> from textwrap import dedent
-    >>> config.push('max-batch-size', dedent("""\
+    >>> config.push(
+    ...     "max-batch-size",
+    ...     dedent(
+    ...         """\
     ...     [launchpad]
     ...     max_batch_size: 5
-    ...     """))
+    ...     """
+    ...     ),
+    ... )
     >>> request = build_request({"start": "0", "batch": "20"})
-    >>> BatchNavigator(reindeer, request=request )
+    >>> BatchNavigator(reindeer, request=request)
     Traceback (most recent call last):
       ...
     lazr.batchnavigator.interfaces.InvalidBatchSizeError:
     Maximum for "batch" parameter is 5.
 
-    >>> ignored = config.pop('max-batch-size')
+    >>> ignored = config.pop("max-batch-size")
 
 
 Batch views
@@ -103,12 +120,14 @@ upper and lower navigation link views.
     >>> request = build_request({"start": "0", "batch": "10"})
     >>> navigator = BatchNavigator([], request=request)
     >>> upper_view = getMultiAdapter(
-    ...     (navigator, request), name='+navigation-links-upper')
+    ...     (navigator, request), name="+navigation-links-upper"
+    ... )
     >>> print(upper_view.render())
     <BLANKLINE>
 
     >>> lower_view = getMultiAdapter(
-    ...     (navigator, request), name='+navigation-links-lower')
+    ...     (navigator, request), name="+navigation-links-lower"
+    ... )
     >>> print(lower_view.render())
     <BLANKLINE>
 
@@ -117,7 +136,8 @@ batches, both the upper and lower navigation links view will render.
 
     >>> navigator = BatchNavigator(reindeer, request=request)
     >>> upper_view = getMultiAdapter(
-    ...     (navigator, request), name='+navigation-links-upper')
+    ...     (navigator, request), name="+navigation-links-upper"
+    ... )
     >>> print(upper_view.render())
     <table...
     ...<strong>1</strong>...&rarr;...<strong>9</strong>...of 9 results...
@@ -127,7 +147,8 @@ batches, both the upper and lower navigation links view will render.
     ...<span class="last inactive">...Last...
 
     >>> lower_view = getMultiAdapter(
-    ...     (navigator, request), name='+navigation-links-lower')
+    ...     (navigator, request), name="+navigation-links-lower"
+    ... )
     >>> print(lower_view.render())
     <table...
     ...<strong>1</strong>...&rarr;...<strong>9</strong>...of 9 results...
diff --git a/lib/lp/app/doc/celebrities.rst b/lib/lp/app/doc/celebrities.rst
index 720037f..25d948d 100644
--- a/lib/lp/app/doc/celebrities.rst
+++ b/lib/lp/app/doc/celebrities.rst
@@ -25,7 +25,7 @@ The 'admins' team contain the user who have super-power over all of
 Launchpad. This team is accessible through the admin attribute.
 
     >>> personset = getUtility(IPersonSet)
-    >>> admins = personset.getByName('admins')
+    >>> admins = personset.getByName("admins")
     >>> celebs.admin == admins
     True
 
@@ -35,7 +35,7 @@ vcs-imports
 
 The vcs-imports celebrity owns the branches created by importd.
 
-    >>> vcs_imports = personset.getByName('vcs-imports')
+    >>> vcs_imports = personset.getByName("vcs-imports")
     >>> celebs.vcs_imports == vcs_imports
     True
 
@@ -46,7 +46,7 @@ english
 The English language is used in many places.  It's the native language
 for translation, as well as for Launchpad itself.
 
-    >>> english = getUtility(ILanguageSet).getLanguageByCode('en')
+    >>> english = getUtility(ILanguageSet).getLanguageByCode("en")
     >>> english == celebs.english
     True
 
@@ -57,7 +57,7 @@ registry_experts
 The registry_experts celebrity has permissions to perform registry
 gardening operations.
 
-    >>> registry = personset.getByName('registry')
+    >>> registry = personset.getByName("registry")
     >>> celebs.registry_experts == registry
     True
 
@@ -68,7 +68,7 @@ buildd_admin
 The buildd_admin celebrity has permission to perform routine task in the
 buildfarm.
 
-    >>> buildd_admin = personset.getByName('launchpad-buildd-admins')
+    >>> buildd_admin = personset.getByName("launchpad-buildd-admins")
     >>> celebs.buildd_admin == buildd_admin
     True
 
@@ -78,7 +78,7 @@ bug_watch_updater
 
 The bug_watch_updater celebrity updates the bug watches.
 
-    >>> bug_watch_updater = personset.getByName('bug-watch-updater')
+    >>> bug_watch_updater = personset.getByName("bug-watch-updater")
     >>> celebs.bug_watch_updater == bug_watch_updater
     True
 
@@ -90,7 +90,7 @@ For all the products using SourceForge, we have a single registered
 tracker
 
     >>> from lp.bugs.interfaces.bugtracker import IBugTrackerSet
-    >>> sf_tracker = getUtility(IBugTrackerSet).getByName('sf')
+    >>> sf_tracker = getUtility(IBugTrackerSet).getByName("sf")
     >>> celebs.sourceforge_tracker == sf_tracker
     True
 
@@ -102,7 +102,7 @@ We have the Launchpad Janitor which takes care of expiring old
 questions, team memberships when they reach their expiry date, and old
 incomplete bugtasks.
 
-    >>> janitor = personset.getByName('janitor')
+    >>> janitor = personset.getByName("janitor")
     >>> celebs.janitor == janitor
     True
 
@@ -113,7 +113,7 @@ launchpad
 The Launchpad product itself.
 
     >>> from lp.registry.interfaces.product import IProductSet
-    >>> launchpad = getUtility(IProductSet).getByName('launchpad')
+    >>> launchpad = getUtility(IProductSet).getByName("launchpad")
     >>> celebs.launchpad == launchpad
     True
 
@@ -124,7 +124,7 @@ obsolete_junk
 The 'Obsolete Junk' project is used to hold undeletable objects like
 productseries that other projects no longer want.
 
-    >>> obsolete_junk = getUtility(IProductSet).getByName('obsolete-junk')
+    >>> obsolete_junk = getUtility(IProductSet).getByName("obsolete-junk")
     >>> celebs.obsolete_junk == obsolete_junk
     True
 
@@ -136,7 +136,7 @@ There is a 'Commercial Subscription Admins' team that has administrative
 power over the licence review process and has the ability to de-activate
 projects.
 
-    >>> commercial_admin = personset.getByName('commercial-admins')
+    >>> commercial_admin = personset.getByName("commercial-admins")
     >>> celebs.commercial_admin == commercial_admin
     True
 
@@ -148,8 +148,7 @@ There is a 'Savannah Bug Tracker' bugtracker which represents the bug
 tracker for all registered Savannah projects.
 
     >>> from lp.bugs.interfaces.bugtracker import IBugTrackerSet
-    >>> savannah_tracker = getUtility(IBugTrackerSet).getByName(
-    ...     'savannah')
+    >>> savannah_tracker = getUtility(IBugTrackerSet).getByName("savannah")
     >>> celebs.savannah_tracker == savannah_tracker
     True
 
@@ -158,6 +157,7 @@ http://savannah.nognu.org/
 
     >>> for alias in celebs.savannah_tracker.aliases:
     ...     print(alias)
+    ...
     http://savannah.nognu.org/
 
 
@@ -167,7 +167,7 @@ Gnome Bugzilla
 There is a 'Gnome Bugzilla' celebrity, which is used to represent the
 Gnome Bugzilla instance by the checkwatches script.
 
-    >>> gnome_bugzilla = getUtility(IBugTrackerSet).getByName('gnome-bugs')
+    >>> gnome_bugzilla = getUtility(IBugTrackerSet).getByName("gnome-bugs")
     >>> celebs.gnome_bugzilla == gnome_bugzilla
     True
 
@@ -177,7 +177,7 @@ PPA key guard
 
 There is a 'PPA key guard' celebrity which owns all PPA 'signing_keys'.
 
-    >>> ppa_key_guard = personset.getByName('ppa-key-guard')
+    >>> ppa_key_guard = personset.getByName("ppa-key-guard")
     >>> celebs.ppa_key_guard == ppa_key_guard
     True
 
@@ -189,7 +189,7 @@ There's a celebrity for the Ubuntu technical board, the 'techboard'
 team. It's used for determining who is allowed to create new package
 sets.
 
-    >>> ubuntu_techboard = personset.getByName('techboard')
+    >>> ubuntu_techboard = personset.getByName("techboard")
     >>> print(ubuntu_techboard.name)
     techboard
 
@@ -210,22 +210,28 @@ adding/removing the appropriate "in_" attribute(s).
     ...     for name in ILaunchpadCelebrities.names():
     ...         if IPerson.providedBy(getattr(celebs, name)):
     ...             yield "in_" + name
+    ...
     >>> def get_person_roles_names():
     ...     for name in IPersonRoles.names():
     ...         if name.startswith("in_"):
     ...             yield name
+    ...
 
 Treating the lists as sets and determining their difference gives us a
 clear picture of what is missing where.
 
     >>> person_celebrity_names = set(get_person_celebrity_names())
     >>> person_roles_names = set(get_person_roles_names())
-    >>> print("Please add to IPersonRoles: " + (
-    ...       ", ".join(list(person_celebrity_names - person_roles_names))))
+    >>> print(
+    ...     "Please add to IPersonRoles: "
+    ...     + (", ".join(list(person_celebrity_names - person_roles_names)))
+    ... )
     Please add to IPersonRoles:
 
-    >>> print("Please remove from IPersonRoles: " + (
-    ...       ", ".join(list(person_roles_names - person_celebrity_names))))
+    >>> print(
+    ...     "Please remove from IPersonRoles: "
+    ...     + (", ".join(list(person_roles_names - person_celebrity_names)))
+    ... )
     Please remove from IPersonRoles:
 
 
@@ -240,13 +246,13 @@ We can ask if a person has celebrity status.
     >>> celebs.isCelebrityPerson(obsolete_junk.name)
     False
 
-    >>> celebs.isCelebrityPerson('admins')
+    >>> celebs.isCelebrityPerson("admins")
     True
 
-    >>> celebs.isCelebrityPerson('admin')
+    >>> celebs.isCelebrityPerson("admin")
     False
 
-    >>> celebs.isCelebrityPerson('janitor')
+    >>> celebs.isCelebrityPerson("janitor")
     True
 
 
diff --git a/lib/lp/app/doc/displaying-dates.rst b/lib/lp/app/doc/displaying-dates.rst
index dbd2d54..401e522 100644
--- a/lib/lp/app/doc/displaying-dates.rst
+++ b/lib/lp/app/doc/displaying-dates.rst
@@ -29,7 +29,7 @@ First, let's bring in some dependencies:
     >>> from datetime import datetime, timedelta
     >>> from lp.testing import test_tales
     >>> import pytz
-    >>> UTC = pytz.timezone('UTC')
+    >>> UTC = pytz.timezone("UTC")
 
 fmt:approximatedate and fmt:displaydate display the difference between
 the formatted timestamp and the present.  This is a really bad idea
@@ -46,93 +46,115 @@ timestamp.
     ...             return fixed_time_utc
     ...         else:
     ...             return fixed_time
+    ...
     >>> from zope.component import provideAdapter
     >>> from zope.traversing.interfaces import IPathAdapter
     >>> provideAdapter(
-    ...     TestDateTimeFormatterAPI, (datetime,), IPathAdapter, 'testfmt')
+    ...     TestDateTimeFormatterAPI, (datetime,), IPathAdapter, "testfmt"
+    ... )
 
 A time that is ten seconds or less will be displayed as an approximate:
 
     >>> t = fixed_time + timedelta(0, 5, 0)
-    >>> test_tales('t/testfmt:approximatedate', t=t)
+    >>> test_tales("t/testfmt:approximatedate", t=t)
     'in a moment'
     >>> t = fixed_time + timedelta(0, 9, 0)
-    >>> test_tales('t/testfmt:approximatedate', t=t)
+    >>> test_tales("t/testfmt:approximatedate", t=t)
     'in a moment'
-    >>> print(test_tales('t/testfmt:approximatedate', t=t) ==
-    ...       test_tales('t/testfmt:displaydate', t=t))
+    >>> print(
+    ...     test_tales("t/testfmt:approximatedate", t=t)
+    ...     == test_tales("t/testfmt:displaydate", t=t)
+    ... )
     True
     >>> t = fixed_time_utc - timedelta(0, 10, 0)
-    >>> test_tales('t/testfmt:approximatedate', t=t)
+    >>> test_tales("t/testfmt:approximatedate", t=t)
     'a moment ago'
-    >>> print(test_tales('t/testfmt:approximatedate', t=t) ==
-    ...       test_tales('t/testfmt:displaydate', t=t))
+    >>> print(
+    ...     test_tales("t/testfmt:approximatedate", t=t)
+    ...     == test_tales("t/testfmt:displaydate", t=t)
+    ... )
     True
 
 A time that is very close to the present will be displayed in seconds:
 
     >>> t = fixed_time + timedelta(0, 11, 0)
-    >>> test_tales('t/testfmt:approximatedate', t=t)
+    >>> test_tales("t/testfmt:approximatedate", t=t)
     'in 11 seconds'
-    >>> print(test_tales('t/testfmt:approximatedate', t=t) ==
-    ...       test_tales('t/testfmt:displaydate', t=t))
+    >>> print(
+    ...     test_tales("t/testfmt:approximatedate", t=t)
+    ...     == test_tales("t/testfmt:displaydate", t=t)
+    ... )
     True
     >>> t = fixed_time_utc - timedelta(0, 25, 0)
-    >>> test_tales('t/testfmt:approximatedate', t=t)
+    >>> test_tales("t/testfmt:approximatedate", t=t)
     '25 seconds ago'
-    >>> print(test_tales('t/testfmt:approximatedate', t=t) ==
-    ...       test_tales('t/testfmt:displaydate', t=t))
+    >>> print(
+    ...     test_tales("t/testfmt:approximatedate", t=t)
+    ...     == test_tales("t/testfmt:displaydate", t=t)
+    ... )
     True
 
 Further out we expect minutes.  Note that for singular units (e.g. "1
 minute"), we present the singular unit:
 
     >>> t = fixed_time_utc + timedelta(0, 185, 0)
-    >>> test_tales('t/testfmt:approximatedate', t=t)
+    >>> test_tales("t/testfmt:approximatedate", t=t)
     'in 3 minutes'
-    >>> print(test_tales('t/testfmt:approximatedate', t=t) ==
-    ...       test_tales('t/testfmt:displaydate', t=t))
+    >>> print(
+    ...     test_tales("t/testfmt:approximatedate", t=t)
+    ...     == test_tales("t/testfmt:displaydate", t=t)
+    ... )
     True
     >>> t = fixed_time_utc - timedelta(0, 75, 0)
-    >>> test_tales('t/testfmt:approximatedate', t=t)
+    >>> test_tales("t/testfmt:approximatedate", t=t)
     '1 minute ago'
-    >>> print(test_tales('t/testfmt:approximatedate', t=t) ==
-    ...       test_tales('t/testfmt:displaydate', t=t))
+    >>> print(
+    ...     test_tales("t/testfmt:approximatedate", t=t)
+    ...     == test_tales("t/testfmt:displaydate", t=t)
+    ... )
     True
 
 Further out we expect hours:
 
     >>> t = fixed_time_utc + timedelta(0, 3635, 0)
-    >>> test_tales('t/testfmt:approximatedate', t=t)
+    >>> test_tales("t/testfmt:approximatedate", t=t)
     'in 1 hour'
-    >>> print(test_tales('t/testfmt:approximatedate', t=t) ==
-    ...       test_tales('t/testfmt:displaydate', t=t))
+    >>> print(
+    ...     test_tales("t/testfmt:approximatedate", t=t)
+    ...     == test_tales("t/testfmt:displaydate", t=t)
+    ... )
     True
     >>> t = fixed_time_utc - timedelta(0, 3635, 0)
-    >>> test_tales('t/testfmt:approximatedate', t=t)
+    >>> test_tales("t/testfmt:approximatedate", t=t)
     '1 hour ago'
-    >>> print(test_tales('t/testfmt:approximatedate', t=t) ==
-    ...       test_tales('t/testfmt:displaydate', t=t))
+    >>> print(
+    ...     test_tales("t/testfmt:approximatedate", t=t)
+    ...     == test_tales("t/testfmt:displaydate", t=t)
+    ... )
     True
 
 And if the approximate date is more than a day away, we expect the date. We
 also expect the fmt:displaydate to change form, and become "on yyyy-mm-dd".
 
     >>> t = datetime(2004, 1, 13, 15, 35)
-    >>> test_tales('t/testfmt:approximatedate', t=t)
+    >>> test_tales("t/testfmt:approximatedate", t=t)
     '2004-01-13'
-    >>> print(test_tales('t/testfmt:approximatedate', t=t) ==
-    ...       test_tales('t/testfmt:displaydate', t=t))
+    >>> print(
+    ...     test_tales("t/testfmt:approximatedate", t=t)
+    ...     == test_tales("t/testfmt:displaydate", t=t)
+    ... )
     False
-    >>> test_tales('t/testfmt:displaydate', t=t)
+    >>> test_tales("t/testfmt:displaydate", t=t)
     'on 2004-01-13'
     >>> t = datetime(2015, 1, 13, 15, 35)
-    >>> test_tales('t/testfmt:approximatedate', t=t)
+    >>> test_tales("t/testfmt:approximatedate", t=t)
     '2015-01-13'
-    >>> print(test_tales('t/testfmt:approximatedate', t=t) ==
-    ...       test_tales('t/testfmt:displaydate', t=t))
+    >>> print(
+    ...     test_tales("t/testfmt:approximatedate", t=t)
+    ...     == test_tales("t/testfmt:displaydate", t=t)
+    ... )
     False
-    >>> test_tales('t/testfmt:displaydate', t=t)
+    >>> test_tales("t/testfmt:displaydate", t=t)
     'on 2015-01-13'
 
 We have two more related TALES formatters, fmt:approximatedatetitle and
@@ -141,9 +163,9 @@ fmt:displaydatetitle.  These act similarly to their siblings without
 and "datetime" attributes in order that browsers show the timestamp as hover
 text.
 
-    >>> print(test_tales('t/testfmt:approximatedatetitle', t=t))
+    >>> print(test_tales("t/testfmt:approximatedatetitle", t=t))
     <time title="2015-01-13 15:35:00"
           datetime="2015-01-13T15:35:00">2015-01-13</time>
-    >>> print(test_tales('t/testfmt:displaydatetitle', t=t))
+    >>> print(test_tales("t/testfmt:displaydatetitle", t=t))
     <time title="2015-01-13 15:35:00"
           datetime="2015-01-13T15:35:00">on 2015-01-13</time>
diff --git a/lib/lp/app/doc/displaying-numbers.rst b/lib/lp/app/doc/displaying-numbers.rst
index 1f4131b..a91cfb7 100644
--- a/lib/lp/app/doc/displaying-numbers.rst
+++ b/lib/lp/app/doc/displaying-numbers.rst
@@ -9,34 +9,34 @@ bytes: Byte contractions
 The TALES formatter for numbers allows them to be rendered as byte
 contractions as per IEC60027-2:
 
-    >>> test_tales('foo/fmt:bytes', foo=0)
+    >>> test_tales("foo/fmt:bytes", foo=0)
     '0 bytes'
 
-    >>> test_tales('foo/fmt:bytes', foo=1)
+    >>> test_tales("foo/fmt:bytes", foo=1)
     '1 byte'
 
-    >>> test_tales('foo/fmt:bytes', foo=123)
+    >>> test_tales("foo/fmt:bytes", foo=123)
     '123 bytes'
 
-    >>> test_tales('foo/fmt:bytes', foo=1234)
+    >>> test_tales("foo/fmt:bytes", foo=1234)
     '1.2 KiB'
 
-    >>> test_tales('foo/fmt:bytes', foo=12345678)
+    >>> test_tales("foo/fmt:bytes", foo=12345678)
     '11.8 MiB'
 
-    >>> test_tales('foo/fmt:bytes', foo=12345567901)
+    >>> test_tales("foo/fmt:bytes", foo=12345567901)
     '11.5 GiB'
 
-    >>> test_tales('foo/fmt:bytes', foo=12345678901234)
+    >>> test_tales("foo/fmt:bytes", foo=12345678901234)
     '11.2 TiB'
 
-    >>> test_tales('foo/fmt:bytes', foo=123456789012345678)
+    >>> test_tales("foo/fmt:bytes", foo=123456789012345678)
     '109.7 PiB'
 
 We top out at YiB though, since there is no valid contraction above
 that (thank god!)
 
-    >>> test_tales('foo/fmt:bytes', foo=123456789012345678901234567890)
+    >>> test_tales("foo/fmt:bytes", foo=123456789012345678901234567890)
     '102121.1 YiB'
 
 float: Float formatting
@@ -48,7 +48,7 @@ to how the Python "%f" string formatter works:
 For instance:
 
     >>> foo = 12345.67890
-    >>> print(test_tales('foo/fmt:float/7.2', foo=foo))
+    >>> print(test_tales("foo/fmt:float/7.2", foo=foo))
     12345.68
 
 Is the same as:
@@ -58,31 +58,31 @@ Is the same as:
 
 Here's a set of exhaustive examples:
 
-    >>> test_tales('foo/fmt:float', foo=12345.67890)
+    >>> test_tales("foo/fmt:float", foo=12345.67890)
     Traceback (most recent call last):
     ...
     zope.location.interfaces.LocationError:
     'fmt:float requires a single decimal argument'
 
-    >>> test_tales('foo/fmt:float/0.3', foo=-12345.67890)
+    >>> test_tales("foo/fmt:float/0.3", foo=-12345.67890)
     '-12345.679'
 
-    >>> test_tales('foo/fmt:float/.4', foo=12345.67890)
+    >>> test_tales("foo/fmt:float/.4", foo=12345.67890)
     '12345.6789'
 
-    >>> test_tales('foo/fmt:float/-.2', foo=12345.67890)
+    >>> test_tales("foo/fmt:float/-.2", foo=12345.67890)
     '12345.68'
 
-    >>> test_tales('foo/fmt:float/-7.1', foo=-12345.67890)
+    >>> test_tales("foo/fmt:float/-7.1", foo=-12345.67890)
     '-12345.7'
 
-    >>> test_tales('foo/fmt:float/2', foo=12345.67890)
+    >>> test_tales("foo/fmt:float/2", foo=12345.67890)
     '12346'
 
-    >>> test_tales('foo/fmt:float/-2', foo=-12345.67890)
+    >>> test_tales("foo/fmt:float/-2", foo=-12345.67890)
     '-12346'
 
-    >>> test_tales('foo/fmt:float/bong', foo=12345.67890)
+    >>> test_tales("foo/fmt:float/bong", foo=12345.67890)
     Traceback (most recent call last):
     ...
     ValueError: ... float...bong...
diff --git a/lib/lp/app/doc/displaying-paragraphs-of-text.rst b/lib/lp/app/doc/displaying-paragraphs-of-text.rst
index c50955a..3cf2c14 100644
--- a/lib/lp/app/doc/displaying-paragraphs-of-text.rst
+++ b/lib/lp/app/doc/displaying-paragraphs-of-text.rst
@@ -10,29 +10,26 @@ Basics
 
     >>> from lp.testing import test_tales
 
-    >>> text = ('This is a paragraph.\n'
-    ...         '\n'
-    ...         'This is another paragraph.')
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    >>> text = "This is a paragraph.\n" "\n" "This is another paragraph."
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p>This is a paragraph.</p>
     <p>This is another paragraph.</p>
 
-    >>> text = ('This is a line.\n'
-    ...         'This is another line.')
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    >>> text = "This is a line.\n" "This is another line."
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p>This is a line.<br />
     This is another line.</p>
 
     >>> text = (
-    ...     'This is a paragraph that has been hard-wrapped by an email'
-    ...     ' application.\n'
-    ...     'We used to handle this specially, but we no longer do because it'
-    ...     ' was disturbing\n'
-    ...     'the display of backtraces. Expected results:\n'
-    ...     '* joy\n'
-    ...     '* elation'
-    ...     )
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    ...     "This is a paragraph that has been hard-wrapped by an email"
+    ...     " application.\n"
+    ...     "We used to handle this specially, but we no longer do because it"
+    ...     " was disturbing\n"
+    ...     "the display of backtraces. Expected results:\n"
+    ...     "* joy\n"
+    ...     "* elation"
+    ... )
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p>This is a paragraph that has been hard-wrapped by an email
     application.<br />
     We used to handle this specially, but we no longer do because it was
@@ -48,8 +45,8 @@ Basics
     ...     "means converting them to &nbsp;. Trailing spaces are passed "
     ...     "through as-is, which means browsers will ignore them, but "
     ...     "that's fine, they're not important anyway.\n"
-    ...     )
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    ... )
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p>&nbsp;1. Here&#x27;s an example<br />
     &nbsp;2. where a list is followed by a paragraph.<br />
     &nbsp;&nbsp;&nbsp;Leading spaces in a line or paragraph are presented,
@@ -57,20 +54,19 @@ Basics
     through as-is, which means browsers will ignore them, but that&#x27;s
     fine, they&#x27;re not important anyway.</p>
 
-    >>> text = (
-    ...     'This is a little paragraph all by itself. How cute!'
-    ...     )
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    >>> text = "This is a little paragraph all by itself. How cute!"
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p>This is a little paragraph all by itself. How cute!</p>
 
     >>> text = (
-    ...     'Here are two paragraphs with lots of whitespace between them.\n'
-    ...     '\n'
-    ...     '\n'
-    ...     '\n'
-    ...     '\n'
-    ...     'But they\'re still just two paragraphs.')
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    ...     "Here are two paragraphs with lots of whitespace between them.\n"
+    ...     "\n"
+    ...     "\n"
+    ...     "\n"
+    ...     "\n"
+    ...     "But they're still just two paragraphs."
+    ... )
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p>Here are two paragraphs with lots of whitespace between them.</p>
     <p>But they&#x27;re still just two paragraphs.</p>
 
@@ -78,15 +74,16 @@ If a line begins with whitespace, it will not be merged with the
 previous line.  This aids in the display of code samples:
 
     >>> text = (
-    ...     'This is a code sample written in Python.\n'
-    ...     '    def messageCount(self):\n'
+    ...     "This is a code sample written in Python.\n"
+    ...     "    def messageCount(self):\n"
     ...     '        """See IRosettaStats."""\n'
-    ...     '        return self.potemplate.messageCount()\n'
-    ...     '\n'
-    ...     '    def currentCount(self, language=None):\n'
+    ...     "        return self.potemplate.messageCount()\n"
+    ...     "\n"
+    ...     "    def currentCount(self, language=None):\n"
     ...     '        """See IRosettaStats."""\n'
-    ...     '        return self.currentCount\n')
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))  # noqa
+    ...     "        return self.currentCount\n"
+    ... )
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))  # noqa
     <p>This is a code sample written in Python.<br />
     &nbsp;&nbsp;&nbsp;&nbsp;def messageCount(self):<br />
     &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;&quot;&quot;See IRosettaStats.&quot;&quot;&quot;<br />
@@ -98,16 +95,17 @@ previous line.  This aids in the display of code samples:
 Testing a bunch of URL links.
 
     >>> text = (
-    ...     'https://launchpad.net/ is the new Launchpad site\n'
-    ...     'http://example.com/something?foo=bar&hum=baz\n'
-    ...     'You can check the PPC md5sums at '
-    ...     'ftp://ftp.ubuntu.com/ubuntu/dists/breezy/main/installer-powerpc'
-    ...     '/current/images/MD5SUMS\n'
-    ...     'irc://chat.freenode.net/#launchpad\n'
-    ...     '\n'
-    ...     'I have a Jabber account (jabber:foo@xxxxxxxxxxxxxxxxxx)\n'
-    ...     'Foo Bar <mailto:foo.bar@xxxxxxxxxxx>')
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))  # noqa
+    ...     "https://launchpad.net/ is the new Launchpad site\n"
+    ...     "http://example.com/something?foo=bar&hum=baz\n";
+    ...     "You can check the PPC md5sums at "
+    ...     "ftp://ftp.ubuntu.com/ubuntu/dists/breezy/main/installer-powerpc";
+    ...     "/current/images/MD5SUMS\n"
+    ...     "irc://chat.freenode.net/#launchpad\n"
+    ...     "\n"
+    ...     "I have a Jabber account (jabber:foo@xxxxxxxxxxxxxxxxxx)\n"
+    ...     "Foo Bar <mailto:foo.bar@xxxxxxxxxxx>"
+    ... )
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))  # noqa
     <p><a rel="nofollow" href="https://launchpad.net/";>https:/<wbr />/launchpad.<wbr />net/</a> is the new Launchpad site<br />
     <a rel="nofollow" href="http://example.com/something?foo=bar&amp;hum=baz";>http://<wbr />example.<wbr />com/something?<wbr />foo=bar&amp;<wbr />hum=baz</a><br />
     You can check the PPC md5sums at <a rel="nofollow" href="ftp://ftp.ubuntu.com/ubuntu/dists/breezy/main/installer-powerpc/current/images/MD5SUMS";>ftp://ftp.<wbr />ubuntu.<wbr />com/ubuntu/<wbr />dists/breezy/<wbr />main/installer-<wbr />powerpc/<wbr />current/<wbr />images/<wbr />MD5SUMS</a><br />
@@ -122,47 +120,47 @@ URL linkification
 fmt:text-to-html knows how to linkify URLs:
 
     >>> text = (
-    ...     'http://localhost:8086/bar/baz/foo.html\n'
-    ...     'ftp://localhost:8086/bar/baz/foo.bar.html\n'
-    ...     'sftp://localhost:8086/bar/baz/foo.bar.html.\n'
-    ...     'http://localhost:8086/bar/baz/foo.bar.html;\n'
-    ...     'news://localhost:8086/bar/baz/foo.bar.html:\n'
-    ...     'http://localhost:8086/bar/baz/foo.bar.html?\n'
-    ...     'http://localhost:8086/bar/baz/foo.bar.html,\n'
-    ...     '<http://localhost:8086/bar/baz/foo.bar.html>\n'
-    ...     '<http://localhost:8086/bar/baz/foo.bar.html>,\n'
-    ...     '<http://localhost:8086/bar/baz/foo.bar.html>.\n'
-    ...     '<http://localhost:8086/bar/baz/foo.bar.html>;\n'
-    ...     '<http://localhost:8086/bar/baz/foo.bar.html>:\n'
-    ...     '<http://localhost:8086/bar/baz/foo.bar.html>?\n'
-    ...     '(http://localhost:8086/bar/baz/foo.bar.html)\n'
-    ...     '(http://localhost:8086/bar/baz/foo.bar.html),\n'
-    ...     '(http://localhost:8086/bar/baz/foo.bar.html).\n'
-    ...     '(http://localhost:8086/bar/baz/foo.bar.html);\n'
-    ...     '(http://localhost:8086/bar/baz/foo.bar.html):\n'
-    ...     'http://localhost/bar/baz/foo.bar.html?a=b&b=a\n'
-    ...     'http://localhost/bar/baz/foo.bar.html?a=b&b=a.\n'
-    ...     'http://localhost/bar/baz/foo.bar.html?a=b&b=a,\n'
-    ...     'http://localhost/bar/baz/foo.bar.html?a=b&b=a;\n'
-    ...     'http://localhost/bar/baz/foo.bar.html?a=b&b=a:\n'
-    ...     'http://localhost/bar/baz/foo.bar.html?'
-    ...         'a=b&b=a:b;c@d_e%f~g#h,j!k-l+m$n*o\'p\n'
-    ...     'http://www.searchtools.com/test/urls/(parens).html\n'
-    ...     'http://www.searchtools.com/test/urls/-dash.html\n'
-    ...     'http://www.searchtools.com/test/urls/_underscore.html\n'
-    ...     'http://www.searchtools.com/test/urls/period.x.html\n'
-    ...     'http://www.searchtools.com/test/urls/!exclamation.html\n'
-    ...     'http://www.searchtools.com/test/urls/~tilde.html\n'
-    ...     'http://www.searchtools.com/test/urls/*asterisk.html\n'
-    ...     'irc://chat.freenode.net/launchpad\n'
-    ...     'irc://chat.freenode.net/%23launchpad,isserver\n'
-    ...     'mailto:noreply@xxxxxxxxxxxxx\n'
-    ...     'jabber:noreply@xxxxxxxxxxxxx\n'
-    ...     'http://localhost/foo?xxx&\n'
-    ...     'http://localhost?testing=[square-brackets-in-query]\n'
+    ...     "http://localhost:8086/bar/baz/foo.html\n";
+    ...     "ftp://localhost:8086/bar/baz/foo.bar.html\n";
+    ...     "sftp://localhost:8086/bar/baz/foo.bar.html.\n";
+    ...     "http://localhost:8086/bar/baz/foo.bar.html;\n";
+    ...     "news://localhost:8086/bar/baz/foo.bar.html:\n";
+    ...     "http://localhost:8086/bar/baz/foo.bar.html?\n";
+    ...     "http://localhost:8086/bar/baz/foo.bar.html,\n";
+    ...     "<http://localhost:8086/bar/baz/foo.bar.html>\n"
+    ...     "<http://localhost:8086/bar/baz/foo.bar.html>,\n"
+    ...     "<http://localhost:8086/bar/baz/foo.bar.html>.\n"
+    ...     "<http://localhost:8086/bar/baz/foo.bar.html>;\n"
+    ...     "<http://localhost:8086/bar/baz/foo.bar.html>:\n"
+    ...     "<http://localhost:8086/bar/baz/foo.bar.html>?\n"
+    ...     "(http://localhost:8086/bar/baz/foo.bar.html)\n"
+    ...     "(http://localhost:8086/bar/baz/foo.bar.html),\n"
+    ...     "(http://localhost:8086/bar/baz/foo.bar.html).\n"
+    ...     "(http://localhost:8086/bar/baz/foo.bar.html);\n"
+    ...     "(http://localhost:8086/bar/baz/foo.bar.html):\n"
+    ...     "http://localhost/bar/baz/foo.bar.html?a=b&b=a\n";
+    ...     "http://localhost/bar/baz/foo.bar.html?a=b&b=a.\n";
+    ...     "http://localhost/bar/baz/foo.bar.html?a=b&b=a,\n";
+    ...     "http://localhost/bar/baz/foo.bar.html?a=b&b=a;\n";
+    ...     "http://localhost/bar/baz/foo.bar.html?a=b&b=a:\n";
+    ...     "http://localhost/bar/baz/foo.bar.html?";
+    ...     "a=b&b=a:b;c@d_e%f~g#h,j!k-l+m$n*o'p\n"
+    ...     "http://www.searchtools.com/test/urls/(parens).html\n"
+    ...     "http://www.searchtools.com/test/urls/-dash.html\n";
+    ...     "http://www.searchtools.com/test/urls/_underscore.html\n";
+    ...     "http://www.searchtools.com/test/urls/period.x.html\n";
+    ...     "http://www.searchtools.com/test/urls/!exclamation.html\n";
+    ...     "http://www.searchtools.com/test/urls/~tilde.html\n";
+    ...     "http://www.searchtools.com/test/urls/*asterisk.html\n";
+    ...     "irc://chat.freenode.net/launchpad\n"
+    ...     "irc://chat.freenode.net/%23launchpad,isserver\n"
+    ...     "mailto:noreply@xxxxxxxxxxxxx\n";
+    ...     "jabber:noreply@xxxxxxxxxxxxx\n"
+    ...     "http://localhost/foo?xxx&\n";
+    ...     "http://localhost?testing=[square-brackets-in-query]\n";
     ... )
 
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))  # noqa
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))  # noqa
     <p><a rel="nofollow" href="http://localhost:8086/bar/baz/foo.html";>http://<wbr />localhost:<wbr />8086/bar/<wbr />baz/foo.<wbr />html</a><br />
     <a rel="nofollow" href="ftp://localhost:8086/bar/baz/foo.bar.html";>ftp://localhost<wbr />:8086/bar/<wbr />baz/foo.<wbr />bar.html</a><br />
     <a rel="nofollow" href="sftp://localhost:8086/bar/baz/foo.bar.html";>sftp://<wbr />localhost:<wbr />8086/bar/<wbr />baz/foo.<wbr />bar.html</a>.<br />
@@ -204,10 +202,8 @@ fmt:text-to-html knows how to linkify URLs:
 
 The fmt:text-to-html formatter leaves a number of non-URIs unlinked:
 
-    >>> text = (
-    ...     'nothttp://launchpad.net/\n'
-    ...     'http::No-cache=True\n')
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    >>> text = "nothttp://launchpad.net/\n"; "http::No-cache=True\n"
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p>nothttp:<wbr />//launchpad.<wbr />net/<br />
     http::No-cache=True</p>
 
@@ -219,24 +215,25 @@ fmt:text-to-html is also smart enough to convert bug references into
 links:
 
     >>> text = (
-    ...     'bug 123\n'
-    ...     'bug    123\n'
-    ...     'bug #123\n'
-    ...     'bug number 123\n'
-    ...     'bug number. 123\n'
-    ...     'bug num 123\n'
-    ...     'bug num. 123\n'
-    ...     'bug no 123\n'
-    ...     'bug report 123\n'
-    ...     'bug no. 123\n'
-    ...     'bug#123\n'
-    ...     'bug-123\n'
-    ...     'bug-report-123\n'
-    ...     'bug=123\n'
-    ...     'bug\n'
-    ...     '#123\n'
-    ...     'debug #52\n')
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    ...     "bug 123\n"
+    ...     "bug    123\n"
+    ...     "bug #123\n"
+    ...     "bug number 123\n"
+    ...     "bug number. 123\n"
+    ...     "bug num 123\n"
+    ...     "bug num. 123\n"
+    ...     "bug no 123\n"
+    ...     "bug report 123\n"
+    ...     "bug no. 123\n"
+    ...     "bug#123\n"
+    ...     "bug-123\n"
+    ...     "bug-report-123\n"
+    ...     "bug=123\n"
+    ...     "bug\n"
+    ...     "#123\n"
+    ...     "debug #52\n"
+    ... )
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p><a href="/bugs/123" class="bug-link">bug 123</a><br />
     <a href="/bugs/123" class="bug-link">bug    123</a><br />
     <a href="/bugs/123" class="bug-link">bug #123</a><br />
@@ -254,50 +251,50 @@ links:
     <a href="/bugs/123" class="bug-link">bug<br /> #123</a><br />
     debug #52</p>
 
-    >>> text = (
-    ...     'bug 123\n'
-    ...     'bug 123\n')
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    >>> text = "bug 123\n" "bug 123\n"
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p><a href="/bugs/123" class="bug-link">bug 123</a><br />
     <a href="/bugs/123" class="bug-link">bug 123</a></p>
 
-    >>> text = (
-    ...     'bug 1234\n'
-    ...     'bug 123\n')
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    >>> text = "bug 1234\n" "bug 123\n"
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p><a href="/bugs/1234" class="bug-link">bug 1234</a><br />
     <a href="/bugs/123" class="bug-link">bug 123</a></p>
 
-    >>> text = 'bug 0123\n'
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    >>> text = "bug 0123\n"
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p><a href="/bugs/123" class="bug-link">bug 0123</a></p>
 
 
 We linkify bugs that are in the Ubuntu convention for referring to bugs in
 Debian changelogs.
 
-    >>> text = 'LP: #123.\n'
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    >>> text = "LP: #123.\n"
+    ...
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p>LP: <a href="/bugs/123" class="bug-link">#123</a>.</p>
 
 Works with multiple bugs:
 
-    >>> text = 'LP: #123, #2.\n'
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    >>> text = "LP: #123, #2.\n"
+    ...
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p>LP: <a href="/bugs/123" class="bug-link">#123</a>,
            <a href="/bugs/2" class="bug-link">#2</a>.</p>
 
 And with lower case 'lp' too:
 
-    >>> text = 'lp: #123, #2.\n'
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    >>> text = "lp: #123, #2.\n"
+    ...
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p>lp: <a href="/bugs/123" class="bug-link">#123</a>,
            <a href="/bugs/2" class="bug-link">#2</a>.</p>
 
 Even line breaks cannot stop the power of bug linking:
 
-    >>> text = 'LP:  #123,\n#2.\n'
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    >>> text = "LP:  #123,\n#2.\n"
+    ...
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p>LP:  <a href="/bugs/123" class="bug-link">#123</a>,<br />
     <a href="/bugs/2" class="bug-link">#2</a>.</p>
 
@@ -320,8 +317,8 @@ subscriber.
 
 A private bug is still linked as no check is made on the actual bug.
 
-    >>> text = 'bug 6\n'
-    >>> print(test_tales('foo/fmt:text-to-html', foo=text))
+    >>> text = "bug 6\n"
+    >>> print(test_tales("foo/fmt:text-to-html", foo=text))
     <p><a href="/bugs/6" class="bug-link">bug 6</a></p>
 
 
@@ -331,14 +328,14 @@ FAQ references
 FAQ references are global, and also linkified:
 
     >>> text = (
-    ...     'faq 1\n'
-    ...     'faq #2\n'
-    ...     'faq-2\n'
-