Merge ~cjwatson/launchpad:py3-app-doctest-unicode-strings into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: deeca7c8d97ee34940daea25ddb940708d13b853
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:py3-app-doctest-unicode-strings
Merge into: launchpad:master
Diff against target: 1425 lines (+341/-326)
20 files modified
lib/lp/app/browser/doc/launchpad-search-pages.txt (+80/-76)
lib/lp/app/doc/batch-navigation.txt (+4/-4)
lib/lp/app/doc/displaying-paragraphs-of-text.txt (+10/-8)
lib/lp/app/doc/hierarchical-menu.txt (+2/-2)
lib/lp/app/doc/launchpadform.txt (+24/-22)
lib/lp/app/doc/launchpadformharness.txt (+2/-2)
lib/lp/app/doc/launchpadview.txt (+4/-4)
lib/lp/app/doc/menus.txt (+4/-4)
lib/lp/app/doc/tales.txt (+169/-162)
lib/lp/app/doc/validation.txt (+2/-2)
lib/lp/app/stories/basics/xx-dbpolicy.txt (+2/-2)
lib/lp/app/stories/basics/xx-opstats.txt (+2/-2)
lib/lp/app/widgets/date.py (+4/-4)
lib/lp/app/widgets/doc/image-widget.txt (+8/-10)
lib/lp/app/widgets/doc/lower-case-text-widget.txt (+5/-5)
lib/lp/app/widgets/doc/noneable-text-widgets.txt (+2/-3)
lib/lp/app/widgets/doc/project-scope-widget.txt (+2/-2)
lib/lp/app/widgets/doc/stripped-text-widget.txt (+2/-2)
lib/lp/app/widgets/doc/tokens-text-widget.txt (+2/-2)
lib/lp/app/widgets/textwidgets.py (+11/-8)
Reviewer Review Type Date Requested Status
Colin Watson Approve
Review via email: mp+396328@code.launchpad.net

Commit message

lp.app: Fix u'...' doctest examples for Python 3

Description of the change

There are loads of u'...' doctest examples in our test suite, and unfortunately I don't think any solution is better than just going through and making all of them bilingual. In most cases the best option is to use print(), but occasionally something like the repr of six.ensure_str() works better in cases where we need a finer-grained test.

This is the first tranche of fixes for this pattern, beginning arbitrarily with lp.app.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

Self-approving as discussed on Mattermost.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/app/browser/doc/launchpad-search-pages.txt b/lib/lp/app/browser/doc/launchpad-search-pages.txt
2index 83347e4..ded7f0d 100644
3--- a/lib/lp/app/browser/doc/launchpad-search-pages.txt
4+++ b/lib/lp/app/browser/doc/launchpad-search-pages.txt
5@@ -54,12 +54,12 @@ When text is not None, the title indicates what was searched.
6 >>> search_view = getSearchView(
7 ... form={'field.text': 'albatross'})
8
9- >>> search_view.text
10- u'albatross'
11- >>> search_view.page_title
12- u'Pages matching "albatross" in Launchpad'
13- >>> search_view.page_heading
14- u'Pages matching "albatross" in Launchpad'
15+ >>> print(search_view.text)
16+ albatross
17+ >>> print(search_view.page_title)
18+ Pages matching "albatross" in Launchpad
19+ >>> print(search_view.page_heading)
20+ Pages matching "albatross" in Launchpad
21
22
23 No matches
24@@ -92,14 +92,14 @@ matched by their id.
25
26 >>> search_view = getSearchView(
27 ... form={'field.text': '5'})
28- >>> search_view._getNumericToken(search_view.text)
29- u'5'
30+ >>> print(search_view._getNumericToken(search_view.text))
31+ 5
32 >>> search_view.has_matches
33 True
34- >>> search_view.bug.title
35- u'Firefox install instructions should be complete'
36- >>> search_view.question.title
37- u'Installation failed'
38+ >>> print(search_view.bug.title)
39+ Firefox install instructions should be complete
40+ >>> print(search_view.question.title)
41+ Installation failed
42
43 Bugs and questions are matched independent of each other. The number
44 extracted may only match one kind of object. For example, there are
45@@ -107,12 +107,12 @@ more bugs than questions.
46
47 >>> search_view = getSearchView(
48 ... form={'field.text': '15'})
49- >>> search_view._getNumericToken(search_view.text)
50- u'15'
51+ >>> print(search_view._getNumericToken(search_view.text))
52+ 15
53 >>> search_view.has_matches
54 True
55- >>> search_view.bug.title
56- u'Nonsensical bugs are useless'
57+ >>> print(search_view.bug.title)
58+ Nonsensical bugs are useless
59 >>> print(search_view.question)
60 None
61
62@@ -149,12 +149,12 @@ numbers do match questions, but they are not used.
63
64 >>> search_view = getSearchView(
65 ... form={'field.text': 'Question #15, #7, and 5.'})
66- >>> search_view._getNumericToken(search_view.text)
67- u'15'
68+ >>> print(search_view._getNumericToken(search_view.text))
69+ 15
70 >>> search_view.has_matches
71 True
72- >>> search_view.bug.title
73- u'Nonsensical bugs are useless'
74+ >>> print(search_view.bug.title)
75+ Nonsensical bugs are useless
76 >>> print(search_view.question)
77 None
78
79@@ -162,8 +162,8 @@ It is not an error to search for a non-existent bug or question.
80
81 >>> search_view = getSearchView(
82 ... form={'field.text': '55555'})
83- >>> search_view._getNumericToken(search_view.text)
84- u'55555'
85+ >>> print(search_view._getNumericToken(search_view.text))
86+ 55555
87 >>> search_view.has_matches
88 False
89 >>> print(search_view.bug)
90@@ -208,14 +208,14 @@ product, or project group. A person is a person or a team.
91
92 >>> search_view = getSearchView(
93 ... form={'field.text': 'launchpad'})
94- >>> search_view._getNameToken(search_view.text)
95- u'launchpad'
96+ >>> print(search_view._getNameToken(search_view.text))
97+ launchpad
98 >>> search_view.has_matches
99 True
100- >>> search_view.pillar.displayname
101- u'Launchpad'
102- >>> search_view.person_or_team.displayname
103- u'Launchpad Developers'
104+ >>> print(search_view.pillar.displayname)
105+ Launchpad
106+ >>> print(search_view.person_or_team.displayname)
107+ Launchpad Developers
108
109 A launchpad name is constructed from the search text. The letters are
110 converted to lowercase. groups of spaces and punctuation are replaced
111@@ -223,12 +223,12 @@ with a hyphen.
112
113 >>> search_view = getSearchView(
114 ... form={'field.text': 'Gnome Terminal'})
115- >>> search_view._getNameToken(search_view.text)
116- u'gnome-terminal'
117+ >>> print(search_view._getNameToken(search_view.text))
118+ gnome-terminal
119 >>> search_view.has_matches
120 True
121- >>> search_view.pillar.displayname
122- u'GNOME Terminal'
123+ >>> print(search_view.pillar.displayname)
124+ GNOME Terminal
125 >>> print(search_view.person_or_team)
126 None
127
128@@ -242,12 +242,12 @@ by any of its aliases.
129 >>> login(ANONYMOUS)
130 >>> search_view = getSearchView(
131 ... form={'field.text': 'iceweasel'})
132- >>> search_view._getNameToken(search_view.text)
133- u'iceweasel'
134+ >>> print(search_view._getNameToken(search_view.text))
135+ iceweasel
136 >>> search_view.has_matches
137 True
138- >>> search_view.pillar.displayname
139- u'Mozilla Firefox'
140+ >>> print(search_view.pillar.displayname)
141+ Mozilla Firefox
142
143 This is a harder example that illustrates that text that is clearly not
144 the name of a pillar will none-the-less be tried. See the `Page searches`
145@@ -255,8 +255,8 @@ section for how this kind of search can return matches.
146
147 >>> search_view = getSearchView(
148 ... form={'field.text': "YAHOO! webservice's Python API."})
149- >>> search_view._getNameToken(search_view.text)
150- u'yahoo-webservices-python-api.'
151+ >>> print(search_view._getNameToken(search_view.text))
152+ yahoo-webservices-python-api.
153 >>> search_view.has_matches
154 False
155 >>> print(search_view.pillar)
156@@ -268,14 +268,14 @@ Leading and trailing punctuation and whitespace are stripped.
157
158 >>> search_view = getSearchView(
159 ... form={'field.text': "~name12"})
160- >>> search_view._getNameToken(search_view.text)
161- u'name12'
162+ >>> print(search_view._getNameToken(search_view.text))
163+ name12
164 >>> search_view.has_matches
165 True
166 >>> print(search_view.pillar)
167 None
168- >>> search_view.person_or_team.displayname
169- u'Sample Person'
170+ >>> print(search_view.person_or_team.displayname)
171+ Sample Person
172
173 Pillars, persons and teams are only returned for the first page of
174 search, when the start param is 0.
175@@ -305,22 +305,22 @@ pillar, nor will nsv match Nicolas Velin's unclaimed account.
176 >>> search_view = getSearchView(
177 ... form={'field.text': 'python-gnome2-dev',
178 ... 'start': '0'})
179- >>> search_view._getNameToken(search_view.text)
180- u'python-gnome2-dev'
181+ >>> print(search_view._getNameToken(search_view.text))
182+ python-gnome2-dev
183 >>> print(search_view.pillar)
184 None
185
186 >>> nsv = getUtility(IPersonSet).getByName('nsv')
187- >>> nsv.displayname
188- u'Nicolas Velin'
189+ >>> print(nsv.displayname)
190+ Nicolas Velin
191 >>> nsv.is_valid_person_or_team
192 False
193
194 >>> search_view = getSearchView(
195 ... form={'field.text': 'nsv',
196 ... 'start': '0'})
197- >>> search_view._getNameToken(search_view.text)
198- u'nsv'
199+ >>> print(search_view._getNameToken(search_view.text))
200+ nsv
201 >>> print(search_view.person_or_team)
202 None
203
204@@ -416,8 +416,8 @@ search terms.
205
206 >>> search_view = getSearchView(
207 ... form={'field.text': " bug"})
208- >>> search_view.text
209- u'bug'
210+ >>> print(search_view.text)
211+ bug
212 >>> search_view.has_matches
213 True
214 >>> search_view.pages
215@@ -436,8 +436,10 @@ is a heading when there are only Search Service page matches...
216
217 >>> search_view.has_exact_matches
218 False
219- >>> search_view.batch_heading
220- (u'page matching "bug"', u'pages matching "bug"')
221+ >>> for heading in search_view.batch_heading:
222+ ... print(heading)
223+ page matching "bug"
224+ pages matching "bug"
225
226 ...and a heading for when there are exact matches and Search Service page
227 matches.
228@@ -446,8 +448,10 @@ matches.
229 ... form={'field.text': " launchpad"})
230 >>> search_view.has_exact_matches
231 True
232- >>> search_view.batch_heading
233- (u'other page matching "launchpad"', u'other pages matching "launchpad"')
234+ >>> for heading in search_view.batch_heading:
235+ ... print(heading)
236+ other page matching "launchpad"
237+ other pages matching "launchpad"
238
239 The SiteSearchBatchNavigator behaves like most BatchNavigators, except that
240 its batch size is always 20. The size restriction conforms to Google's
241@@ -461,12 +465,12 @@ maximum number of results that can be returned per request.
242 >>> len(pages)
243 20
244 >>> for page in pages[0:5]:
245- ... six.ensure_text(page.title)
246- u'Launchpad Bugs'
247- u'Bugs in Ubuntu Linux'
248- u'Bugs related to Sample Person'
249- u'...Bug... #1 in Mozilla Firefox: ...Firefox does not support SVG...'
250- u'Bugs in Source Package ...thunderbird... in Ubuntu Linux'
251+ ... print("'%s'" % page.title)
252+ 'Launchpad Bugs'
253+ 'Bugs in Ubuntu Linux'
254+ 'Bugs related to Sample Person'
255+ '...Bug... #1 in Mozilla Firefox: ...Firefox does not support SVG...'
256+ 'Bugs in Source Package ...thunderbird... in Ubuntu Linux'
257
258 The batch navigator provides access to the other batches. There are two
259 batches of pages that match the search text 'bugs'. The navigator
260@@ -486,8 +490,8 @@ is 20. That is because there were only 25 matching pages.
261 ... 'start': '20'})
262 >>> search_view.start
263 20
264- >>> search_view.text
265- u'bug'
266+ >>> print(search_view.text)
267+ bug
268 >>> search_view.has_matches
269 True
270
271@@ -497,12 +501,12 @@ is 20. That is because there were only 25 matching pages.
272 >>> len(pages)
273 5
274 >>> for page in pages:
275- ... six.ensure_text(page.title)
276- u'...Bug... #2 in Ubuntu Hoary: \u201cBlackhole Trash folder\u201d'
277- u'...Bug... #2 in mozilla-firefox (Debian): ...Blackhole Trash folder...'
278- u'...Bug... #3 in mozilla-firefox (Debian): \u201cBug Title Test\u201d'
279- u'...Bug... trackers registered in Launchpad'
280- u'...Bug... tracker \u201cDebian Bug tracker\u201d'
281+ ... print("'%s'" % page.title)
282+ '...Bug... #2 in Ubuntu Hoary: “Blackhole Trash folder”'
283+ '...Bug... #2 in mozilla-firefox (Debian): ...Blackhole Trash folder...'
284+ '...Bug... #3 in mozilla-firefox (Debian): “Bug Title Test”'
285+ '...Bug... trackers registered in Launchpad'
286+ '...Bug... tracker “Debian Bug tracker”'
287
288 >>> search_view.pages.nextBatchURL()
289 ''
290@@ -516,12 +520,12 @@ showing the matching terms in context of the page text.
291 >>> page = pages[0]
292 >>> page
293 <...PageMatch ...>
294- >>> page.title
295- u'...Bug... #2 in Ubuntu Hoary: \u201cBlackhole Trash folder\u201d'
296+ >>> print("'%s'" % page.title)
297+ '...Bug... #2 in Ubuntu Hoary: “Blackhole Trash folder”'
298 >>> page.url
299 'http://bugs.launchpad.test/ubuntu/hoary/+bug/2'
300- >>> page.summary
301- u'...Launchpad\u2019s ...bug... tracker allows collaboration...'
302+ >>> print("'%s'" % page.summary)
303+ '...Launchpad’s ...bug... tracker allows collaboration...'
304
305
306 No page matches
307@@ -562,8 +566,8 @@ matching pages.
308 >>> search_view = getSearchView(form={'field.text': 'gnomebaker'})
309 >>> search_view.has_matches
310 True
311- >>> search_view.pillar.displayname
312- u'gnomebaker'
313+ >>> print(search_view.pillar.displayname)
314+ gnomebaker
315 >>> search_view.has_page_service
316 False
317
318@@ -656,8 +660,8 @@ the first 20 items are None. Only the last 5 items are PageMatches.
319 25
320 >>> print(results[0])
321 None
322- >>> results[24].title
323- u'...Bug... tracker \u201cDebian Bug tracker\u201d'
324+ >>> print("'%s'" % results[24].title)
325+ '...Bug... tracker “Debian Bug tracker”'
326 >>> results[18, 22]
327 [None, None, <...PageMatch ...>, <...PageMatch ...>]
328
329diff --git a/lib/lp/app/doc/batch-navigation.txt b/lib/lp/app/doc/batch-navigation.txt
330index 1456ef0..86436d7 100644
331--- a/lib/lp/app/doc/batch-navigation.txt
332+++ b/lib/lp/app/doc/batch-navigation.txt
333@@ -103,13 +103,13 @@ upper and lower navigation link views.
334 >>> navigator = BatchNavigator([], request=request)
335 >>> upper_view = getMultiAdapter(
336 ... (navigator, request), name='+navigation-links-upper')
337- >>> upper_view.render()
338- u''
339+ >>> print(upper_view.render())
340+ <BLANKLINE>
341
342 >>> lower_view = getMultiAdapter(
343 ... (navigator, request), name='+navigation-links-lower')
344- >>> lower_view.render()
345- u''
346+ >>> print(lower_view.render())
347+ <BLANKLINE>
348
349 When there is a current batch, but there are no previous or next
350 batches, both the upper and lower navigation links view will render.
351diff --git a/lib/lp/app/doc/displaying-paragraphs-of-text.txt b/lib/lp/app/doc/displaying-paragraphs-of-text.txt
352index 1b3a638..9875fd8 100644
353--- a/lib/lp/app/doc/displaying-paragraphs-of-text.txt
354+++ b/lib/lp/app/doc/displaying-paragraphs-of-text.txt
355@@ -13,13 +13,15 @@ Basics
356 >>> text = ('This is a paragraph.\n'
357 ... '\n'
358 ... 'This is another paragraph.')
359- >>> test_tales('foo/fmt:text-to-html', foo=text)
360- u'<p>This is a paragraph.</p>\n<p>This is another paragraph.</p>'
361+ >>> print(test_tales('foo/fmt:text-to-html', foo=text))
362+ <p>This is a paragraph.</p>
363+ <p>This is another paragraph.</p>
364
365 >>> text = ('This is a line.\n'
366 ... 'This is another line.')
367- >>> test_tales('foo/fmt:text-to-html', foo=text)
368- u'<p>This is a line.<br />\nThis is another line.</p>'
369+ >>> print(test_tales('foo/fmt:text-to-html', foo=text))
370+ <p>This is a line.<br />
371+ This is another line.</p>
372
373 >>> text = (
374 ... 'This is a paragraph that has been hard-wrapped by an email'
375@@ -55,8 +57,8 @@ Basics
376 >>> text = (
377 ... 'This is a little paragraph all by itself. How cute!'
378 ... )
379- >>> test_tales('foo/fmt:text-to-html', foo=text)
380- u'<p>This is a little paragraph all by itself. How cute!</p>'
381+ >>> print(test_tales('foo/fmt:text-to-html', foo=text))
382+ <p>This is a little paragraph all by itself. How cute!</p>
383
384 >>> text = (
385 ... 'Here are two paragraphs with lots of whitespace between them.\n'
386@@ -523,8 +525,8 @@ url to demonstrate quoting in the HTML attribute.
387 ... six.ensure_str('y"y'))
388 >>> sorted(matchobj.groupdict().items())
389 [('bug', None), ('url', 'y"y')]
390- >>> FormattersAPI._linkify_substitution(matchobj)
391- u'<a rel="nofollow" href="y&quot;y">y&quot;y</a>'
392+ >>> print(FormattersAPI._linkify_substitution(matchobj))
393+ <a rel="nofollow" href="y&quot;y">y&quot;y</a>
394
395 When we have a bug reference, the 'bug' group is used as the text of the link,
396 and the 'bugnum' is used to look up the bug.
397diff --git a/lib/lp/app/doc/hierarchical-menu.txt b/lib/lp/app/doc/hierarchical-menu.txt
398index 8bfa600..d9d2af0 100644
399--- a/lib/lp/app/doc/hierarchical-menu.txt
400+++ b/lib/lp/app/doc/hierarchical-menu.txt
401@@ -288,8 +288,8 @@ considered to be on the home page if there are no breadcrumbs.
402 >>> homepage_hierarchy.items
403 []
404
405- >>> homepage_hierarchy.render().strip()
406- u''
407+ >>> print(homepage_hierarchy.render().strip())
408+ <BLANKLINE>
409
410
411 Put the monkey patched method back.
412diff --git a/lib/lp/app/doc/launchpadform.txt b/lib/lp/app/doc/launchpadform.txt
413index a5a1e83..571002f 100644
414--- a/lib/lp/app/doc/launchpadform.txt
415+++ b/lib/lp/app/doc/launchpadform.txt
416@@ -245,10 +245,12 @@ Check that form wide errors can be reported:
417 >>> view.setUpFields()
418 >>> view.setUpWidgets()
419 >>> data = {}
420- >>> view._validate(None, data)
421- [u'your password may not be the same as your name']
422- >>> view.form_wide_errors
423- [u'your password may not be the same as your name']
424+ >>> for error in view._validate(None, data):
425+ ... print(error)
426+ your password may not be the same as your name
427+ >>> for error in view.form_wide_errors:
428+ ... print(error)
429+ your password may not be the same as your name
430
431 Check that widget specific errors can be reported:
432
433@@ -259,10 +261,12 @@ Check that widget specific errors can be reported:
434 >>> view.setUpFields()
435 >>> view.setUpWidgets()
436 >>> data = {}
437- >>> view._validate(None, data)
438- [u'your password must not be &quot;password&quot;']
439- >>> view.widget_errors
440- {'password': u'your password must not be &quot;password&quot;'}
441+ >>> for error in view._validate(None, data):
442+ ... print(error)
443+ your password must not be &quot;password&quot;
444+ >>> for field, error in view.widget_errors.items():
445+ ... print("%s: %s" % (field, error))
446+ password: your password must not be &quot;password&quot;
447
448 The base template used for LaunchpadFormView classes takes care of
449 displaying these errors in the appropriate locations.
450@@ -370,8 +374,8 @@ executing the template attribute (which can be set from ZCML):
451
452 >>> context = FormTest()
453 >>> view = RenderFormTest(context, LaunchpadTestRequest(form={}))
454- >>> view()
455- u'Content that comes from a ZCML registered template.'
456+ >>> print(view())
457+ Content that comes from a ZCML registered template.
458
459 When a redirection is done (either by calling
460 self.request.response.redirect() or setting the next_url attribute), the
461@@ -383,8 +387,8 @@ rendered content is always the empty string.
462 ... form={'field.displayname': 'bob',
463 ... 'field.actions.redirect': 'Redirect'})
464 >>> view = RenderFormTest(context, request)
465- >>> view()
466- u''
467+ >>> print(view())
468+ <BLANKLINE>
469
470 As an alternative to executing the template attribute, an action handler
471 can directly return the rendered content:
472@@ -395,8 +399,8 @@ can directly return the rendered content:
473 ... form={'field.displayname': 'bob',
474 ... 'field.actions.update': 'Update'})
475 >>> view = RenderFormTest(context, request)
476- >>> view()
477- u'Display name changed to: bob.'
478+ >>> print(view())
479+ Display name changed to: bob.
480
481 This is also true of failure handlers:
482
483@@ -406,8 +410,8 @@ This is also true of failure handlers:
484 ... form={'field.displayname': '',
485 ... 'field.actions.update': 'Update'})
486 >>> view = RenderFormTest(context, request)
487- >>> view()
488- u'Some errors occured.'
489+ >>> print(view())
490+ Some errors occured.
491
492
493 == Initial Focused Widget ==
494@@ -467,7 +471,6 @@ using a custom widget.
495 First we'll create a fake pagetemplate which doesn't use Launchpad's main
496 template and thus is way simpler.
497
498- >>> from StringIO import StringIO
499 >>> from tempfile import mkstemp
500 >>> from zope.browserpage import ViewPageTemplateFile
501 >>> file, filename = mkstemp()
502@@ -624,8 +627,8 @@ object for us too:
503 >>> request.response.getStatus()
504 302
505
506- >>> context.displayname
507- u'James Henstridge'
508+ >>> print(context.displayname)
509+ James Henstridge
510
511 By default updateContextFromData() uses the view's context, but it's
512 possible to pass in a specific context to use instead:
513@@ -633,6 +636,5 @@ possible to pass in a specific context to use instead:
514 >>> custom_context = FormTest()
515 >>> view.updateContextFromData({'displayname': u'New name'}, custom_context)
516 True
517- >>> custom_context.displayname
518- u'New name'
519-
520+ >>> print(custom_context.displayname)
521+ New name
522diff --git a/lib/lp/app/doc/launchpadformharness.txt b/lib/lp/app/doc/launchpadformharness.txt
523index bcf0015..314db9f 100644
524--- a/lib/lp/app/doc/launchpadformharness.txt
525+++ b/lib/lp/app/doc/launchpadformharness.txt
526@@ -103,8 +103,8 @@ see where we were redirected to:
527 We can also see that the context object was updated by this form
528 submission:
529
530- >>> context.string
531- u'abcdef'
532+ >>> print(context.string)
533+ abcdef
534 >>> context.number
535 42
536
537diff --git a/lib/lp/app/doc/launchpadview.txt b/lib/lp/app/doc/launchpadview.txt
538index d0ffb61..35abcdb 100644
539--- a/lib/lp/app/doc/launchpadview.txt
540+++ b/lib/lp/app/doc/launchpadview.txt
541@@ -81,8 +81,8 @@ an IStructuredString implementation.
542 >>> from lp.services.webapp.escaping import structured
543 >>> view.error_message = structured(
544 ... 'A structure is just "%s".', 'smoke & mirrors')
545- >>> view.error_message.escapedtext
546- u'A structure is just "smoke &amp; mirrors".'
547+ >>> print(view.error_message.escapedtext)
548+ A structure is just "smoke &amp; mirrors".
549 >>> view.error_message = structured('Information overload.')
550- >>> view.error_message.escapedtext
551- u'Information overload.'
552+ >>> print(view.error_message.escapedtext)
553+ Information overload.
554diff --git a/lib/lp/app/doc/menus.txt b/lib/lp/app/doc/menus.txt
555index 7625701..e50330a 100644
556--- a/lib/lp/app/doc/menus.txt
557+++ b/lib/lp/app/doc/menus.txt
558@@ -89,11 +89,11 @@ object has a canonical url derived from its place in the hierarchy.
559 ... 'http://launchpad.test/joy-of-cooking/fried-spam',
560 ... traversed_objects=[cookbook, recipe])
561
562- >>> canonical_url(cookbook)
563- u'http://launchpad.test/joy-of-cooking'
564+ >>> print(canonical_url(cookbook))
565+ http://launchpad.test/joy-of-cooking
566
567- >>> canonical_url(recipe)
568- u'http://launchpad.test/joy-of-cooking/fried-spam'
569+ >>> print(canonical_url(recipe))
570+ http://launchpad.test/joy-of-cooking/fried-spam
571
572 Content objects are not suitable for presentation by themselves; they
573 require a view class to adapt them to the required format. An object may
574diff --git a/lib/lp/app/doc/tales.txt b/lib/lp/app/doc/tales.txt
575index 427b4c0..75e2629 100644
576--- a/lib/lp/app/doc/tales.txt
577+++ b/lib/lp/app/doc/tales.txt
578@@ -284,26 +284,28 @@ The string is not ellipsized if it is less than the max length.
579
580 To preserve newlines in text when displaying as HTML, use fmt:nl_to_br:
581
582- >>> test_tales('foo/fmt:nl_to_br',
583- ... foo='icicle\nbicycle\ntricycle & troika')
584- u'icicle<br />\nbicycle<br />\ntricycle &amp; troika'
585+ >>> print(test_tales('foo/fmt:nl_to_br',
586+ ... foo='icicle\nbicycle\ntricycle & troika'))
587+ icicle<br />
588+ bicycle<br />
589+ tricycle &amp; troika
590
591 To "<pre>" format a string, use fmt:nice_pre:
592
593- >>> import pprint, textwrap
594- >>> pprint.pprint(textwrap.wrap(
595- ... test_tales('foo/fmt:nice_pre', foo='hello & goodbye')
596- ... ))
597- [u'<pre class="wrap">hello &amp; goodbye</pre>']
598+ >>> import textwrap
599+ >>> for line in textwrap.wrap(
600+ ... test_tales('foo/fmt:nice_pre', foo='hello & goodbye')):
601+ ... print(line)
602+ <pre class="wrap">hello &amp; goodbye</pre>
603
604 Add manual word breaks to long words in a string:
605
606- >>> test_tales('foo/fmt:break-long-words', foo='short words')
607- u'short words'
608+ >>> print(test_tales('foo/fmt:break-long-words', foo='short words'))
609+ short words
610
611- >>> test_tales('foo/fmt:break-long-words',
612- ... foo='<http://launchpad.net/products/launchpad>')
613- u'&lt;http:/<wbr />/launchpad.<wbr />...<wbr />launchpad&gt;'
614+ >>> print(test_tales('foo/fmt:break-long-words',
615+ ... foo='<http://launchpad.net/products/launchpad>'))
616+ &lt;http:/<wbr />/launchpad.<wbr />...<wbr />launchpad&gt;
617
618 To get a int with its thousands separated by a comma, use fmt:intcomma.
619
620@@ -418,15 +420,15 @@ Person entries
621 For a person or team, fmt:link gives us a link to that person's page,
622 containing the person name and an icon.
623
624- >>> test_tales("person/fmt:link", person=mark)
625- u'<a href=".../~mark" class="sprite person">Mark Shuttleworth</a>'
626+ >>> print(test_tales("person/fmt:link", person=mark))
627+ <a href=".../~mark" class="sprite person">Mark Shuttleworth</a>
628
629- >>> test_tales("person/fmt:link", person=matsubara)
630- u'<a href=".../~matsubara" class="sprite person-inactive">Diogo ...</a>'
631+ >>> print(test_tales("person/fmt:link", person=matsubara))
632+ <a href=".../~matsubara" class="sprite person-inactive">Diogo ...</a>
633
634 >>> ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
635- >>> test_tales("person/fmt:link", person=ubuntu_team)
636- u'<a href=".../~ubuntu-team" class="sprite team">Ubuntu Team</a>'
637+ >>> print(test_tales("person/fmt:link", person=ubuntu_team))
638+ <a href=".../~ubuntu-team" class="sprite team">Ubuntu Team</a>
639
640 The link can make the URL go to a specific app.
641
642@@ -467,22 +469,22 @@ Person's displayname will be escaped; averting a XSS vulnerability.
643 >>> sample_person = getUtility(IPersonSet).getByName('name12')
644 >>> sample_person.display_name = (
645 ... "Sample Person<br/><script>alert('XSS')</script>")
646- >>> test_tales("person/fmt:link", person=sample_person)
647- u'<a href=".../~name12"...>Sample
648- Person&lt;br/&gt;&lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;</a>'
649+ >>> print(test_tales("person/fmt:link", person=sample_person))
650+ <a href=".../~name12"...>Sample
651+ Person&lt;br/&gt;&lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;</a>
652
653 The fmt:link formatter takes an additional view_name component to extend
654 the link:
655
656 >>> login(ANONYMOUS, LaunchpadTestRequest())
657- >>> test_tales("person/fmt:link/+edit", person=matsubara)
658- u'<a href=".../~matsubara/+edit"...>...'
659+ >>> print(test_tales("person/fmt:link/+edit", person=matsubara))
660+ <a href=".../~matsubara/+edit"...>...
661
662 The fmt:local-time formatter will return the local time for that person.
663 If the person has no time_zone specified, we use UTC.
664
665- >>> sample_person.time_zone
666- u'Australia/Perth'
667+ >>> print(sample_person.time_zone)
668+ Australia/Perth
669
670 >>> test_tales("person/fmt:local-time", person=sample_person)
671 '... AWST'
672@@ -490,8 +492,8 @@ If the person has no time_zone specified, we use UTC.
673 >>> from zope.security.proxy import removeSecurityProxy
674 >>> print(removeSecurityProxy(mark).location)
675 None
676- >>> mark.time_zone
677- u'UTC'
678+ >>> print(mark.time_zone)
679+ UTC
680
681 >>> test_tales("person/fmt:local-time", person=mark)
682 '... UTC'
683@@ -552,17 +554,17 @@ Bugs
684 For bugs, fmt:link takes to the bug redirect page.
685
686 >>> bug = getUtility(IBugSet).get(1)
687- >>> test_tales("bug/fmt:link", bug=bug)
688- u'<a href=".../bugs/1" class="sprite bug">Bug #1:
689- Firefox does not support SVG</a>'
690+ >>> print(test_tales("bug/fmt:link", bug=bug))
691+ <a href=".../bugs/1" class="sprite bug">Bug #1:
692+ Firefox does not support SVG</a>
693
694 For bugtasks, fmt:link shows the severity bug icon, and links to the
695 appropriate project's bug.
696
697 >>> bugtask = bug.bugtasks[0]
698- >>> test_tales("bugtask/fmt:link", bugtask=bugtask)
699- u'<a href=".../firefox/+bug/1" class="sprite bug-low"
700- title="Low - New">Bug #1: Firefox does not support SVG</a>'
701+ >>> print(test_tales("bugtask/fmt:link", bugtask=bugtask))
702+ <a href=".../firefox/+bug/1" class="sprite bug-low"
703+ title="Low - New">Bug #1: Firefox does not support SVG</a>
704
705 Bug titles may contain markup (when describing issue regarding markup).
706 Their titles are escaped so that they display correctly. This also
707@@ -571,13 +573,13 @@ title might be interpreted by the browser.
708
709 >>> login('test@canonical.com')
710 >>> bug.title = "Opps<br/><script>alert('XSS')</script>"
711- >>> test_tales("bug/fmt:link", bug=getUtility(IBugSet).get(1))
712- u'<a href=".../bugs/1" ...>Bug #1:
713- Opps&lt;br/&gt;&lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;</a>'
714+ >>> print(test_tales("bug/fmt:link", bug=getUtility(IBugSet).get(1)))
715+ <a href=".../bugs/1" ...>Bug #1:
716+ Opps&lt;br/&gt;&lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;</a>
717
718- >>> test_tales("bugtask/fmt:link", bugtask=bugtask)
719- u'<a href=".../firefox/+bug/1" ...>Bug #1:
720- Opps&lt;br/&gt;&lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;</a>'
721+ >>> print(test_tales("bugtask/fmt:link", bugtask=bugtask))
722+ <a href=".../firefox/+bug/1" ...>Bug #1:
723+ Opps&lt;br/&gt;&lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;</a>
724
725
726 Branch subscriptions
727@@ -592,16 +594,15 @@ adequate permissions, a link is not generated.
728 ... name='michael', displayname='Michael the Viking')
729 >>> subscription = factory.makeBranchSubscription(
730 ... branch=branch, person=michael)
731- >>> test_tales("subscription/fmt:link", subscription=subscription)
732- u'Subscription of Michael the Viking to
733- lp://dev/~eric/fooix/my-branch'
734+ >>> print(test_tales("subscription/fmt:link", subscription=subscription))
735+ Subscription of Michael the Viking to lp://dev/~eric/fooix/my-branch
736
737 But if we log in as the subscriber, a link is presented.
738
739 >>> ignored = login_person(subscription.person)
740- >>> test_tales("subscription/fmt:link", subscription=subscription)
741- u'<a href="http://.../+subscription/michael">Subscription
742- of Michael the Viking to lp://dev/~eric/fooix/my-branch</a>'
743+ >>> print(test_tales("subscription/fmt:link", subscription=subscription))
744+ <a href="http://.../+subscription/michael">Subscription
745+ of Michael the Viking to lp://dev/~eric/fooix/my-branch</a>
746
747 Merge proposals also have a link formatter, which displays branch
748 titles:
749@@ -616,8 +617,8 @@ Merge proposals
750 >>> target = factory.makeProductBranch(product=fooix)
751 >>> fooix.development_focus.branch = target
752 >>> proposal = source.addLandingTarget(eric, target)
753- >>> test_tales("proposal/fmt:link", proposal=proposal)
754- u'<a href="...">[Merge] lp://dev/~eric/fooix/fix into lp://dev/fooix</a>'
755+ >>> print(test_tales("proposal/fmt:link", proposal=proposal))
756+ <a href="...">[Merge] lp://dev/~eric/fooix/fix into lp://dev/fooix</a>
757
758
759 Code review comments
760@@ -639,8 +640,8 @@ Bug branches
761 >>> bug = factory.makeBug()
762 >>> bug.linkBranch(branch, branch.owner)
763 >>> [bugbranch] = bug.linked_bugbranches
764- >>> test_tales("bugbranch/fmt:link", bugbranch=bugbranch)
765- u'<a href="...+bug...">Bug #...</a>'
766+ >>> print(test_tales("bugbranch/fmt:link", bugbranch=bugbranch))
767+ <a href="...+bug...">Bug #...</a>
768
769
770 Code imports
771@@ -652,8 +653,8 @@ support the branch deletion code.
772
773 >>> login('foo.bar@canonical.com')
774 >>> code_import = factory.makeCodeImport(branch_name="trunk")
775- >>> test_tales("code_import/fmt:link", code_import=code_import)
776- u'<a href=".../trunk">Import of...</a>'
777+ >>> print(test_tales("code_import/fmt:link", code_import=code_import))
778+ <a href=".../trunk">Import of...</a>
779
780
781 Product release files
782@@ -711,8 +712,9 @@ Product series
783 ..............
784
785 >>> product_series = factory.makeProductSeries()
786- >>> test_tales("product_series/fmt:link", product_series=product_series)
787- u'... series...'
788+ >>> print("'%s'" % test_tales(
789+ ... "product_series/fmt:link", product_series=product_series))
790+ '... series...'
791
792
793 Blueprints
794@@ -723,8 +725,9 @@ Blueprints
795 >>> login('test@canonical.com')
796 >>> specification = factory.makeSpecification(
797 ... priority=SpecificationPriority.UNDEFINED)
798- >>> test_tales("specification/fmt:link", specification=specification)
799- u'<a...class="sprite blueprint-undefined">...</a>'
800+ >>> print(test_tales(
801+ ... "specification/fmt:link", specification=specification))
802+ <a...class="sprite blueprint-undefined">...</a>
803
804
805 Blueprint branches
806@@ -734,17 +737,17 @@ Blueprint branches
807 ... priority=SpecificationPriority.UNDEFINED)
808 >>> branch = factory.makeAnyBranch()
809 >>> specification_branch = specification.linkBranch(branch, branch.owner)
810- >>> test_tales("specification_branch/fmt:link",
811- ... specification_branch=specification_branch)
812- u'<a...class="sprite blueprint-undefined">...</a>'
813+ >>> print(test_tales("specification_branch/fmt:link",
814+ ... specification_branch=specification_branch))
815+ <a...class="sprite blueprint-undefined">...</a>
816
817
818 Projects
819 ........
820
821 >>> product = factory.makeProduct()
822- >>> test_tales('product/fmt:link', product=product)
823- u'<a href=... class="sprite product">...</a>'
824+ >>> print(test_tales('product/fmt:link', product=product))
825+ <a href=... class="sprite product">...</a>
826
827
828 Questions
829@@ -752,24 +755,24 @@ Questions
830
831 >>> from lp.answers.interfaces.questioncollection import IQuestionSet
832 >>> question = getUtility(IQuestionSet).get(1)
833- >>> test_tales("question/fmt:link", question=question)
834- u'<a... class="sprite question">1:...</a>'
835+ >>> print(test_tales("question/fmt:link", question=question))
836+ <a... class="sprite question">1:...</a>
837
838
839 Distributions
840 .............
841
842 >>> distribution = factory.makeDistribution()
843- >>> test_tales("distribution/fmt:link", distribution=distribution)
844- u'<a... class="sprite distribution">...</a>'
845+ >>> print(test_tales("distribution/fmt:link", distribution=distribution))
846+ <a... class="sprite distribution">...</a>
847
848
849 Distribution Series
850 ...................
851
852 >>> distroseries = factory.makeDistroArchSeries().distroseries
853- >>> test_tales("distroseries/fmt:link", distroseries=distroseries)
854- u'<a href="...">...</a>'
855+ >>> print(test_tales("distroseries/fmt:link", distroseries=distroseries))
856+ <a href="...">...</a>
857
858
859 The fmt: namespace for specially formatted object info
860@@ -787,8 +790,8 @@ Bug Trackers
861
862 The "standard" 'url' name is supported:
863
864- >>> test_tales("bugtracker/fmt:url", bugtracker=bugtracker)
865- u'http://bugs.launchpad.test/bugs/bugtrackers/email'
866+ >>> print(test_tales("bugtracker/fmt:url", bugtracker=bugtracker))
867+ http://bugs.launchpad.test/bugs/bugtrackers/email
868
869 (The url is relative if possible, and our test request claims to be from
870 launchpad.test, so the url is relative.)
871@@ -799,36 +802,36 @@ which help when hiding email addresses from users who are not logged in.
872 >>> def print_formatted_bugtrackers():
873 ... expression = "bugtracker/fmt:%s"
874 ... for format in ['link', 'external-link', 'external-title-link']:
875- ... print('%s -->\n %r' % (
876+ ... print("%s -->\n '%s'" % (
877 ... format, test_tales(expression % format,
878 ... bugtracker=bugtracker)))
879- ... print('aliases -->\n %r' % (
880- ... list(test_tales(expression % 'aliases',
881- ... bugtracker=bugtracker)),))
882+ ... print("aliases -->\n [%s]" % (', '.join(
883+ ... "'%s'" % alias for alias in test_tales(
884+ ... expression % 'aliases', bugtracker=bugtracker))))
885
886 >>> login('test@canonical.com')
887 >>> print_formatted_bugtrackers()
888 link -->
889- u'<a href=".../bugs/bugtrackers/email">an@email.address bug tracker</a>'
890+ '<a href=".../bugs/bugtrackers/email">an@email.address bug tracker</a>'
891 external-link -->
892- u'<a class="link-external"
893- href="mailto:bugs@example.com">mailto:bugs@example.com</a>'
894+ '<a class="link-external"
895+ href="mailto:bugs@example.com">mailto:bugs@example.com</a>'
896 external-title-link -->
897- u'<a class="link-external"
898- href="mailto:bugs@example.com">an@email.address bug tracker</a>'
899+ '<a class="link-external"
900+ href="mailto:bugs@example.com">an@email.address bug tracker</a>'
901 aliases -->
902- [u'http://bugs.vikingsrool.no/', u'mailto:eatme@wundrlnd.com']
903+ ['http://bugs.vikingsrool.no/', 'mailto:eatme@wundrlnd.com']
904
905 >>> login(ANONYMOUS)
906 >>> print_formatted_bugtrackers()
907 link -->
908- u'<a href="...ckers/email">&lt;email address hidden&gt; bug tracker</a>'
909+ '<a href="...ckers/email">&lt;email address hidden&gt; bug tracker</a>'
910 external-link -->
911- u'mailto:&lt;email address hidden&gt;'
912+ 'mailto:&lt;email address hidden&gt;'
913 external-title-link -->
914- u'&lt;email address hidden&gt; bug tracker'
915+ '&lt;email address hidden&gt; bug tracker'
916 aliases -->
917- [u'http://bugs.vikingsrool.no/', u'mailto:<email address hidden>']
918+ ['http://bugs.vikingsrool.no/', 'mailto:<email address hidden>']
919
920 >>> login('test@canonical.com')
921
922@@ -850,48 +853,52 @@ Bug Watches
923
924 The "standard" 'url' name is supported:
925
926- >>> test_tales("bugwatch/fmt:url", bugwatch=sf_bugwatch)
927- u'http://bugs.launchpad.test/bugs/12/+watch/...'
928+ >>> print(test_tales("bugwatch/fmt:url", bugwatch=sf_bugwatch))
929+ http://bugs.launchpad.test/bugs/12/+watch/...
930
931- >>> test_tales("bugwatch/fmt:url", bugwatch=email_bugwatch)
932- u'http://bugs.launchpad.test/bugs/12/+watch/...'
933+ >>> print(test_tales("bugwatch/fmt:url", bugwatch=email_bugwatch))
934+ http://bugs.launchpad.test/bugs/12/+watch/...
935
936 As are 'external-link' and 'external-link-short', which help when hiding
937 email addresses from users who are not logged in:
938
939 >>> login('test@canonical.com')
940
941- >>> test_tales("bugwatch/fmt:external-link", bugwatch=sf_bugwatch)
942- u'<a class="link-external"
943- href="http://sourceforge.net/support/tracker.php?aid=1234">sf #1234</a>'
944+ >>> print(test_tales("bugwatch/fmt:external-link", bugwatch=sf_bugwatch))
945+ <a class="link-external"
946+ href="http://sourceforge.net/support/tracker.php?aid=1234">sf #1234</a>
947
948- >>> test_tales("bugwatch/fmt:external-link-short", bugwatch=sf_bugwatch)
949- u'<a class="link-external"
950- href="http://sourceforge.net/support/tracker.php?aid=1234">1234</a>'
951+ >>> print(test_tales(
952+ ... "bugwatch/fmt:external-link-short", bugwatch=sf_bugwatch))
953+ <a class="link-external"
954+ href="http://sourceforge.net/support/tracker.php?aid=1234">1234</a>
955
956- >>> test_tales("bugwatch/fmt:external-link", bugwatch=email_bugwatch)
957- u'<a class="link-external" href="mailto:bugs@example.com">email</a>'
958+ >>> print(test_tales(
959+ ... "bugwatch/fmt:external-link", bugwatch=email_bugwatch))
960+ <a class="link-external" href="mailto:bugs@example.com">email</a>
961
962- >>> test_tales(
963- ... "bugwatch/fmt:external-link-short", bugwatch=email_bugwatch)
964- u'<a class="link-external" href="mailto:bugs@example.com">&mdash;</a>'
965+ >>> print(test_tales(
966+ ... "bugwatch/fmt:external-link-short", bugwatch=email_bugwatch))
967+ <a class="link-external" href="mailto:bugs@example.com">&mdash;</a>
968
969 >>> login(ANONYMOUS)
970
971- >>> test_tales("bugwatch/fmt:external-link", bugwatch=sf_bugwatch)
972- u'<a class="link-external"
973- href="http://sourceforge.net/support/tracker.php?aid=1234">sf #1234</a>'
974+ >>> print(test_tales("bugwatch/fmt:external-link", bugwatch=sf_bugwatch))
975+ <a class="link-external"
976+ href="http://sourceforge.net/support/tracker.php?aid=1234">sf #1234</a>
977
978- >>> test_tales("bugwatch/fmt:external-link-short", bugwatch=sf_bugwatch)
979- u'<a class="link-external"
980- href="http://sourceforge.net/support/tracker.php?aid=1234">1234</a>'
981+ >>> print(test_tales(
982+ ... "bugwatch/fmt:external-link-short", bugwatch=sf_bugwatch))
983+ <a class="link-external"
984+ href="http://sourceforge.net/support/tracker.php?aid=1234">1234</a>
985
986- >>> test_tales("bugwatch/fmt:external-link", bugwatch=email_bugwatch)
987- u'email'
988+ >>> print(test_tales(
989+ ... "bugwatch/fmt:external-link", bugwatch=email_bugwatch))
990+ email
991
992- >>> test_tales(
993- ... "bugwatch/fmt:external-link-short", bugwatch=email_bugwatch)
994- u'&mdash;'
995+ >>> print(test_tales(
996+ ... "bugwatch/fmt:external-link-short", bugwatch=email_bugwatch))
997+ &mdash;
998
999 >>> login('test@canonical.com')
1000
1001@@ -1065,11 +1072,11 @@ Escaping strings
1002
1003 To escape a string you should use fmt:escape.
1004
1005- >>> test_tales('foo/fmt:escape', foo='some value')
1006- u'some value'
1007+ >>> print(test_tales('foo/fmt:escape', foo='some value'))
1008+ some value
1009
1010- >>> test_tales('foo/fmt:escape', foo='some <br /> value')
1011- u'some &lt;br /&gt; value'
1012+ >>> print(test_tales('foo/fmt:escape', foo='some <br /> value'))
1013+ some &lt;br /&gt; value
1014
1015
1016 CSS ids
1017@@ -1204,25 +1211,25 @@ Launchpad and linkify them to point at the profile page for that person.
1018 The resulting HTML includes a person icon next to the linked text to
1019 emphasise the linkage.
1020
1021- >>> test_tales('foo/fmt:linkify-email',
1022- ... foo='I am the mighty foo.bar@canonical.com hear me roar.')
1023- u'...<a href="http://launchpad.test/~name16"
1024- class="sprite person">foo.bar@canonical.com</a>...'
1025+ >>> print("'%s'" % test_tales('foo/fmt:linkify-email',
1026+ ... foo='I am the mighty foo.bar@canonical.com hear me roar.'))
1027+ '...<a href="http://launchpad.test/~name16"
1028+ class="sprite person">foo.bar@canonical.com</a>...'
1029
1030 Multiple addresses may be linkified at once:
1031
1032- >>> test_tales('foo/fmt:linkify-email',
1033- ... foo='foo.bar@canonical.com and cprov@ubuntu.com')
1034- u'<a href="http://launchpad.test/~name16"
1035- class="sprite person">foo.bar@canonical.com</a>
1036- and <a href="http://launchpad.test/~cprov"
1037- class="sprite person">cprov@ubuntu.com</a>'
1038+ >>> print(test_tales('foo/fmt:linkify-email',
1039+ ... foo='foo.bar@canonical.com and cprov@ubuntu.com'))
1040+ <a href="http://launchpad.test/~name16"
1041+ class="sprite person">foo.bar@canonical.com</a>
1042+ and <a href="http://launchpad.test/~cprov"
1043+ class="sprite person">cprov@ubuntu.com</a>
1044
1045 Team addresses are linkified with a team icon:
1046
1047- >>> test_tales('foo/fmt:linkify-email', foo='support@ubuntu.com')
1048- u'<a href="http://launchpad.test/~ubuntu-team"
1049- class="sprite team">support@ubuntu.com</a>'
1050+ >>> print(test_tales('foo/fmt:linkify-email', foo='support@ubuntu.com'))
1051+ <a href="http://launchpad.test/~ubuntu-team"
1052+ class="sprite team">support@ubuntu.com</a>
1053
1054 Unknown email addresses are not altered in any way:
1055
1056@@ -1255,8 +1262,8 @@ Test the 'fmt:url' namespace for canonical urls.
1057 ... rootsite = None
1058
1059 >>> object_having_url = ObjectThatHasUrl()
1060- >>> test_tales('foo/fmt:url', foo=object_having_url)
1061- u'/bonobo/saki'
1062+ >>> print(test_tales('foo/fmt:url', foo=object_having_url))
1063+ /bonobo/saki
1064
1065 Now, we need to test that it gets the correct application URL from the
1066 request.
1067@@ -1291,8 +1298,8 @@ Make a mock-up IBrowserRequest, and use this as the interaction.
1068 Note how the URL has only a path part, because it is for the same site
1069 as the current request.
1070
1071- >>> test_tales('foo/fmt:url', foo=object_having_url)
1072- u'/bonobo/saki'
1073+ >>> print(test_tales('foo/fmt:url', foo=object_having_url))
1074+ /bonobo/saki
1075
1076
1077 The some_string/fmt:something helper
1078@@ -1563,17 +1570,17 @@ And the url format is also available.
1079 If the link is disabled, no markup is rendered.
1080
1081 >>> menu_link.enabled = False
1082- >>> test_tales('menu_link/fmt:icon', menu_link=menu_link)
1083- u''
1084+ >>> print(test_tales('menu_link/fmt:icon', menu_link=menu_link))
1085+ <BLANKLINE>
1086
1087- >>> test_tales('menu_link/fmt:link-icon', menu_link=menu_link)
1088- u''
1089+ >>> print(test_tales('menu_link/fmt:link-icon', menu_link=menu_link))
1090+ <BLANKLINE>
1091
1092- >>> test_tales('menu_link/fmt:link', menu_link=menu_link)
1093- u''
1094+ >>> print(test_tales('menu_link/fmt:link', menu_link=menu_link))
1095+ <BLANKLINE>
1096
1097- >>> test_tales('menu_link/fmt:url', menu_link=menu_link)
1098- u''
1099+ >>> print(test_tales('menu_link/fmt:url', menu_link=menu_link))
1100+ <BLANKLINE>
1101
1102
1103 CSS classes for public and private objects
1104@@ -1627,48 +1634,48 @@ Foo Bar is an administrator so they can see all.
1105
1106 >>> login('foo.bar@canonical.com')
1107 >>> myteam = getUtility(IPersonSet).getByName('myteam')
1108- >>> test_tales("team/fmt:link", team=myteam)
1109- u'<a ...class="sprite team private"...>My Team</a>'
1110+ >>> print(test_tales("team/fmt:link", team=myteam))
1111+ <a ...class="sprite team private"...>My Team</a>
1112
1113- >>> test_tales("team/fmt:displayname", team=myteam)
1114- u'My Team'
1115+ >>> print(test_tales("team/fmt:displayname", team=myteam))
1116+ My Team
1117
1118- >>> test_tales("team/fmt:unique_displayname", team=myteam)
1119- u'My Team (myteam)'
1120+ >>> print(test_tales("team/fmt:unique_displayname", team=myteam))
1121+ My Team (myteam)
1122
1123 Owner is a member of myteam so they can see all.
1124
1125 >>> login('owner@canonical.com')
1126- >>> test_tales("team/fmt:link", team=myteam)
1127- u'<a ...class="sprite team private"...>My Team</a>'
1128+ >>> print(test_tales("team/fmt:link", team=myteam))
1129+ <a ...class="sprite team private"...>My Team</a>
1130
1131- >>> test_tales("team/fmt:displayname", team=myteam)
1132- u'My Team'
1133+ >>> print(test_tales("team/fmt:displayname", team=myteam))
1134+ My Team
1135
1136- >>> test_tales("team/fmt:unique_displayname", team=myteam)
1137- u'My Team (myteam)'
1138+ >>> print(test_tales("team/fmt:unique_displayname", team=myteam))
1139+ My Team (myteam)
1140
1141 No Priv is neither a member of myteam nor an administrator, so the
1142 information about myteam is hidden.
1143
1144 >>> login('no-priv@canonical.com')
1145- >>> test_tales("team/fmt:link", team=myteam)
1146- u'<span ...class="sprite team"...>&lt;hidden&gt;</span>'
1147+ >>> print(test_tales("team/fmt:link", team=myteam))
1148+ <span ...class="sprite team"...>&lt;hidden&gt;</span>
1149
1150- >>> test_tales("team/fmt:displayname", team=myteam)
1151- u'<hidden>'
1152+ >>> print(test_tales("team/fmt:displayname", team=myteam))
1153+ <hidden>
1154
1155- >>> test_tales("team/fmt:unique_displayname", team=myteam)
1156- u'<hidden>'
1157+ >>> print(test_tales("team/fmt:unique_displayname", team=myteam))
1158+ <hidden>
1159
1160 The anonymous user is not allowed to see private team details.
1161
1162 >>> login(ANONYMOUS)
1163- >>> test_tales("team/fmt:link", team=myteam)
1164- u'<span ...class="sprite team"...>&lt;hidden&gt;</span>'
1165+ >>> print(test_tales("team/fmt:link", team=myteam))
1166+ <span ...class="sprite team"...>&lt;hidden&gt;</span>
1167
1168- >>> test_tales("team/fmt:displayname", team=myteam)
1169- u'<hidden>'
1170+ >>> print(test_tales("team/fmt:displayname", team=myteam))
1171+ <hidden>
1172
1173- >>> test_tales("team/fmt:unique_displayname", team=myteam)
1174- u'<hidden>'
1175+ >>> print(test_tales("team/fmt:unique_displayname", team=myteam))
1176+ <hidden>
1177diff --git a/lib/lp/app/doc/validation.txt b/lib/lp/app/doc/validation.txt
1178index 38cffa3..7244fe5 100644
1179--- a/lib/lp/app/doc/validation.txt
1180+++ b/lib/lp/app/doc/validation.txt
1181@@ -18,5 +18,5 @@ an IWidgetInputErrorView:
1182
1183 >>> IWidgetInputErrorView.providedBy(view)
1184 True
1185- >>> view.snippet()
1186- u'lp validation error'
1187+ >>> print(view.snippet())
1188+ lp validation error
1189diff --git a/lib/lp/app/stories/basics/xx-dbpolicy.txt b/lib/lp/app/stories/basics/xx-dbpolicy.txt
1190index b315236..1d42045 100644
1191--- a/lib/lp/app/stories/basics/xx-dbpolicy.txt
1192+++ b/lib/lp/app/stories/basics/xx-dbpolicy.txt
1193@@ -25,8 +25,8 @@ request is querying the master or slave database.
1194 >>> dbname == master.execute("SELECT current_database()").get_one()[0]
1195 True
1196 >>> slave = ISlaveStore(Person)
1197- >>> slave.execute("SELECT current_database()").get_one()[0]
1198- u'launchpad_empty'
1199+ >>> print(slave.execute("SELECT current_database()").get_one()[0])
1200+ launchpad_empty
1201
1202 We should confirm that the empty database is as empty as we hope it is.
1203
1204diff --git a/lib/lp/app/stories/basics/xx-opstats.txt b/lib/lp/app/stories/basics/xx-opstats.txt
1205index 88a567b..f502ede 100644
1206--- a/lib/lp/app/stories/basics/xx-opstats.txt
1207+++ b/lib/lp/app/stories/basics/xx-opstats.txt
1208@@ -299,5 +299,5 @@ But our database connections are broken.
1209 >>> dummy = config.pop('no_db')
1210 >>> getUtility(IZStorm)._reset()
1211
1212- >>> IStore(Person).find(Person, name='janitor').one().name
1213- u'janitor'
1214+ >>> print(IStore(Person).find(Person, name='janitor').one().name)
1215+ janitor
1216diff --git a/lib/lp/app/widgets/date.py b/lib/lp/app/widgets/date.py
1217index bbeca15..0c317cb 100644
1218--- a/lib/lp/app/widgets/date.py
1219+++ b/lib/lp/app/widgets/date.py
1220@@ -408,8 +408,8 @@ class DateTimeWidget(TextWidget):
1221
1222 The 'missing' value is converted to an empty string:
1223
1224- >>> widget._toFormValue(field.missing_value)
1225- u''
1226+ >>> print(widget._toFormValue(field.missing_value))
1227+ <BLANKLINE>
1228
1229 DateTimes are displayed without the corresponding time zone
1230 information:
1231@@ -562,8 +562,8 @@ class DateWidget(DateTimeWidget):
1232
1233 The 'missing' value is converted to an empty string:
1234
1235- >>> widget._toFormValue(field.missing_value)
1236- u''
1237+ >>> print(widget._toFormValue(field.missing_value))
1238+ <BLANKLINE>
1239
1240 The widget ignores time and time zone information, returning only
1241 the date:
1242diff --git a/lib/lp/app/widgets/doc/image-widget.txt b/lib/lp/app/widgets/doc/image-widget.txt
1243index 164576c..23d4c86 100644
1244--- a/lib/lp/app/widgets/doc/image-widget.txt
1245+++ b/lib/lp/app/widgets/doc/image-widget.txt
1246@@ -137,8 +137,8 @@ And now we change it to a random image.
1247 >>> widget = ImageChangeWidget(
1248 ... person_logo, LaunchpadTestRequest(form=form), edit_style)
1249 >>> fileupload = widget.getInputValue()
1250- >>> fileupload.filename
1251- u'logo.png'
1252+ >>> print(fileupload.filename)
1253+ logo.png
1254
1255 >>> fileupload.content.filesize == len(logo.getvalue())
1256 True
1257@@ -299,8 +299,8 @@ Image is the correct dimensions:
1258 >>> widget = ImageChangeWidget(
1259 ... person_mugshot, LaunchpadTestRequest(form=form), edit_style)
1260 >>> fileupload = widget.getInputValue()
1261- >>> fileupload.filename
1262- u'mugshot.png'
1263+ >>> print(fileupload.filename)
1264+ mugshot.png
1265
1266 >>> fileupload.content.filesize == len(mugshot.getvalue())
1267 True
1268@@ -390,8 +390,8 @@ If the image is smaller than the dimensions, the input validates:
1269 >>> widget = ImageChangeWidget(
1270 ... person_mugshot, LaunchpadTestRequest(form=form), edit_style)
1271 >>> fileupload = widget.getInputValue()
1272- >>> fileupload.filename
1273- u'mugshot.png'
1274+ >>> print(fileupload.filename)
1275+ mugshot.png
1276
1277 The same occurs if the image matches the specified dimensions:
1278
1279@@ -400,7 +400,5 @@ The same occurs if the image matches the specified dimensions:
1280 >>> widget = ImageChangeWidget(
1281 ... person_mugshot, LaunchpadTestRequest(form=form), edit_style)
1282 >>> fileupload = widget.getInputValue()
1283- >>> fileupload.filename
1284- u'mugshot.png'
1285-
1286-
1287+ >>> print(fileupload.filename)
1288+ mugshot.png
1289diff --git a/lib/lp/app/widgets/doc/lower-case-text-widget.txt b/lib/lp/app/widgets/doc/lower-case-text-widget.txt
1290index 36c4e29..eb84fe1 100644
1291--- a/lib/lp/app/widgets/doc/lower-case-text-widget.txt
1292+++ b/lib/lp/app/widgets/doc/lower-case-text-widget.txt
1293@@ -13,16 +13,16 @@ lower case:
1294 >>> field = IBug['description']
1295 >>> request = LaunchpadTestRequest(form={'field.description':'Foo'})
1296 >>> widget = LowerCaseTextWidget(field, request)
1297- >>> widget.getInputValue()
1298- u'foo'
1299+ >>> print(widget.getInputValue())
1300+ foo
1301
1302 However, strings without lower case characters are left unchanged:
1303
1304 >>> field = IBug['description']
1305 >>> request = LaunchpadTestRequest(form={'field.description':'foo1'})
1306 >>> widget = LowerCaseTextWidget(field, request)
1307- >>> widget.getInputValue()
1308- u'foo1'
1309+ >>> print(widget.getInputValue())
1310+ foo1
1311
1312 In addition, the widget also renders itself with a CSS style that causes
1313 characters to be rendered in lower case as they are typed in by the
1314@@ -33,4 +33,4 @@ user:
1315
1316 This style is defined by "lib/canonical/launchpad/icing/style.css". Note
1317 that the style only causes text to be rendered in lower case, and does
1318-not convert the underlying string to lower case.
1319\ No newline at end of file
1320+not convert the underlying string to lower case.
1321diff --git a/lib/lp/app/widgets/doc/noneable-text-widgets.txt b/lib/lp/app/widgets/doc/noneable-text-widgets.txt
1322index 362edb3..911a9e3 100644
1323--- a/lib/lp/app/widgets/doc/noneable-text-widgets.txt
1324+++ b/lib/lp/app/widgets/doc/noneable-text-widgets.txt
1325@@ -62,6 +62,5 @@ Excess whitespace is stripped, but newlines are preserved.
1326 >>> request = LaunchpadTestRequest(
1327 ... form={'field.summary' : ' flower \n grass '})
1328 >>> widget = NoneableDescriptionWidget(field, request)
1329- >>> widget.getInputValue()
1330- u'flower \n grass'
1331-
1332+ >>> six.ensure_str(widget.getInputValue())
1333+ 'flower \n grass'
1334diff --git a/lib/lp/app/widgets/doc/project-scope-widget.txt b/lib/lp/app/widgets/doc/project-scope-widget.txt
1335index a09b2a0..35c3ed0 100644
1336--- a/lib/lp/app/widgets/doc/project-scope-widget.txt
1337+++ b/lib/lp/app/widgets/doc/project-scope-widget.txt
1338@@ -104,8 +104,8 @@ by getInputValue().
1339 >>> selected_scope = widget.getInputValue()
1340 >>> IProjectGroup.providedBy(selected_scope)
1341 True
1342- >>> selected_scope.name
1343- u'mozilla'
1344+ >>> print(selected_scope.name)
1345+ mozilla
1346
1347 If an non-existant distribution name is provided, a widget error is
1348 raised:
1349diff --git a/lib/lp/app/widgets/doc/stripped-text-widget.txt b/lib/lp/app/widgets/doc/stripped-text-widget.txt
1350index 06e0a1d..9d363c2 100644
1351--- a/lib/lp/app/widgets/doc/stripped-text-widget.txt
1352+++ b/lib/lp/app/widgets/doc/stripped-text-widget.txt
1353@@ -43,8 +43,8 @@ We pass a string with leading and trailing whitespaces to the widget
1354
1355 And check that the leading and trailing whitespaces were correctly stripped.
1356
1357- >>> widget.getInputValue()
1358- u'123456'
1359+ >>> print(widget.getInputValue())
1360+ 123456
1361
1362 If only whitespace is provided, the widget acts like no input was
1363 provided.
1364diff --git a/lib/lp/app/widgets/doc/tokens-text-widget.txt b/lib/lp/app/widgets/doc/tokens-text-widget.txt
1365index bc38372..e51ff15 100644
1366--- a/lib/lp/app/widgets/doc/tokens-text-widget.txt
1367+++ b/lib/lp/app/widgets/doc/tokens-text-widget.txt
1368@@ -19,7 +19,7 @@ satisfied.
1369
1370 The widget removed the extra whitespace and punctuation.
1371
1372- >>> widget.getInputValue()
1373- u'news feeds HTTP RSS UTF-8'
1374+ >>> print(widget.getInputValue())
1375+ news feeds HTTP RSS UTF-8
1376
1377
1378diff --git a/lib/lp/app/widgets/textwidgets.py b/lib/lp/app/widgets/textwidgets.py
1379index 23b3f5f..4532762 100644
1380--- a/lib/lp/app/widgets/textwidgets.py
1381+++ b/lib/lp/app/widgets/textwidgets.py
1382@@ -123,8 +123,8 @@ class LocalDateTimeWidget(TextWidget):
1383
1384 The 'missing' value is converted to an empty string:
1385
1386- >>> widget._toFormValue(field.missing_value)
1387- u''
1388+ >>> print(widget._toFormValue(field.missing_value))
1389+ <BLANKLINE>
1390
1391 Dates are displayed without an associated time zone:
1392
1393@@ -218,14 +218,14 @@ class DelimitedListWidget(TextAreaWidget):
1394
1395 The 'missing' value is converted to an empty string:
1396
1397- >>> widget._toFormValue(field.missing_value)
1398- u''
1399+ >>> print(widget._toFormValue(field.missing_value))
1400+ <BLANKLINE>
1401
1402 By default, lists are displayed one item on a line:
1403
1404 >>> names = ['fred', 'bob', 'harry']
1405- >>> widget._toFormValue(names)
1406- u'fred\\r\\nbob\\r\\nharry'
1407+ >>> six.ensure_str(widget._toFormValue(names))
1408+ 'fred\\r\\nbob\\r\\nharry'
1409 """
1410 if value == self.context.missing_value:
1411 value = self._missing
1412@@ -250,8 +250,11 @@ class DelimitedListWidget(TextAreaWidget):
1413
1414 By default, lists are split by whitespace:
1415
1416- >>> print(widget._toFieldValue(u'fred\\nbob harry'))
1417- [u'fred', u'bob', u'harry']
1418+ >>> for item in widget._toFieldValue(u'fred\\nbob harry'):
1419+ ... print("'%s'" % item)
1420+ 'fred'
1421+ 'bob'
1422+ 'harry'
1423 """
1424 value = super(
1425 DelimitedListWidget, self)._toFieldValue(value)

Subscribers

People subscribed via source and target branches

to status/vote changes: