Merge ~cjwatson/launchpad:bs4-feeds into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 3535948a45d3a6eb486436b61d7d4ff6459175e5
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:bs4-feeds
Merge into: launchpad:master
Diff against target: 1148 lines (+247/-223)
10 files modified
lib/lp/bugs/stories/feeds/xx-bug-atom.txt (+51/-42)
lib/lp/bugs/stories/feeds/xx-bug-html.txt (+5/-4)
lib/lp/code/stories/feeds/xx-branch-atom.txt (+29/-22)
lib/lp/code/stories/feeds/xx-revision-atom.txt (+9/-9)
lib/lp/registry/stories/announcements/xx-announcements.txt (+13/-15)
lib/lp/services/feeds/doc/feeds.txt (+1/-1)
lib/lp/services/feeds/feed.py (+2/-4)
lib/lp/services/feeds/stories/xx-links.txt (+109/-98)
lib/lp/services/feeds/stories/xx-security.txt (+19/-19)
lib/lp/services/feeds/tests/helper.py (+9/-9)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+377977@code.launchpad.net

Commit message

Port feed tests to Beautiful Soup 4

To post a comment you must log in.
~cjwatson/launchpad:bs4-feeds updated
3535948... by Colin Watson

Port xx-announcements.txt too

It uses lp.services.feeds.tests.helper, so needs to be ported along with
the other feed tests.

By default, Beautiful Soup 4 produces output with HTML entities
converted to Unicode characters. I could have used formatter="html" or
similar to restore something closer to the old behaviour in this case,
but the new behaviour is easier to read.

Revision history for this message
Colin Watson (cjwatson) wrote :

Self-approving: this is almost entirely tests, and is mechanical enough that it's not very interesting to review.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/bugs/stories/feeds/xx-bug-atom.txt b/lib/lp/bugs/stories/feeds/xx-bug-atom.txt
2index 4b0faa4..840b943 100644
3--- a/lib/lp/bugs/stories/feeds/xx-bug-atom.txt
4+++ b/lib/lp/bugs/stories/feeds/xx-bug-atom.txt
5@@ -1,10 +1,12 @@
6 = Atom Feeds =
7
8 Atom feeds produce XML not HTML. Therefore we must parse the output as XML
9-using BeautifulStoneSoup instead of BSS or the helper functions.
10+by asking BeautifulSoup to use lxml.
11
12- >>> from BeautifulSoup import BeautifulStoneSoup as BSS
13- >>> from BeautifulSoup import SoupStrainer
14+ >>> from lp.services.beautifulsoup import (
15+ ... BeautifulSoup4 as BeautifulSoup,
16+ ... SoupStrainer4 as SoupStrainer,
17+ ... )
18 >>> from lp.services.feeds.tests.helper import (
19 ... parse_entries, parse_links, validate_feed)
20
21@@ -26,25 +28,26 @@ point to the bugs themselves.
22 >>> validate_feed(browser.contents,
23 ... browser.headers['content-type'], browser.url)
24 No Errors
25- >>> BSS(browser.contents).title.contents
26+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
27 [u'Bugs in Jokosher']
28 >>> browser.url
29 'http://feeds.launchpad.test/jokosher/latest-bugs.atom'
30
31- >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
32+ >>> soup = BeautifulSoup(
33+ ... browser.contents, 'xml', parse_only=SoupStrainer('id'))
34 >>> print(extract_text(soup.find('id')))
35 tag:launchpad.net,2007-03-15:/bugs/jokosher
36 >>> alternate_links = parse_links(browser.contents, 'alternate')
37 >>> for link in alternate_links:
38 ... print(link)
39- <link rel="alternate" href="http://bugs.launchpad.test/jokosher" />
40- <link rel="alternate" href="http://bugs.launchpad.test/bugs/12" />
41- <link rel="alternate" href="http://bugs.launchpad.test/bugs/11" />
42+ <link href="http://bugs.launchpad.test/jokosher" rel="alternate"/>
43+ <link href="http://bugs.launchpad.test/bugs/12" rel="alternate"/>
44+ <link href="http://bugs.launchpad.test/bugs/11" rel="alternate"/>
45
46 >>> self_links = parse_links(browser.contents, 'self')
47 >>> for link in self_links:
48 ... print(link)
49- <link rel="self" href="http://feeds.launchpad.test/jokosher/latest-bugs.atom" />
50+ <link href="http://feeds.launchpad.test/jokosher/latest-bugs.atom" rel="self"/>
51
52 >>> entries = parse_entries(browser.contents)
53 >>> print(len(entries))
54@@ -83,19 +86,20 @@ as the latest bugs feed for a product.
55 >>> validate_feed(browser.contents,
56 ... browser.headers['content-type'], browser.url)
57 No Errors
58- >>> BSS(browser.contents).title.contents
59+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
60 [u'Bugs in The Mozilla Project']
61 >>> browser.url
62 'http://feeds.launchpad.test/mozilla/latest-bugs.atom'
63
64- >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
65+ >>> soup = BeautifulSoup(
66+ ... browser.contents, 'xml', parse_only=SoupStrainer('id'))
67 >>> print(extract_text(soup.find('id')))
68 tag:launchpad.net,2004-09-24:/bugs/mozilla
69
70 >>> self_links = parse_links(browser.contents, 'self')
71 >>> for link in self_links:
72 ... print(link)
73- <link rel="self" href="http://feeds.launchpad.test/mozilla/latest-bugs.atom" />
74+ <link href="http://feeds.launchpad.test/mozilla/latest-bugs.atom" rel="self"/>
75
76 >>> entries = parse_entries(browser.contents)
77 >>> print(len(entries))
78@@ -144,19 +148,20 @@ of content as the latest bugs feed for a product.
79 >>> validate_feed(browser.contents,
80 ... browser.headers['content-type'], browser.url)
81 No Errors
82- >>> BSS(browser.contents).title.contents
83+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
84 [u'Bugs in Ubuntu']
85 >>> browser.url
86 'http://feeds.launchpad.test/ubuntu/latest-bugs.atom'
87
88- >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
89+ >>> soup = BeautifulSoup(
90+ ... browser.contents, 'xml', parse_only=SoupStrainer('id'))
91 >>> print(extract_text(soup.find('id')))
92 tag:launchpad.net,2006-10-16:/bugs/ubuntu
93
94 >>> self_links = parse_links(browser.contents, 'self')
95 >>> for link in self_links:
96 ... print(link)
97- <link rel="self" href="http://feeds.launchpad.test/ubuntu/latest-bugs.atom" />
98+ <link href="http://feeds.launchpad.test/ubuntu/latest-bugs.atom" rel="self"/>
99
100 >>> entries = parse_entries(browser.contents)
101 >>> print(len(entries))
102@@ -214,11 +219,8 @@ The bug should be included in the feed.
103
104 Private teams should show as '-'.
105
106- >>> entry_content = BSS(
107- ... entry.find('content').text,
108- ... convertEntities=BSS.HTML_ENTITIES)
109- >>> soup = BSS(entry_content.text)
110- >>> print([tr.findAll('td')[4].text for tr in soup.findAll('tr')[1:4]])
111+ >>> soup = BeautifulSoup(entry.find('content').text, 'xml')
112+ >>> print([tr.find_all('td')[4].text for tr in soup.find_all('tr')[1:4]])
113 [u'Mark Shuttleworth', u'-', u'-']
114
115 == Latest bugs for a source package ==
116@@ -232,11 +234,12 @@ type of content as the latest bugs feed for a product.
117 >>> validate_feed(browser.contents,
118 ... browser.headers['content-type'], browser.url)
119 No Errors
120- >>> BSS(browser.contents).title.contents
121+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
122 [u'Bugs in thunderbird in Ubuntu']
123 >>> browser.url
124 'http://feeds.launchpad.test/ubuntu/+source/thunderbird/latest-bugs.atom'
125- >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
126+ >>> soup = BeautifulSoup(
127+ ... browser.contents, 'xml', parse_only=SoupStrainer('id'))
128 >>> print(extract_text(soup.find('id')))
129 tag:launchpad.net,2008:/bugs/ubuntu/+source/thunderbird
130 >>> entries = parse_entries(browser.contents)
131@@ -264,19 +267,20 @@ type of content as the latest bugs feed for a product.
132 >>> validate_feed(browser.contents,
133 ... browser.headers['content-type'], browser.url)
134 No Errors
135- >>> BSS(browser.contents).title.contents
136+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
137 [u'Bugs in Hoary']
138 >>> browser.url
139 'http://feeds.launchpad.test/ubuntu/hoary/latest-bugs.atom'
140
141- >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
142+ >>> soup = BeautifulSoup(
143+ ... browser.contents, 'xml', parse_only=SoupStrainer('id'))
144 >>> print(extract_text(soup.find('id')))
145 tag:launchpad.net,2006-10-16:/bugs/ubuntu/hoary
146
147 >>> self_links = parse_links(browser.contents, 'self')
148 >>> for link in self_links:
149 ... print(link)
150- <link rel="self" href="http://feeds.launchpad.test/ubuntu/hoary/latest-bugs.atom" />
151+ <link href="http://feeds.launchpad.test/ubuntu/hoary/latest-bugs.atom" rel="self"/>
152
153 >>> entries = parse_entries(browser.contents)
154 >>> print(len(entries))
155@@ -304,19 +308,20 @@ type of content as the latest bugs feed for a product.
156 >>> validate_feed(browser.contents,
157 ... browser.headers['content-type'], browser.url)
158 No Errors
159- >>> BSS(browser.contents).title.contents
160+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
161 [u'Bugs in 1.0']
162 >>> browser.url
163 'http://feeds.launchpad.test/firefox/1.0/latest-bugs.atom'
164
165- >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
166+ >>> soup = BeautifulSoup(
167+ ... browser.contents, 'xml', parse_only=SoupStrainer('id'))
168 >>> print(extract_text(soup.find('id')))
169 tag:launchpad.net,2005-06-06:/bugs/firefox/1.0
170
171 >>> self_links = parse_links(browser.contents, 'self')
172 >>> for link in self_links:
173 ... print(link)
174- <link rel="self" href="http://feeds.launchpad.test/firefox/1.0/latest-bugs.atom" />
175+ <link href="http://feeds.launchpad.test/firefox/1.0/latest-bugs.atom" rel="self"/>
176
177 >>> entries = parse_entries(browser.contents)
178 >>> print(len(entries))
179@@ -342,19 +347,20 @@ This feed gets the latest bugs for a person.
180 >>> validate_feed(browser.contents,
181 ... browser.headers['content-type'], browser.url)
182 No Errors
183- >>> BSS(browser.contents).title.contents
184+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
185 [u'Bugs for Foo Bar']
186 >>> browser.url
187 'http://feeds.launchpad.test/~name16/latest-bugs.atom'
188
189- >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
190+ >>> soup = BeautifulSoup(
191+ ... browser.contents, 'xml', parse_only=SoupStrainer('id'))
192 >>> print(extract_text(soup.find('id')))
193 tag:launchpad.net,2005-06-06:/bugs/~name16
194
195 >>> self_links = parse_links(browser.contents, 'self')
196 >>> for link in self_links:
197 ... print(link)
198- <link rel="self" href="http://feeds.launchpad.test/~name16/latest-bugs.atom" />
199+ <link href="http://feeds.launchpad.test/~name16/latest-bugs.atom" rel="self"/>
200
201 >>> entries = parse_entries(browser.contents)
202 >>> print(len(entries))
203@@ -417,17 +423,18 @@ some results.
204 >>> validate_feed(browser.contents,
205 ... browser.headers['content-type'], browser.url)
206 No Errors
207- >>> BSS(browser.contents).title.contents
208+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
209 [u'Bugs for Simple Team']
210
211- >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
212+ >>> soup = BeautifulSoup(
213+ ... browser.contents, 'xml', parse_only=SoupStrainer('id'))
214 >>> print(extract_text(soup.find('id')))
215 tag:launchpad.net,2007-02-21:/bugs/~simple-team
216
217 >>> self_links = parse_links(browser.contents, 'self')
218 >>> for link in self_links:
219 ... print(link)
220- <link rel="self" href="http://feeds.launchpad.test/~simple-team/latest-bugs.atom" />
221+ <link href="http://feeds.launchpad.test/~simple-team/latest-bugs.atom" rel="self"/>
222
223 >>> entries = parse_entries(browser.contents)
224 >>> print(len(entries))
225@@ -445,19 +452,20 @@ This feed gets the latest bugs reported against any target.
226 >>> validate_feed(browser.contents,
227 ... browser.headers['content-type'], browser.url)
228 No Errors
229- >>> BSS(browser.contents).title.contents
230+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
231 [u'Launchpad bugs']
232 >>> browser.url
233 'http://feeds.launchpad.test/bugs/latest-bugs.atom'
234
235- >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
236+ >>> soup = BeautifulSoup(
237+ ... browser.contents, 'xml', parse_only=SoupStrainer('id'))
238 >>> print(extract_text(soup.find('id')))
239 tag:launchpad.net,2008:/bugs
240
241 >>> self_links = parse_links(browser.contents, 'self')
242 >>> for link in self_links:
243 ... print(link)
244- <link rel="self" href="http://feeds.launchpad.test/bugs/latest-bugs.atom" />
245+ <link href="http://feeds.launchpad.test/bugs/latest-bugs.atom" rel="self"/>
246
247 >>> entries = parse_entries(browser.contents)
248 >>> print(len(entries))
249@@ -508,10 +516,11 @@ The bug search feed can be tested after setting is_bug_search_feed_active
250 to True.
251
252 >>> browser.open(url)
253- >>> BSS(browser.contents).title.contents
254+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
255 [u'Bugs from custom search']
256
257- >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
258+ >>> soup = BeautifulSoup(
259+ ... browser.contents, 'xml', parse_only=SoupStrainer('id'))
260 >>> feed_id = extract_text(soup.find('id'))
261 >>> print(feed_id)
262 tag:launchpad.net,2008:/+bugs.atom?field.scope.target=&amp;field.scope=all&amp;field.searchtext=&amp;search=Search+Bug+Reports
263@@ -523,7 +532,7 @@ to True.
264 >>> self_links = parse_links(browser.contents, 'self')
265 >>> for link in self_links:
266 ... print(link)
267- <link rel="self" href="http://feeds.launchpad.test/bugs/+bugs.atom?field.scope.target=&amp;field.scope=all&amp;field.searchtext=&amp;search=Search+Bug+Reports" />
268+ <link href="http://feeds.launchpad.test/bugs/+bugs.atom?field.scope.target=&amp;field.scope=all&amp;field.searchtext=&amp;search=Search+Bug+Reports" rel="self"/>
269
270 >>> entries = parse_entries(browser.contents)
271 >>> print(len(entries))
272@@ -554,7 +563,7 @@ This feed shows the status of a single bug.
273 >>> validate_feed(browser.contents,
274 ... browser.headers['content-type'], browser.url)
275 No Errors
276- >>> BSS(browser.contents).title.contents
277+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
278 [u'Bug 1']
279 >>> entries = parse_entries(browser.contents)
280 >>> print(len(entries))
281@@ -565,7 +574,7 @@ This feed shows the status of a single bug.
282 >>> self_links = parse_links(browser.contents, 'self')
283 >>> for link in self_links:
284 ... print(link)
285- <link rel="self" href="http://feeds.launchpad.test/bugs/1/bug.atom" />
286+ <link href="http://feeds.launchpad.test/bugs/1/bug.atom" rel="self"/>
287
288 == Feeds Configuration Options ==
289
290diff --git a/lib/lp/bugs/stories/feeds/xx-bug-html.txt b/lib/lp/bugs/stories/feeds/xx-bug-html.txt
291index fff1f4f..fa02653 100644
292--- a/lib/lp/bugs/stories/feeds/xx-bug-html.txt
293+++ b/lib/lp/bugs/stories/feeds/xx-bug-html.txt
294@@ -5,15 +5,16 @@ The content of an HTML feed is very similar to an Atom feed, but is formatted
295 as HTML instead of Atom.
296
297 >>> from lp.services.beautifulsoup import (
298- ... BeautifulSoup,
299- ... SoupStrainer,
300+ ... BeautifulSoup4 as BeautifulSoup,
301+ ... SoupStrainer4 as SoupStrainer,
302 ... )
303
304 Define a helper function for parsing the entries:
305
306 >>> def parse_entries(contents):
307- ... entries = [tag for tag in BeautifulSoup(browser.contents,
308- ... parseOnlyThese=SoupStrainer('tr'))]
309+ ... entries = [
310+ ... tag for tag in BeautifulSoup(
311+ ... browser.contents, parse_only=SoupStrainer('tr'))]
312 ... return entries
313
314 And two for printing the results:
315diff --git a/lib/lp/code/stories/feeds/xx-branch-atom.txt b/lib/lp/code/stories/feeds/xx-branch-atom.txt
316index db90f8f..74db8e1 100644
317--- a/lib/lp/code/stories/feeds/xx-branch-atom.txt
318+++ b/lib/lp/code/stories/feeds/xx-branch-atom.txt
319@@ -1,10 +1,12 @@
320 = Atom Feeds For Branches =
321
322 Atom feeds produce XML not HTML. Therefore we must parse the output as XML
323-using BeautifulStoneSoup instead of BeautifulSoup or the helper functions.
324+by asking BeautifulSoup to use lxml.
325
326- >>> from BeautifulSoup import BeautifulStoneSoup as BSS
327- >>> from BeautifulSoup import SoupStrainer
328+ >>> from lp.services.beautifulsoup import (
329+ ... BeautifulSoup4 as BeautifulSoup,
330+ ... SoupStrainer4 as SoupStrainer,
331+ ... )
332 >>> from lp.services.feeds.tests.helper import (
333 ... parse_ids, parse_links, validate_feed)
334
335@@ -49,7 +51,7 @@ which will include an entry for each branch.
336 ... browser.contents, browser.headers['content-type'], browser.url)
337 >>> validate_browser_feed(anon_browser)
338 No Errors
339- >>> BSS(anon_browser.contents).title.contents
340+ >>> BeautifulSoup(anon_browser.contents, 'xml').title.contents
341 [u'Branches for Mike Murphy']
342 >>> def print_parse_ids(browser):
343 ... for id in parse_ids(browser.contents):
344@@ -71,14 +73,15 @@ Ensure the self link is correct and there is only one.
345 ... for link in parse_links(browser.contents, rel="self"):
346 ... print(link)
347 >>> print_parse_links(anon_browser)
348- <link rel="self" href="http://feeds.launchpad.test/~mike/branches.atom" />
349+ <link href="http://feeds.launchpad.test/~mike/branches.atom" rel="self"/>
350
351 The <update> field for the feed will be the most recent value for the
352 updated field in all of the entries.
353
354 >>> strainer = SoupStrainer('updated')
355- >>> updated_dates = [extract_text(tag) for tag in BSS(anon_browser.contents,
356- ... parseOnlyThese=strainer)]
357+ >>> updated_dates = [
358+ ... extract_text(tag) for tag in BeautifulSoup(
359+ ... anon_browser.contents, 'xml', parse_only=strainer)]
360 >>> feed_updated = updated_dates[0]
361 >>> entry_dates = sorted(updated_dates[1:], reverse=True)
362 >>> assert feed_updated == entry_dates[0], (
363@@ -90,7 +93,7 @@ still be hidden:
364 >>> anon_browser.open('http://feeds.launchpad.test/~name12/branches.atom')
365 >>> validate_browser_feed(anon_browser)
366 No Errors
367- >>> BSS(anon_browser.contents).title.contents
368+ >>> BeautifulSoup(anon_browser.contents, 'xml').title.contents
369 [u'Branches for Sample Person']
370 >>> 'foo@localhost' in anon_browser.contents
371 False
372@@ -125,7 +128,7 @@ branches listed, just an id for the feed.
373 >>> browser.open('http://feeds.launchpad.test/~landscape-developers/branches.atom')
374 >>> validate_browser_feed(browser)
375 No Errors
376- >>> BSS(browser.contents).title.contents
377+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
378 [u'Branches for Landscape Developers']
379 >>> print_parse_ids(browser)
380 <id>tag:launchpad.net,2006-07-11:/code/~landscape-developers</id>
381@@ -139,7 +142,7 @@ which will include an entry for each branch.
382 >>> anon_browser.open('http://feeds.launchpad.test/fooix/branches.atom')
383 >>> validate_browser_feed(anon_browser)
384 No Errors
385- >>> BSS(anon_browser.contents).title.contents
386+ >>> BeautifulSoup(anon_browser.contents, 'xml').title.contents
387 [u'Branches for Fooix']
388 >>> print_parse_ids(anon_browser)
389 <id>tag:launchpad.net,...:/code/fooix</id>
390@@ -148,14 +151,15 @@ which will include an entry for each branch.
391 <id>tag:launchpad.net,2007-12-01:/code/~mike/fooix/first</id>
392
393 >>> print_parse_links(anon_browser)
394- <link rel="self" href="http://feeds.launchpad.test/fooix/branches.atom" />
395+ <link href="http://feeds.launchpad.test/fooix/branches.atom" rel="self"/>
396
397 The <update> field for the feed will be the most recent value for the
398 updated field in all of the entries.
399
400 >>> strainer = SoupStrainer('updated')
401- >>> updated_dates = [extract_text(tag) for tag in BSS(anon_browser.contents,
402- ... parseOnlyThese=strainer)]
403+ >>> updated_dates = [
404+ ... extract_text(tag) for tag in BeautifulSoup(
405+ ... anon_browser.contents, 'xml', parse_only=strainer)]
406 >>> feed_updated = updated_dates[0]
407 >>> entry_dates = sorted(updated_dates[1:], reverse=True)
408 >>> assert feed_updated == entry_dates[0], (
409@@ -170,7 +174,7 @@ branches which will include an entry for each branch.
410 >>> anon_browser.open('http://feeds.launchpad.test/oh-man/branches.atom')
411 >>> validate_browser_feed(anon_browser)
412 No Errors
413- >>> BSS(anon_browser.contents).title.contents
414+ >>> BeautifulSoup(anon_browser.contents, 'xml').title.contents
415 [u'Branches for Oh Man']
416 >>> print_parse_ids(anon_browser)
417 <id>tag:launchpad.net,...:/code/oh-man</id>
418@@ -182,14 +186,15 @@ branches which will include an entry for each branch.
419 <id>tag:launchpad.net,2007-12-01:/code/~mike/fooix/first</id>
420
421 >>> print_parse_links(anon_browser)
422- <link rel="self" href="http://feeds.launchpad.test/oh-man/branches.atom" />
423+ <link href="http://feeds.launchpad.test/oh-man/branches.atom" rel="self"/>
424
425 The <update> field for the feed will be the most recent value for the
426 updated field in all of the entries.
427
428 >>> strainer = SoupStrainer('updated')
429- >>> updated_dates = [extract_text(tag) for tag in BSS(anon_browser.contents,
430- ... parseOnlyThese=strainer)]
431+ >>> updated_dates = [
432+ ... extract_text(tag) for tag in BeautifulSoup(
433+ ... anon_browser.contents, 'xml', parse_only=strainer)]
434 >>> feed_updated = updated_dates[0]
435 >>> entry_dates = sorted(updated_dates[1:], reverse=True)
436 >>> assert feed_updated == entry_dates[0], (
437@@ -206,7 +211,7 @@ different entry.
438 >>> validate_feed(browser.contents,
439 ... browser.headers['content-type'], browser.url)
440 No Errors
441- >>> BSS(browser.contents).title.contents
442+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
443 [u'Latest Revisions for Branch lp://dev/~mark/firefox/release--0.9.1']
444 >>> print(browser.url)
445 http://feeds.launchpad.test/~mark/firefox/release--0.9.1/branch.atom
446@@ -214,17 +219,19 @@ different entry.
447 The first <id> in a feed identifies the feed. Each entry then has its
448 own <id>, which in the case of a single branch feed will be identical.
449
450- >>> soup = BSS(browser.contents, parseOnlyThese=SoupStrainer('id'))
451+ >>> soup = BeautifulSoup(
452+ ... browser.contents, 'xml', parse_only=SoupStrainer('id'))
453 >>> ids = parse_ids(browser.contents)
454 >>> for id_ in ids:
455 ... print(id_)
456 <id>tag:launchpad.net,2006-10-16:/code/~mark/firefox/release--0.9.1</id>
457 <id>tag:launchpad.net,2005-03-09:/code/~mark/firefox/release--0.9.1/revision/1</id>
458 >>> print_parse_links(browser)
459- <link rel="self" href="http://feeds.launchpad.test/~mark/firefox/release--0.9.1/branch.atom" />
460+ <link href="http://feeds.launchpad.test/~mark/firefox/release--0.9.1/branch.atom" rel="self"/>
461 >>> strainer = SoupStrainer('updated')
462- >>> updated_dates = [extract_text(tag) for tag in BSS(browser.contents,
463- ... parseOnlyThese=strainer)]
464+ >>> updated_dates = [
465+ ... extract_text(tag) for tag in BeautifulSoup(
466+ ... browser.contents, 'xml', parse_only=strainer)]
467
468 The update date for the entire feed (updated_dates[0]) must be equal
469 to the update_date of the first entry in the feed (updated_dates[1]).
470diff --git a/lib/lp/code/stories/feeds/xx-revision-atom.txt b/lib/lp/code/stories/feeds/xx-revision-atom.txt
471index 0ef5eae..3638b09 100644
472--- a/lib/lp/code/stories/feeds/xx-revision-atom.txt
473+++ b/lib/lp/code/stories/feeds/xx-revision-atom.txt
474@@ -1,9 +1,9 @@
475 = Atom Feeds For Revisions =
476
477 Atom feeds produce XML not HTML. Therefore we must parse the output as XML
478-using BeautifulStoneSoup instead of BeautifulSoup or the helper functions.
479+by asking BeautifulSoup to use lxml.
480
481- >>> from BeautifulSoup import BeautifulStoneSoup as BSS
482+ >>> from lp.services.beautifulsoup import BeautifulSoup4 as BeautifulSoup
483 >>> from lp.services.feeds.tests.helper import (
484 ... parse_ids, parse_links, validate_feed)
485
486@@ -75,7 +75,7 @@ that have been committed by that person (or attributed to that person).
487 ... browser.contents, browser.headers['content-type'], browser.url)
488 >>> validate_browser_feed(anon_browser)
489 No Errors
490- >>> BSS(anon_browser.contents).title.contents
491+ >>> BeautifulSoup(anon_browser.contents, 'xml').title.contents
492 [u'Latest Revisions by Mike Murphy']
493 >>> def print_parse_ids(browser):
494 ... for id in parse_ids(browser.contents):
495@@ -96,7 +96,7 @@ Ensure the self link is correct and there is only one.
496 ... for link in parse_links(browser.contents, rel="self"):
497 ... print(link)
498 >>> print_parse_links(anon_browser)
499- <link rel="self" href="http://feeds.launchpad.test/~mike/revisions.atom" />
500+ <link href="http://feeds.launchpad.test/~mike/revisions.atom" rel="self"/>
501
502 If we look at the feed for a team, we get revisions created by any member
503 of that team.
504@@ -104,7 +104,7 @@ of that team.
505 >>> browser.open('http://feeds.launchpad.test/~m-team/revisions.atom')
506 >>> validate_browser_feed(browser)
507 No Errors
508- >>> BSS(browser.contents).title.contents
509+ >>> BeautifulSoup(browser.contents, 'xml').title.contents
510 [u'Latest Revisions by members of The M Team']
511 >>> print_parse_ids(browser)
512 <id>tag:launchpad.net,...:/code/~m-team</id>
513@@ -122,7 +122,7 @@ that have been committed on branches for the product.
514 >>> anon_browser.open('http://feeds.launchpad.test/fooix/revisions.atom')
515 >>> validate_browser_feed(anon_browser)
516 No Errors
517- >>> BSS(anon_browser.contents).title.contents
518+ >>> BeautifulSoup(anon_browser.contents, 'xml').title.contents
519 [u'Latest Revisions for Fooix']
520
521 Ignore the date associated with the id of 'fooix' as this is the date created
522@@ -136,7 +136,7 @@ for the product, which will be different each time the test is run.
523 Ensure the self link points to the feed location and there is only one.
524
525 >>> print_parse_links(anon_browser)
526- <link rel="self" href="http://feeds.launchpad.test/fooix/revisions.atom" />
527+ <link href="http://feeds.launchpad.test/fooix/revisions.atom" rel="self"/>
528
529
530 == Feed for a project group's revisions ==
531@@ -147,7 +147,7 @@ branch for any product that is associated with the project group.
532 >>> anon_browser.open('http://feeds.launchpad.test/fubar/revisions.atom')
533 >>> validate_browser_feed(anon_browser)
534 No Errors
535- >>> BSS(anon_browser.contents).title.contents
536+ >>> BeautifulSoup(anon_browser.contents, 'xml').title.contents
537 [u'Latest Revisions for Fubar']
538
539 Ignore the date associated with the id of 'fubar' as this is the date created
540@@ -163,4 +163,4 @@ of the project group, which will be different each time the test is run.
541 Ensure the self link points to the feed location and there is only one.
542
543 >>> print_parse_links(anon_browser)
544- <link rel="self" href="http://feeds.launchpad.test/fubar/revisions.atom" />
545+ <link href="http://feeds.launchpad.test/fubar/revisions.atom" rel="self"/>
546diff --git a/lib/lp/registry/stories/announcements/xx-announcements.txt b/lib/lp/registry/stories/announcements/xx-announcements.txt
547index f8e1045..addfc4a 100644
548--- a/lib/lp/registry/stories/announcements/xx-announcements.txt
549+++ b/lib/lp/registry/stories/announcements/xx-announcements.txt
550@@ -7,8 +7,8 @@ dedicated batched page showing all announcements, and as an RSS/Atom
551 news feed.
552
553 >>> from lp.services.beautifulsoup import (
554- ... BeautifulSoup,
555- ... SoupStrainer,
556+ ... BeautifulSoup4 as BeautifulSoup,
557+ ... SoupStrainer4 as SoupStrainer,
558 ... )
559 >>> from lp.services.feeds.tests.helper import (
560 ... parse_ids, parse_links, validate_feed)
561@@ -643,7 +643,7 @@ domain.
562 >>> links = parse_links(nopriv_browser.contents, rel='self')
563 >>> for link in links:
564 ... print link
565- <link rel="self" href="http://feeds.launchpad.test/netapplet/announcements.atom" />
566+ <link href="http://feeds.launchpad.test/netapplet/announcements.atom" rel="self"/>
567
568 >>> for id_ in parse_ids(nopriv_browser.contents):
569 ... print extract_text(id_)
570@@ -716,7 +716,7 @@ products.
571 >>> links = parse_links(nopriv_browser.contents, rel='self')
572 >>> for link in links:
573 ... print link
574- <link rel="self" href="http://feeds.launchpad.test/apache/announcements.atom" />
575+ <link href="http://feeds.launchpad.test/apache/announcements.atom" rel="self"/>
576
577 Finally, there is a feed for all announcements across all projects
578 hosted in Launchpad:
579@@ -755,18 +755,16 @@ let us use a DTD to define the html entities that standard xml is missing.
580 No Errors
581 >>> soup = BeautifulSoup(nopriv_browser.contents)
582 >>> soup.find('feed').entry.title
583- <...>Ampersand=&quot;&amp;&quot; LessThan=&quot;&lt;&quot;
584- GreaterThan=&quot;&gt;&quot;</title>
585- >>> soup.find('feed').entry.content
586+ <...>Ampersand="&amp;" LessThan="&lt;" GreaterThan="&gt;"</title>
587+ >>> print(soup.find('feed').entry.content)
588 <...
589- Ampersand=&amp;quot;&amp;amp;&amp;quot;&lt;br /&gt;
590- LessThan=&amp;quot;&amp;lt;&amp;quot;&lt;br /&gt;
591- GreaterThan=&amp;quot;&amp;gt;&amp;quot;&lt;br /&gt;
592- Newline=&amp;quot;&lt;br /&gt;
593- &amp;quot;&lt;br /&gt;
594- url=&amp;quot;&lt;a rel=&quot;nofollow&quot;
595- href=&quot;http://www.ubuntu.com&quot;&gt;http://&lt;wbr
596- /&gt;www.ubuntu.&lt;wbr /&gt;com&lt;/a&gt;&amp;quot;...
597+ Ampersand="&amp;amp;"&lt;br/&gt;
598+ LessThan="&amp;lt;"&lt;br/&gt;
599+ GreaterThan="&amp;gt;"&lt;br/&gt;
600+ Newline="&lt;br/&gt;
601+ "&lt;br/&gt;
602+ url="&lt;a href="http://www.ubuntu.com"
603+ rel="nofollow"&gt;http://&lt;wbr/&gt;www.ubuntu.&lt;wbr/&gt;com&lt;/a&gt;"...
604
605
606 Deletion
607diff --git a/lib/lp/services/feeds/doc/feeds.txt b/lib/lp/services/feeds/doc/feeds.txt
608index 35086d2..02315cb 100644
609--- a/lib/lp/services/feeds/doc/feeds.txt
610+++ b/lib/lp/services/feeds/doc/feeds.txt
611@@ -157,7 +157,7 @@ we are testing xhtml encoding here in case we need it in the future.
612 >>> xhtml = FeedTypedData("<b> and &nbsp; and &amp;</b><hr/>",
613 ... content_type="xhtml")
614 >>> xhtml.content
615- u'<b> and \xa0 and &amp;</b><hr />'
616+ u'<b> and \xa0 and &amp;</b><hr/>'
617
618
619 == validate_feed() helper function ==
620diff --git a/lib/lp/services/feeds/feed.py b/lib/lp/services/feeds/feed.py
621index 9462062..143121c 100644
622--- a/lib/lp/services/feeds/feed.py
623+++ b/lib/lp/services/feeds/feed.py
624@@ -27,7 +27,7 @@ from zope.component import getUtility
625 from zope.datetime import rfc1123_date
626 from zope.interface import implementer
627
628-from lp.services.beautifulsoup import BeautifulSoup
629+from lp.services.beautifulsoup import BeautifulSoup4 as BeautifulSoup
630 from lp.services.config import config
631 from lp.services.feeds.interfaces.feed import (
632 IFeed,
633@@ -302,9 +302,7 @@ class FeedTypedData:
634 if self.content_type in ('text', 'html'):
635 altered_content = html_escape(altered_content)
636 elif self.content_type == 'xhtml':
637- soup = BeautifulSoup(
638- altered_content,
639- convertEntities=BeautifulSoup.HTML_ENTITIES)
640+ soup = BeautifulSoup(altered_content)
641 altered_content = unicode(soup)
642 return altered_content
643
644diff --git a/lib/lp/services/feeds/stories/xx-links.txt b/lib/lp/services/feeds/stories/xx-links.txt
645index 83110fd..23e467b 100644
646--- a/lib/lp/services/feeds/stories/xx-links.txt
647+++ b/lib/lp/services/feeds/stories/xx-links.txt
648@@ -11,13 +11,13 @@ launchpad.test to provide links to corresponding Atom feeds.
649 The root launchpad.test url will have a link to the Atom feed which
650 displays the most recent announcements for all the projects.
651
652- >>> from lp.services.beautifulsoup import BeautifulSoup
653+ >>> from lp.services.beautifulsoup import BeautifulSoup4 as BeautifulSoup
654 >>> browser.open('http://launchpad.test/')
655 >>> soup = BeautifulSoup(browser.contents)
656 >>> soup.head.findAll('link', type='application/atom+xml')
657- [<link rel="alternate" type="application/atom+xml"
658- href="http://feeds.launchpad.test/announcements.atom"
659- title="All Announcements" />]
660+ [<link href="http://feeds.launchpad.test/announcements.atom"
661+ rel="alternate" title="All Announcements"
662+ type="application/atom+xml"/>]
663
664 The http://launchpad.test/+announcements page also displays recent
665 announcements for all the projects so it should have a link to the same
666@@ -26,9 +26,9 @@ feed.
667 >>> browser.open('http://launchpad.test/+announcements')
668 >>> soup = BeautifulSoup(browser.contents)
669 >>> soup.head.findAll('link', type='application/atom+xml')
670- [<link rel="alternate" type="application/atom+xml"
671- href="http://feeds.launchpad.test/announcements.atom"
672- title="All Announcements" />]
673+ [<link href="http://feeds.launchpad.test/announcements.atom"
674+ rel="alternate" title="All Announcements"
675+ type="application/atom+xml"/>]
676
677 == Single Bug Feed ==
678
679@@ -38,9 +38,9 @@ atom feed for that one bug.
680 >>> browser.open('http://bugs.launchpad.test/firefox/+bug/1')
681 >>> soup = BeautifulSoup(browser.contents)
682 >>> soup.head.findAll('link', type='application/atom+xml')
683- [<link rel="alternate" type="application/atom+xml"
684- href="http://feeds.launchpad.test/bugs/1/bug.atom"
685- title="Bug 1 Feed" />]
686+ [<link href="http://feeds.launchpad.test/bugs/1/bug.atom"
687+ rel="alternate" title="Bug 1 Feed"
688+ type="application/atom+xml"/>]
689
690 But if the bug is private, there should be no link.
691
692@@ -80,15 +80,15 @@ branches.
693 >>> browser.open('http://launchpad.test/~stevea')
694 >>> soup = BeautifulSoup(browser.contents)
695 >>> soup.head.findAll('link', type='application/atom+xml')
696- [<link rel="alternate" type="application/atom+xml"
697- href="http://feeds.launchpad.test/~stevea/latest-bugs.atom"
698- title="Latest Bugs for Steve Alexander" />,
699- <link rel="alternate" type="application/atom+xml"
700- href="http://feeds.launchpad.test/~stevea/branches.atom"
701- title="Latest Branches for Steve Alexander" />,
702- <link rel="alternate" type="application/atom+xml"
703- href="http://feeds.launchpad.test/~stevea/revisions.atom"
704- title="Latest Revisions by Steve Alexander" />]
705+ [<link href="http://feeds.launchpad.test/~stevea/latest-bugs.atom"
706+ rel="alternate" title="Latest Bugs for Steve Alexander"
707+ type="application/atom+xml"/>,
708+ <link href="http://feeds.launchpad.test/~stevea/branches.atom"
709+ rel="alternate" title="Latest Branches for Steve Alexander"
710+ type="application/atom+xml"/>,
711+ <link href="http://feeds.launchpad.test/~stevea/revisions.atom"
712+ rel="alternate" title="Latest Revisions by Steve Alexander"
713+ type="application/atom+xml"/>]
714
715 On the bugs subdomain, only a link to the bugs feed will be included,
716 not the branches link.
717@@ -96,9 +96,9 @@ not the branches link.
718 >>> browser.open('http://bugs.launchpad.test/~stevea')
719 >>> soup = BeautifulSoup(browser.contents)
720 >>> soup.head.findAll('link', type='application/atom+xml')
721- [<link rel="alternate" type="application/atom+xml"
722- href="http://feeds.launchpad.test/~stevea/latest-bugs.atom"
723- title="Latest Bugs for Steve Alexander" />]
724+ [<link href="http://feeds.launchpad.test/~stevea/latest-bugs.atom"
725+ rel="alternate" title="Latest Bugs for Steve Alexander"
726+ type="application/atom+xml"/>]
727
728
729 == Latest Bugs, Branches, and Announcements for a Product ==
730@@ -112,27 +112,27 @@ main product page.
731 >>> browser.open('http://launchpad.test/jokosher')
732 >>> soup = BeautifulSoup(browser.contents)
733 >>> soup.head.findAll('link', type='application/atom+xml')
734- [<link rel="alternate" type="application/atom+xml"
735- href="http://feeds.launchpad.test/jokosher/announcements.atom"
736- title="Announcements for Jokosher" />,
737- <link rel="alternate" type="application/atom+xml"
738- href="http://feeds.launchpad.test/jokosher/latest-bugs.atom"
739- title="Latest Bugs for Jokosher" />,
740- <link rel="alternate" type="application/atom+xml"
741- href="http://feeds.launchpad.test/jokosher/branches.atom"
742- title="Latest Branches for Jokosher" />,
743- <link rel="alternate" type="application/atom+xml"
744- href="http://feeds.launchpad.test/jokosher/revisions.atom"
745- title="Latest Revisions for Jokosher" />]
746+ [<link href="http://feeds.launchpad.test/jokosher/announcements.atom"
747+ rel="alternate" title="Announcements for Jokosher"
748+ type="application/atom+xml"/>,
749+ <link href="http://feeds.launchpad.test/jokosher/latest-bugs.atom"
750+ rel="alternate" title="Latest Bugs for Jokosher"
751+ type="application/atom+xml"/>,
752+ <link href="http://feeds.launchpad.test/jokosher/branches.atom"
753+ rel="alternate" title="Latest Branches for Jokosher"
754+ type="application/atom+xml"/>,
755+ <link href="http://feeds.launchpad.test/jokosher/revisions.atom"
756+ rel="alternate" title="Latest Revisions for Jokosher"
757+ type="application/atom+xml"/>]
758
759 Only bug feeds should be linked to on bugs.launchpad.test.
760
761 >>> browser.open('http://bugs.launchpad.test/jokosher')
762 >>> soup = BeautifulSoup(browser.contents)
763 >>> soup.head.findAll('link', type='application/atom+xml')
764- [<link rel="alternate" type="application/atom+xml"
765- href="http://feeds.launchpad.test/jokosher/latest-bugs.atom"
766- title="Latest Bugs for Jokosher" />]
767+ [<link href="http://feeds.launchpad.test/jokosher/latest-bugs.atom"
768+ rel="alternate" title="Latest Bugs for Jokosher"
769+ type="application/atom+xml"/>]
770
771
772 == Escaping the title ==
773@@ -160,18 +160,22 @@ it must have quotes and html escaped.
774 >>> browser.open('http://launchpad.test/bad-displayname')
775 >>> soup = BeautifulSoup(browser.contents)
776 >>> soup.head.findAll('link', type='application/atom+xml')
777- [<link rel="alternate" type="application/atom+xml"
778- href="http://feeds.launchpad.test/bad-displayname/announcements.atom"
779- title='Announcements for Bad displayname"&gt;&lt;script&gt;alert("h4x0r")&lt;/script&gt;' />,
780- <link rel="alternate" type="application/atom+xml"
781- href="http://feeds.launchpad.test/bad-displayname/latest-bugs.atom"
782- title='Latest Bugs for Bad displayname"&gt;&lt;script&gt;alert("h4x0r")&lt;/script&gt;' />,
783- <link rel="alternate" type="application/atom+xml"
784- href="http://feeds.launchpad.test/bad-displayname/branches.atom"
785- title='Latest Branches for Bad displayname"&gt;&lt;script&gt;alert("h4x0r")&lt;/script&gt;' />,
786- <link rel="alternate" type="application/atom+xml"
787- href="http://feeds.launchpad.test/bad-displayname/revisions.atom"
788- title='Latest Revisions for Bad displayname"&gt;&lt;script&gt;alert("h4x0r")&lt;/script&gt;' />]
789+ [<link href="http://feeds.launchpad.test/bad-displayname/announcements.atom"
790+ rel="alternate"
791+ title='Announcements for Bad displayname"&gt;&lt;script&gt;alert("h4x0r")&lt;/script&gt;'
792+ type="application/atom+xml"/>,
793+ <link href="http://feeds.launchpad.test/bad-displayname/latest-bugs.atom"
794+ rel="alternate"
795+ title='Latest Bugs for Bad displayname"&gt;&lt;script&gt;alert("h4x0r")&lt;/script&gt;'
796+ type="application/atom+xml"/>,
797+ <link href="http://feeds.launchpad.test/bad-displayname/branches.atom"
798+ rel="alternate"
799+ title='Latest Branches for Bad displayname"&gt;&lt;script&gt;alert("h4x0r")&lt;/script&gt;'
800+ type="application/atom+xml"/>,
801+ <link href="http://feeds.launchpad.test/bad-displayname/revisions.atom"
802+ rel="alternate"
803+ title='Latest Revisions for Bad displayname"&gt;&lt;script&gt;alert("h4x0r")&lt;/script&gt;'
804+ type="application/atom+xml"/>]
805
806 == Latest Bugs for a ProjectGroup ==
807
808@@ -184,27 +188,27 @@ on the main project group page.
809 >>> browser.open('http://launchpad.test/gnome')
810 >>> soup = BeautifulSoup(browser.contents)
811 >>> soup.head.findAll('link', type='application/atom+xml')
812- [<link rel="alternate" type="application/atom+xml"
813- href="http://feeds.launchpad.test/gnome/announcements.atom"
814- title="Announcements for GNOME" />,
815- <link rel="alternate" type="application/atom+xml"
816- href="http://feeds.launchpad.test/gnome/latest-bugs.atom"
817- title="Latest Bugs for GNOME" />,
818- <link rel="alternate" type="application/atom+xml"
819- href="http://feeds.launchpad.test/gnome/branches.atom"
820- title="Latest Branches for GNOME" />,
821- <link rel="alternate" type="application/atom+xml"
822- href="http://feeds.launchpad.test/gnome/revisions.atom"
823- title="Latest Revisions for GNOME" />]
824+ [<link href="http://feeds.launchpad.test/gnome/announcements.atom"
825+ rel="alternate" title="Announcements for GNOME"
826+ type="application/atom+xml"/>,
827+ <link href="http://feeds.launchpad.test/gnome/latest-bugs.atom"
828+ rel="alternate" title="Latest Bugs for GNOME"
829+ type="application/atom+xml"/>,
830+ <link href="http://feeds.launchpad.test/gnome/branches.atom"
831+ rel="alternate" title="Latest Branches for GNOME"
832+ type="application/atom+xml"/>,
833+ <link href="http://feeds.launchpad.test/gnome/revisions.atom"
834+ rel="alternate" title="Latest Revisions for GNOME"
835+ type="application/atom+xml"/>]
836
837 Only bug feeds should be linked to on bugs.launchpad.test.
838
839 >>> browser.open('http://bugs.launchpad.test/gnome')
840 >>> soup = BeautifulSoup(browser.contents)
841 >>> soup.head.findAll('link', type='application/atom+xml')
842- [<link rel="alternate" type="application/atom+xml"
843- href="http://feeds.launchpad.test/gnome/latest-bugs.atom"
844- title="Latest Bugs for GNOME" />]
845+ [<link href="http://feeds.launchpad.test/gnome/latest-bugs.atom"
846+ rel="alternate" title="Latest Bugs for GNOME"
847+ type="application/atom+xml"/>]
848
849 The default view for a project group on bugs.launchpad.test is +bugs. The
850 default bug listing matches the latest-bugs atom feed, but any search
851@@ -231,21 +235,21 @@ An announcements feed link should also be shown on the main distro page.
852 >>> browser.open('http://launchpad.test/ubuntu')
853 >>> soup = BeautifulSoup(browser.contents)
854 >>> soup.head.findAll('link', type='application/atom+xml')
855- [<link rel="alternate" type="application/atom+xml"
856- href="http://feeds.launchpad.test/ubuntu/announcements.atom"
857- title="Announcements for Ubuntu" />,
858- <link rel="alternate" type="application/atom+xml"
859- href="http://feeds.launchpad.test/ubuntu/latest-bugs.atom"
860- title="Latest Bugs for Ubuntu" />]
861+ [<link href="http://feeds.launchpad.test/ubuntu/announcements.atom"
862+ rel="alternate" title="Announcements for Ubuntu"
863+ type="application/atom+xml"/>,
864+ <link href="http://feeds.launchpad.test/ubuntu/latest-bugs.atom"
865+ rel="alternate" title="Latest Bugs for Ubuntu"
866+ type="application/atom+xml"/>]
867
868 Only bug feeds should be linked to on bugs.launchpad.test.
869
870 >>> browser.open('http://bugs.launchpad.test/ubuntu')
871 >>> soup = BeautifulSoup(browser.contents)
872 >>> soup.head.findAll('link', type='application/atom+xml')
873- [<link rel="alternate" type="application/atom+xml"
874- href="http://feeds.launchpad.test/ubuntu/latest-bugs.atom"
875- title="Latest Bugs for Ubuntu" />]
876+ [<link href="http://feeds.launchpad.test/ubuntu/latest-bugs.atom"
877+ rel="alternate" title="Latest Bugs for Ubuntu"
878+ type="application/atom+xml"/>]
879
880
881 == Latest Bugs for a Distroseries ==
882@@ -256,9 +260,10 @@ show a link to the atom feed for that distroseries' latest bugs.
883 >>> browser.open('http://bugs.launchpad.test/ubuntu/hoary')
884 >>> soup = BeautifulSoup(browser.contents)
885 >>> soup.head.findAll('link', type='application/atom+xml')
886- [<link rel="alternate" type="application/atom+xml"
887+ [<link
888 href="http://feeds.launchpad.test/ubuntu/hoary/latest-bugs.atom"
889- title="Latest Bugs for Hoary" />]
890+ rel="alternate" title="Latest Bugs for Hoary"
891+ type="application/atom+xml"/>]
892
893
894 == Latest Bugs for a Product Series ==
895@@ -269,9 +274,9 @@ show a link to the atom feed for that product series' latest bugs.
896 >>> browser.open('http://bugs.launchpad.test/firefox/1.0')
897 >>> soup = BeautifulSoup(browser.contents)
898 >>> soup.head.findAll('link', type='application/atom+xml')
899- [<link rel="alternate" type="application/atom+xml"
900- href="http://feeds.launchpad.test/firefox/1.0/latest-bugs.atom"
901- title="Latest Bugs for 1.0" />]
902+ [<link href="http://feeds.launchpad.test/firefox/1.0/latest-bugs.atom"
903+ rel="alternate" title="Latest Bugs for 1.0"
904+ type="application/atom+xml"/>]
905
906
907 == Latest Bugs for a Source Package ==
908@@ -282,9 +287,10 @@ show a link to the atom feed for that source package's latest bugs.
909 >>> browser.open('http://bugs.launchpad.test/ubuntu/+source/cnews')
910 >>> soup = BeautifulSoup(browser.contents)
911 >>> soup.head.findAll('link', type='application/atom+xml')
912- [<link rel="alternate" type="application/atom+xml"
913+ [<link
914 href="http://feeds.launchpad.test/ubuntu/+source/cnews/latest-bugs.atom"
915- title="Latest Bugs for cnews in Ubuntu" />]
916+ rel="alternate" title="Latest Bugs for cnews in Ubuntu"
917+ type="application/atom+xml"/>]
918
919
920 == Latest Branches for a ProjectGroup ==
921@@ -295,12 +301,14 @@ to the atom feed for that project group's latest branches.
922 >>> browser.open('http://code.launchpad.test/mozilla')
923 >>> soup = BeautifulSoup(browser.contents)
924 >>> soup.head.findAll('link', type='application/atom+xml')
925- [<link rel="alternate" type="application/atom+xml"
926+ [<link
927 href="http://feeds.launchpad.test/mozilla/branches.atom"
928- title="Latest Branches for The Mozilla Project" />,
929- <link rel="alternate" type="application/atom+xml"
930+ rel="alternate" title="Latest Branches for The Mozilla Project"
931+ type="application/atom+xml"/>,
932+ <link
933 href="http://feeds.launchpad.test/mozilla/revisions.atom"
934- title="Latest Revisions for The Mozilla Project" />]
935+ rel="alternate" title="Latest Revisions for The Mozilla Project"
936+ type="application/atom+xml"/>]
937
938
939 == Latest Branches for a Product ==
940@@ -311,12 +319,13 @@ to the atom feed for that product's latest branches.
941 >>> browser.open('http://code.launchpad.test/firefox')
942 >>> soup = BeautifulSoup(browser.contents)
943 >>> soup.head.findAll('link', type='application/atom+xml')
944- [<link rel="alternate" type="application/atom+xml"
945- href="http://feeds.launchpad.test/firefox/branches.atom"
946- title="Latest Branches for Mozilla Firefox" />,
947- <link rel="alternate" type="application/atom+xml"
948- href="http://feeds.launchpad.test/firefox/revisions.atom"
949- title="Latest Revisions for Mozilla Firefox" />]
950+ [<link href="http://feeds.launchpad.test/firefox/branches.atom"
951+ rel="alternate" title="Latest Branches for Mozilla Firefox"
952+ type="application/atom+xml"/>,
953+ <link href="http://feeds.launchpad.test/firefox/revisions.atom"
954+ rel="alternate"
955+ title="Latest Revisions for Mozilla Firefox"
956+ type="application/atom+xml"/>]
957
958
959 == Latest Branches for a Person ==
960@@ -327,12 +336,12 @@ to the atom feed for that person's latest branches.
961 >>> browser.open('http://code.launchpad.test/~mark')
962 >>> soup = BeautifulSoup(browser.contents)
963 >>> soup.head.findAll('link', type='application/atom+xml')
964- [<link rel="alternate" type="application/atom+xml"
965- href="http://feeds.launchpad.test/~mark/branches.atom"
966- title="Latest Branches for Mark Shuttleworth" />,
967- <link rel="alternate" type="application/atom+xml"
968- href="http://feeds.launchpad.test/~mark/revisions.atom"
969- title="Latest Revisions by Mark Shuttleworth" />]
970+ [<link href="http://feeds.launchpad.test/~mark/branches.atom"
971+ rel="alternate" title="Latest Branches for Mark Shuttleworth"
972+ type="application/atom+xml"/>,
973+ <link href="http://feeds.launchpad.test/~mark/revisions.atom"
974+ rel="alternate" title="Latest Revisions by Mark Shuttleworth"
975+ type="application/atom+xml"/>]
976
977
978 == Latest Revisions on a Branch ==
979@@ -344,9 +353,11 @@ atom feed for that branch's revisions.
980 >>> browser.open(url)
981 >>> soup = BeautifulSoup(browser.contents)
982 >>> soup.head.findAll('link', type='application/atom+xml')
983- [<link rel="alternate" type="application/atom+xml"
984+ [<link
985 href="http://feeds.launchpad.test/~mark/firefox/release--0.9.1/branch.atom"
986- title="Latest Revisions for Branch lp://dev/~mark/firefox/release--0.9.1" />]
987+ rel="alternate"
988+ title="Latest Revisions for Branch lp://dev/~mark/firefox/release--0.9.1"
989+ type="application/atom+xml"/>]
990
991 But if the branch is private, there should be no link.
992
993diff --git a/lib/lp/services/feeds/stories/xx-security.txt b/lib/lp/services/feeds/stories/xx-security.txt
994index ea7122d..2e441df 100644
995--- a/lib/lp/services/feeds/stories/xx-security.txt
996+++ b/lib/lp/services/feeds/stories/xx-security.txt
997@@ -4,32 +4,32 @@ Feeds do not display private bugs
998 Feeds never contain private bugs, as we are serving feeds over HTTP.
999 First, set all the bugs to private.
1000
1001- >>> from zope.security.interfaces import Unauthorized
1002- >>> from BeautifulSoup import BeautifulStoneSoup as BSS
1003- >>> from lp.services.database.interfaces import IStore
1004 >>> import transaction
1005- >>> from lp.bugs.model.bug import Bug
1006+ >>> from zope.security.interfaces import Unauthorized
1007 >>> from lp.app.enums import InformationType
1008+ >>> from lp.bugs.model.bug import Bug
1009+ >>> from lp.services.beautifulsoup import BeautifulSoup4 as BeautifulSoup
1010+ >>> from lp.services.database.interfaces import IStore
1011 >>> IStore(Bug).find(Bug).set(information_type=InformationType.USERDATA)
1012 >>> transaction.commit()
1013
1014 There should be zero entries in these feeds, since all the bugs are private.
1015
1016 >>> browser.open('http://feeds.launchpad.test/jokosher/latest-bugs.atom')
1017- >>> BSS(browser.contents)('entry')
1018+ >>> BeautifulSoup(browser.contents, 'xml')('entry')
1019 []
1020
1021 >>> browser.open('http://feeds.launchpad.test/mozilla/latest-bugs.atom')
1022- >>> BSS(browser.contents)('entry')
1023+ >>> BeautifulSoup(browser.contents, 'xml')('entry')
1024 []
1025
1026 >>> browser.open('http://feeds.launchpad.test/~name16/latest-bugs.atom')
1027- >>> BSS(browser.contents)('entry')
1028+ >>> BeautifulSoup(browser.contents, 'xml')('entry')
1029 []
1030
1031 >>> browser.open(
1032 ... 'http://feeds.launchpad.test/~simple-team/latest-bugs.atom')
1033- >>> BSS(browser.contents)('entry')
1034+ >>> BeautifulSoup(browser.contents, 'xml')('entry')
1035 []
1036
1037 >>> from lp.services.config import config
1038@@ -41,52 +41,52 @@ There should be zero entries in these feeds, since all the bugs are private.
1039 >>> browser.open('http://feeds.launchpad.test/bugs/+bugs.atom?'
1040 ... 'field.searchtext=&search=Search+Bug+Reports&'
1041 ... 'field.scope=all&field.scope.target=')
1042- >>> BSS(browser.contents)('entry')
1043+ >>> BeautifulSoup(browser.contents, 'xml')('entry')
1044 []
1045
1046 There should be just one <tr> elements for the table header in
1047 these HTML feeds, since all the bugs are private.
1048
1049 >>> browser.open('http://feeds.launchpad.test/jokosher/latest-bugs.html')
1050- >>> len(BSS(browser.contents)('tr'))
1051+ >>> len(BeautifulSoup(browser.contents, 'xml')('tr'))
1052 1
1053
1054- >>> print extract_text(BSS(browser.contents)('tr')[0])
1055+ >>> print extract_text(BeautifulSoup(browser.contents, 'xml')('tr')[0])
1056 Bugs in Jokosher
1057
1058 >>> browser.open('http://feeds.launchpad.test/mozilla/latest-bugs.html')
1059- >>> len(BSS(browser.contents)('tr'))
1060+ >>> len(BeautifulSoup(browser.contents, 'xml')('tr'))
1061 1
1062
1063- >>> print extract_text(BSS(browser.contents)('tr')[0])
1064+ >>> print extract_text(BeautifulSoup(browser.contents, 'xml')('tr')[0])
1065 Bugs in The Mozilla Project
1066
1067 >>> browser.open('http://feeds.launchpad.test/~name16/latest-bugs.html')
1068- >>> len(BSS(browser.contents)('tr'))
1069+ >>> len(BeautifulSoup(browser.contents, 'xml')('tr'))
1070 1
1071
1072- >>> print extract_text(BSS(browser.contents)('tr')[0])
1073+ >>> print extract_text(BeautifulSoup(browser.contents, 'xml')('tr')[0])
1074 Bugs for Foo Bar
1075
1076 >>> browser.open(
1077 ... 'http://feeds.launchpad.test/~simple-team/latest-bugs.html')
1078- >>> len(BSS(browser.contents)('tr'))
1079+ >>> len(BeautifulSoup(browser.contents, 'xml')('tr'))
1080 1
1081
1082- >>> print extract_text(BSS(browser.contents)('tr')[0])
1083+ >>> print extract_text(BeautifulSoup(browser.contents, 'xml')('tr')[0])
1084 Bugs for Simple Team
1085
1086 >>> browser.open('http://feeds.launchpad.test/bugs/+bugs.html?'
1087 ... 'field.searchtext=&search=Search+Bug+Reports&'
1088 ... 'field.scope=all&field.scope.target=')
1089- >>> len(BSS(browser.contents)('tr'))
1090+ >>> len(BeautifulSoup(browser.contents, 'xml')('tr'))
1091 1
1092
1093 >>> try:
1094 ... browser.open('http://feeds.launchpad.test/bugs/1/bug.html')
1095 ... except Unauthorized:
1096 ... print "Shouldn't raise Unauthorized exception"
1097- >>> BSS(browser.contents)('entry')
1098+ >>> BeautifulSoup(browser.contents, 'xml')('entry')
1099 []
1100
1101 Revert configuration change after tests are finished.
1102diff --git a/lib/lp/services/feeds/tests/helper.py b/lib/lp/services/feeds/tests/helper.py
1103index e0a96aa..826df83 100644
1104--- a/lib/lp/services/feeds/tests/helper.py
1105+++ b/lib/lp/services/feeds/tests/helper.py
1106@@ -31,9 +31,11 @@ from zope.interface import (
1107 implementer,
1108 Interface,
1109 )
1110-from BeautifulSoup import BeautifulStoneSoup as BSS
1111-from BeautifulSoup import SoupStrainer
1112
1113+from lp.services.beautifulsoup import (
1114+ BeautifulSoup4 as BeautifulSoup,
1115+ SoupStrainer4 as SoupStrainer,
1116+ )
1117 from lp.services.webapp.publisher import LaunchpadView
1118
1119
1120@@ -62,25 +64,23 @@ class ThingFeedView(LaunchpadView):
1121 def parse_entries(contents):
1122 """Define a helper function for parsing feed entries."""
1123 strainer = SoupStrainer('entry')
1124- entries = [tag for tag in BSS(contents,
1125- parseOnlyThese=strainer)]
1126+ entries = [
1127+ tag for tag in BeautifulSoup(contents, 'xml', parse_only=strainer)]
1128 return entries
1129
1130
1131 def parse_links(contents, rel):
1132 """Define a helper function for parsing feed links."""
1133 strainer = SoupStrainer('link', rel=rel)
1134- entries = [tag for tag in BSS(contents,
1135- parseOnlyThese=strainer,
1136- selfClosingTags=['link'])]
1137+ entries = [
1138+ tag for tag in BeautifulSoup(contents, 'xml', parse_only=strainer)]
1139 return entries
1140
1141
1142 def parse_ids(contents):
1143 """Define a helper function for parsing ids."""
1144 strainer = SoupStrainer('id')
1145- ids = [tag for tag in BSS(contents,
1146- parseOnlyThese=strainer)]
1147+ ids = [tag for tag in BeautifulSoup(contents, 'xml', parse_only=strainer)]
1148 return ids
1149
1150

Subscribers

People subscribed via source and target branches

to status/vote changes: