Merge ~cjwatson/launchpad:bs4-feeds into launchpad:master
- Git
- lp:~cjwatson/launchpad
- bs4-feeds
- Merge into 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) |
Related bugs: |
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
Description of the change
To post a comment you must log in.
~cjwatson/launchpad:bs4-feeds
updated
- 3535948... by Colin Watson
-
Port xx-announcement
s.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.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/bugs/stories/feeds/xx-bug-atom.txt b/lib/lp/bugs/stories/feeds/xx-bug-atom.txt |
2 | index 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=&field.scope=all&field.searchtext=&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=&field.scope=all&field.searchtext=&search=Search+Bug+Reports" /> |
268 | + <link href="http://feeds.launchpad.test/bugs/+bugs.atom?field.scope.target=&field.scope=all&field.searchtext=&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 | |
290 | diff --git a/lib/lp/bugs/stories/feeds/xx-bug-html.txt b/lib/lp/bugs/stories/feeds/xx-bug-html.txt |
291 | index 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: |
315 | diff --git a/lib/lp/code/stories/feeds/xx-branch-atom.txt b/lib/lp/code/stories/feeds/xx-branch-atom.txt |
316 | index 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]). |
470 | diff --git a/lib/lp/code/stories/feeds/xx-revision-atom.txt b/lib/lp/code/stories/feeds/xx-revision-atom.txt |
471 | index 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"/> |
546 | diff --git a/lib/lp/registry/stories/announcements/xx-announcements.txt b/lib/lp/registry/stories/announcements/xx-announcements.txt |
547 | index 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="&" LessThan="<" |
584 | - GreaterThan=">"</title> |
585 | - >>> soup.find('feed').entry.content |
586 | + <...>Ampersand="&" LessThan="<" GreaterThan=">"</title> |
587 | + >>> print(soup.find('feed').entry.content) |
588 | <... |
589 | - Ampersand=&quot;&amp;&quot;<br /> |
590 | - LessThan=&quot;&lt;&quot;<br /> |
591 | - GreaterThan=&quot;&gt;&quot;<br /> |
592 | - Newline=&quot;<br /> |
593 | - &quot;<br /> |
594 | - url=&quot;<a rel="nofollow" |
595 | - href="http://www.ubuntu.com">http://<wbr |
596 | - />www.ubuntu.<wbr />com</a>&quot;... |
597 | + Ampersand="&amp;"<br/> |
598 | + LessThan="&lt;"<br/> |
599 | + GreaterThan="&gt;"<br/> |
600 | + Newline="<br/> |
601 | + "<br/> |
602 | + url="<a href="http://www.ubuntu.com" |
603 | + rel="nofollow">http://<wbr/>www.ubuntu.<wbr/>com</a>"... |
604 | |
605 | |
606 | Deletion |
607 | diff --git a/lib/lp/services/feeds/doc/feeds.txt b/lib/lp/services/feeds/doc/feeds.txt |
608 | index 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 and &</b><hr/>", |
613 | ... content_type="xhtml") |
614 | >>> xhtml.content |
615 | - u'<b> and \xa0 and &</b><hr />' |
616 | + u'<b> and \xa0 and &</b><hr/>' |
617 | |
618 | |
619 | == validate_feed() helper function == |
620 | diff --git a/lib/lp/services/feeds/feed.py b/lib/lp/services/feeds/feed.py |
621 | index 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 | |
644 | diff --git a/lib/lp/services/feeds/stories/xx-links.txt b/lib/lp/services/feeds/stories/xx-links.txt |
645 | index 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"><script>alert("h4x0r")</script>' />, |
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"><script>alert("h4x0r")</script>' />, |
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"><script>alert("h4x0r")</script>' />, |
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"><script>alert("h4x0r")</script>' />] |
789 | + [<link href="http://feeds.launchpad.test/bad-displayname/announcements.atom" |
790 | + rel="alternate" |
791 | + title='Announcements for Bad displayname"><script>alert("h4x0r")</script>' |
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"><script>alert("h4x0r")</script>' |
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"><script>alert("h4x0r")</script>' |
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"><script>alert("h4x0r")</script>' |
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 | |
993 | diff --git a/lib/lp/services/feeds/stories/xx-security.txt b/lib/lp/services/feeds/stories/xx-security.txt |
994 | index 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. |
1102 | diff --git a/lib/lp/services/feeds/tests/helper.py b/lib/lp/services/feeds/tests/helper.py |
1103 | index 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 |
Self-approving: this is almost entirely tests, and is mechanical enough that it's not very interesting to review.