Merge lp:~gmb/launchpad/subscribers-timeout-bug-487015 into lp:launchpad
- subscribers-timeout-bug-487015
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | not available |
Merged at revision: | not available |
Proposed branch: | lp:~gmb/launchpad/subscribers-timeout-bug-487015 |
Merge into: | lp:launchpad |
Diff against target: |
395 lines (+188/-20) 7 files modified
lib/lp/bugs/browser/bug.py (+16/-1) lib/lp/bugs/configure.zcml (+4/-1) lib/lp/bugs/doc/bug.txt (+116/-16) lib/lp/bugs/interfaces/bug.py (+18/-0) lib/lp/bugs/model/bug.py (+32/-0) lib/lp/bugs/templates/bug-portlet-subscribers-content.pt (+1/-1) lib/lp/bugs/templates/bug-portlet-subscribers.pt (+1/-1) |
To merge this branch: | bzr merge lp:~gmb/launchpad/subscribers-timeout-bug-487015 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | code | Approve | |
Review via email: mp+15238@code.launchpad.net |
Commit message
The Bug page should no longer time out due to iterating over large numbers of subscribers when generating CSS classes.
Description of the change
Graham Binns (gmb) wrote : | # |
Gavin Panella (allenap) wrote : | # |
Hi Graham,
I have some comments on how to make two of the new methods a bit
faster, but otherwise it all looks good.
I'm still concerned that it's doing at least one query for every
subscriber, rather than batching things up, but at least this branch
should reduce the overhead a lot... actually, I've suddenly had a
thought...
BugViewMixin.
properties, so the current implementation of subscription_class (if
subscribed_person in self.xxx_
worried that this branch may actually cause performance to degrade.
Gavin.
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -437,12 +437,15 @@
>
> For example, "subscribed-false dup-subscribed-
> """
> - if subscribed_person in self.duplicate_
> + bug = self.context
> +
> + if (bug.personIsSu
> + bug.personIsAls
> dup_class = 'dup-subscribed
Now that we can distinguish between from-dupe and also-notified, there
should probably be another CSS class to represent those users, even if
it visually looks exactly the same as dup-subscribed-
do it - I meant it as an observation - unless you've got very itchy
fingers and your keyboard has a scratching board attached.
> else:
> dup_class = 'dup-subscribed
>
> - if subscribed_person in self.direct_
> + if bug.personIsDir
> return 'subscribed-true %s' % dup_class
> else:
> return 'subscribed-false %s' % dup_class
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -569,7 +569,10 @@
> is_complete
> who_made_private
> date_made_private
> - userCanView"/>
> + userCanView
> + personIsDirectS
> + personIsAlsoNot
> + personIsSubscri
> <require
> permission=
> attributes="
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -1,10 +1,12 @@
> -= Bugs in Malone =
> +Bugs in Malone
> +==============
>
> This document describes what a Bug is in Malone, and provides some (currently
> rather incomplete) info on how to poke at bugs through the Component
> Architecture.
>
> -== Working with Bugs ==
> +Working with Bugs
> +-----------------
I didn't know we were definitely moving over to reST, but I assume you
know better because I suspect I'm a bit behind on that front.
>
> Bugs are created and retrieved via IBugSet.
>
> @@ -68,7 +70,8 @@
> >>> print result_
Graham Binns (gmb) wrote : | # |
On Wed, Nov 25, 2009 at 12:07:21PM -0000, Gavin Panella wrote:
> Review: Needs Information code
> Hi Graham,
>
> I have some comments on how to make two of the new methods a bit
> faster, but otherwise it all looks good.
>
> I'm still concerned that it's doing at least one query for every
> subscriber, rather than batching things up, but at least this branch
> should reduce the overhead a lot... actually, I've suddenly had a
> thought...
>
> BugViewMixin.
> properties, so the current implementation of subscription_class (if
> subscribed_person in self.xxx_
> worried that this branch may actually cause performance to degrade.
Ah, so I'd partially misunderstood what subscription_class does and also
managed to miss a step.
The problem isn't that subscription_class is inefficient per se. It
isn't - for large result sets. However, for a single call it's
massively, massively inefficient, and when the bug page is loaded that's
how many times it gets called - once. No more. It's called when the
portlet contents are set up, but that happens *after* the bug page has
rendered, via AJAX, so that's not a concern. But that one call is
bringing the bug page to its knees.
So, I've added a new @property, current_
uses the new methods to work on the subscription class for that one
call, and made the old subscription_class method into
getSubscription
first place; one of the things that confused me was that it was a PEP8
method name).
> Gavin.
>
>
> > === modified file 'lib/lp/
> > --- lib/lp/
> > +++ lib/lp/
> > @@ -437,12 +437,15 @@
> >
> > For example, "subscribed-false dup-subscribed-
> > """
> > - if subscribed_person in self.duplicate_
> > + bug = self.context
> > +
> > + if (bug.personIsSu
> > + bug.personIsAls
> > dup_class = 'dup-subscribed
>
> Now that we can distinguish between from-dupe and also-notified, there
> should probably be another CSS class to represent those users, even if
> it visually looks exactly the same as dup-subscribed-
> do it - I meant it as an observation - unless you've got very itchy
> fingers and your keyboard has a scratching board attached.
>
Nooooooo. And YAGNI, for the moment anyway.
> > === modified file 'lib/lp/
> > --- lib/lp/
> > +++ lib/lp/
> > @@ -1,10 +1,12 @@
> > -= Bugs in Malone =
> > +Bugs in Malone
> > +==============
> >
> > This document describes what a Bug is in Malone, and provides some (currently
> > rather incomplete) info on how to poke at bugs through the Component
> > Architecture.
> >
> > -== Working with Bugs ==
> > +Working with Bugs
> > +-----------------
>
> I didn't know we were definitely moving over to reST, but I ...
Gavin Panella (allenap) wrote : | # |
Ah, thanks for the explanation.
> +
> + # Re-fetch the bug so that the fact that it's a duplicate definitely
> + # registers.
> >>> bug = getUtility(
Although it saves little, out of curiosity I discovered that
IStore(
also works.
Gavin.
Preview Diff
1 | === modified file 'lib/lp/bugs/browser/bug.py' |
2 | --- lib/lp/bugs/browser/bug.py 2009-11-17 14:58:39 +0000 |
3 | +++ lib/lp/bugs/browser/bug.py 2009-11-25 15:15:29 +0000 |
4 | @@ -432,7 +432,7 @@ |
5 | ids[sub.name] = 'subscriber-%s' % sub.id |
6 | return ids |
7 | |
8 | - def subscription_class(self, subscribed_person): |
9 | + def getSubscriptionClassForUser(self, subscribed_person): |
10 | """Return a set of CSS class names based on subscription status. |
11 | |
12 | For example, "subscribed-false dup-subscribed-true". |
13 | @@ -447,6 +447,21 @@ |
14 | else: |
15 | return 'subscribed-false %s' % dup_class |
16 | |
17 | + @property |
18 | + def current_user_subscription_class(self): |
19 | + bug = self.context |
20 | + |
21 | + if (bug.personIsSubscribedToDuplicate(self.user) or |
22 | + bug.personIsAlsoNotifiedSubscriber(self.user)): |
23 | + dup_class = 'dup-subscribed-true' |
24 | + else: |
25 | + dup_class = 'dup-subscribed-false' |
26 | + |
27 | + if bug.personIsDirectSubscriber(self.user): |
28 | + return 'subscribed-true %s' % dup_class |
29 | + else: |
30 | + return 'subscribed-false %s' % dup_class |
31 | + |
32 | |
33 | class BugView(LaunchpadView, BugViewMixin): |
34 | """View class for presenting information about an `IBug`. |
35 | |
36 | === modified file 'lib/lp/bugs/configure.zcml' |
37 | --- lib/lp/bugs/configure.zcml 2009-11-20 04:21:24 +0000 |
38 | +++ lib/lp/bugs/configure.zcml 2009-11-25 15:15:29 +0000 |
39 | @@ -569,7 +569,10 @@ |
40 | is_complete |
41 | who_made_private |
42 | date_made_private |
43 | - userCanView"/> |
44 | + userCanView |
45 | + personIsDirectSubscriber |
46 | + personIsAlsoNotifiedSubscriber |
47 | + personIsSubscribedToDuplicate"/> |
48 | <require |
49 | permission="launchpad.View" |
50 | attributes=" |
51 | |
52 | === modified file 'lib/lp/bugs/doc/bug.txt' |
53 | --- lib/lp/bugs/doc/bug.txt 2009-10-26 17:11:03 +0000 |
54 | +++ lib/lp/bugs/doc/bug.txt 2009-11-25 15:15:29 +0000 |
55 | @@ -1,10 +1,12 @@ |
56 | -= Bugs in Malone = |
57 | +Bugs in Malone |
58 | +============== |
59 | |
60 | This document describes what a Bug is in Malone, and provides some (currently |
61 | rather incomplete) info on how to poke at bugs through the Component |
62 | Architecture. |
63 | |
64 | -== Working with Bugs == |
65 | +Working with Bugs |
66 | +----------------- |
67 | |
68 | Bugs are created and retrieved via IBugSet. |
69 | |
70 | @@ -68,7 +70,8 @@ |
71 | >>> print result_set.count() |
72 | 0 |
73 | |
74 | -== Bug creation events == |
75 | +Bug creation events |
76 | +------------------- |
77 | |
78 | IObjectCreatedEvent events are fired off when a bug is created. First |
79 | we will register a handler to observe the event. |
80 | @@ -155,7 +158,8 @@ |
81 | True |
82 | |
83 | |
84 | -== Interface check == |
85 | +Interface check |
86 | +--------------- |
87 | |
88 | It is guaranteed to implement the correct interface, too: |
89 | |
90 | @@ -166,7 +170,9 @@ |
91 | (We grab the object directly from the database here to avoid it being |
92 | security proxied, which doesn't make sense to test here.) |
93 | |
94 | -== Searching for Bugs == |
95 | + |
96 | +Searching for Bugs |
97 | +------------------ |
98 | |
99 | To search for bugs matching specific criteria, use IBugSet.searchAsUser: |
100 | |
101 | @@ -198,7 +204,9 @@ |
102 | |
103 | >>> login(ANONYMOUS) |
104 | |
105 | -== Absolute URLs == |
106 | + |
107 | +Absolute URLs |
108 | +------------- |
109 | |
110 | For things like bug notification emails, it's handy to be able to |
111 | include a URL to the bug inside the email. |
112 | @@ -207,7 +215,9 @@ |
113 | >>> print canonical_url(firefox_crashes) |
114 | http://.../bugs/6 |
115 | |
116 | -== Bug Privacy == |
117 | + |
118 | +Bug Privacy |
119 | +----------- |
120 | |
121 | A Bug has a "private" field. If Bug.private is False, the bug is |
122 | publicly visible. If Bug.private is True, only people who are directly |
123 | @@ -503,7 +513,8 @@ |
124 | [] |
125 | |
126 | |
127 | -== Prevent reporter from being subscribed to filed bugs == |
128 | +Prevent reporter from being subscribed to filed bugs |
129 | +---------------------------------------------------- |
130 | |
131 | If necessary, subscriber_reporter may be specified when creating a bug, |
132 | to prevent the reporter from being subscribed to the bug. This is useful |
133 | @@ -517,7 +528,8 @@ |
134 | [] |
135 | |
136 | |
137 | -== Date Last Updated == |
138 | +Date Last Updated |
139 | +----------------- |
140 | |
141 | Malone tracks the last time a change was made to a |
142 | bug. IBug.date_last_updated stores the date when anything is changed or |
143 | @@ -956,7 +968,8 @@ |
144 | True |
145 | |
146 | |
147 | -== Bug Completeness == |
148 | +Bug Completeness |
149 | +---------------- |
150 | |
151 | A bug is considered "complete" iff all of its bugtasks are themselves |
152 | complete. The definition of completeness for a bugtask is that the bug |
153 | @@ -987,7 +1000,8 @@ |
154 | mozilla-firefox (Debian) True |
155 | |
156 | |
157 | -== Bug Tasks == |
158 | +Bug Tasks |
159 | +--------- |
160 | |
161 | A bug can be targeted to more than one product, distribution, or source |
162 | package. A BugTask is used to represent a target, which has its own |
163 | @@ -1049,7 +1063,8 @@ |
164 | True |
165 | |
166 | |
167 | -== Bug Expiration == |
168 | +Bug Expiration |
169 | +-------------- |
170 | |
171 | Incomplete bug reports may expire when they become inactive. Expiration |
172 | is only available to projects that use Launchpad to track bugs. There |
173 | @@ -1128,7 +1143,8 @@ |
174 | that can or cannot expire. |
175 | |
176 | |
177 | -== Bug Comments == |
178 | +Bug Comments |
179 | +------------ |
180 | |
181 | A bug comment is actually made up of a number of chunks. The |
182 | IBug.getMessageChunks() method allows you to retreive these chunks in a |
183 | @@ -1166,7 +1182,8 @@ |
184 | 2 Strange bug with duplicate messages. Bug #2 in Tomcat: "Blackhole Trash folder" |
185 | |
186 | |
187 | -== Affected users == |
188 | +Affected users |
189 | +-------------- |
190 | |
191 | Users can mark bugs as affecting or not affecting them. For each bug we |
192 | then keep a count of the number of users affected by it, as well as the |
193 | @@ -1235,7 +1252,8 @@ |
194 | [<Person at ...>] |
195 | |
196 | |
197 | -== Getting the distinct set of Bugs for a set of BugTasks == |
198 | +Getting the distinct set of Bugs for a set of BugTasks |
199 | +------------------------------------------------------ |
200 | |
201 | Sometimes we have a set of BugTasks for which we want to get only the |
202 | distinct set of bugs, i.e. there are several BugTasks in our set which |
203 | @@ -1339,7 +1357,8 @@ |
204 | New bug 0 |
205 | |
206 | |
207 | -== Links to HWDB submissions == |
208 | +Links to HWDB submissions |
209 | +------------------------- |
210 | |
211 | We can link a HWDB submission to a bug, indicating that the |
212 | submission contains information that could help developers |
213 | @@ -1389,3 +1408,84 @@ |
214 | >>> test_bug.unlinkHWSubmission(submission) |
215 | >>> print test_bug.getHWSubmissions().count() |
216 | 0 |
217 | + |
218 | + |
219 | +Discovering subscription types |
220 | +------------------------------ |
221 | + |
222 | +It's possible to find out how a person is subscribed to a bug by calling |
223 | +the bug's personIsDirectSubscriber(), personIsAlsoNotifiedSubscriber() or |
224 | +personIsSubscribedToDuplicate() methods. |
225 | + |
226 | +If a person isn't subscribed to a bug, all of these methods will return |
227 | +False. |
228 | + |
229 | + >>> person = factory.makePerson() |
230 | + >>> bug = factory.makeBug() |
231 | + |
232 | + >>> bug.personIsDirectSubscriber(person) |
233 | + False |
234 | + |
235 | + >>> bug.personIsSubscribedToDuplicate(person) |
236 | + False |
237 | + |
238 | + >>> bug.personIsAlsoNotifiedSubscriber(person) |
239 | + False |
240 | + |
241 | +If our person subscribes to the bug they'll show up as a direct |
242 | +subscriber. |
243 | + |
244 | + >>> subscription = bug.subscribe(person, person) |
245 | + >>> bug.personIsDirectSubscriber(person) |
246 | + True |
247 | + |
248 | + >>> bug.personIsSubscribedToDuplicate(person) |
249 | + False |
250 | + |
251 | + >>> bug.personIsAlsoNotifiedSubscriber(person) |
252 | + False |
253 | + |
254 | +If the user subscribes to a duplicate of the bug, |
255 | +personIsSubscribedToDuplicate() will return True. |
256 | + |
257 | + >>> dupe = factory.makeBug() |
258 | + >>> subscription = dupe.subscribe(person, person) |
259 | + |
260 | + >>> dupe.duplicateof = bug |
261 | + |
262 | + # Re-fetch the bug so that the fact that it's a duplicate definitely |
263 | + # registers. |
264 | + >>> bug = getUtility(IBugSet).get(bug.id) |
265 | + >>> bug.personIsSubscribedToDuplicate(person) |
266 | + True |
267 | + |
268 | +personIsSubscribedToDuplicate() will return True regardless of |
269 | +the result of personIsDirectSubscriber(). personIsAlsoNotifiedSubscriber() |
270 | +will still return False. |
271 | + |
272 | + >>> bug.personIsDirectSubscriber(person) |
273 | + True |
274 | + |
275 | + >>> bug.personIsAlsoNotifiedSubscriber(person) |
276 | + False |
277 | + |
278 | +If the user is subscribed to the bug for a reason other than a direct |
279 | +BugSubscription or a subscription to a duplicate bug, |
280 | +personIsAlsoNotifiedSubscriber() will return True, for example if the |
281 | +user is the assignee for one of the bug's BugTask. |
282 | + |
283 | + >>> new_bug = factory.makeBug() |
284 | + >>> new_bug.default_bugtask.transitionToAssignee(person) |
285 | + >>> new_bug.personIsAlsoNotifiedSubscriber(person) |
286 | + True |
287 | + |
288 | +If the person subscribes directly to the bug, |
289 | +personIsAlsoNotifiedSubscriber() will return False, since direct |
290 | +subscriptions always override indirect ones. |
291 | + |
292 | + >>> subscription = new_bug.subscribe(person, person) |
293 | + >>> new_bug.personIsAlsoNotifiedSubscriber(person) |
294 | + False |
295 | + |
296 | + >>> new_bug.personIsDirectSubscriber(person) |
297 | + True |
298 | |
299 | === modified file 'lib/lp/bugs/interfaces/bug.py' |
300 | --- lib/lp/bugs/interfaces/bug.py 2009-11-18 23:20:49 +0000 |
301 | +++ lib/lp/bugs/interfaces/bug.py 2009-11-25 15:15:30 +0000 |
302 | @@ -945,6 +945,24 @@ |
303 | return Bugs. |
304 | """ |
305 | |
306 | + def personIsDirectSubscriber(person): |
307 | + """Return True if the person is a direct subscriber to this `IBug`. |
308 | + |
309 | + Otherwise, return False. |
310 | + """ |
311 | + |
312 | + def personIsAlsoNotifiedSubscriber(person): |
313 | + """Return True if the person is an indirect subscriber to this `IBug`. |
314 | + |
315 | + Otherwise, return False. |
316 | + """ |
317 | + |
318 | + def personIsSubscribedToDuplicate(person): |
319 | + """Return True if the person subscribed to a duplicate of this `IBug`. |
320 | + |
321 | + Otherwise, return False. |
322 | + """ |
323 | + |
324 | |
325 | class InvalidBugTargetType(Exception): |
326 | """Bug target's type is not valid.""" |
327 | |
328 | === modified file 'lib/lp/bugs/model/bug.py' |
329 | --- lib/lp/bugs/model/bug.py 2009-10-26 17:00:08 +0000 |
330 | +++ lib/lp/bugs/model/bug.py 2009-11-25 15:15:29 +0000 |
331 | @@ -1389,6 +1389,38 @@ |
332 | """See `IBug`.""" |
333 | return getUtility(IHWSubmissionBugSet).submissionsForBug(self, user) |
334 | |
335 | + def personIsDirectSubscriber(self, person): |
336 | + """See `IBug`.""" |
337 | + store = Store.of(self) |
338 | + subscriptions = store.find( |
339 | + BugSubscription, |
340 | + BugSubscription.bug == self, |
341 | + BugSubscription.person == person) |
342 | + |
343 | + return not subscriptions.is_empty() |
344 | + |
345 | + def personIsAlsoNotifiedSubscriber(self, person): |
346 | + """See `IBug`.""" |
347 | + # We have to use getAlsoNotifiedSubscribers() here and iterate |
348 | + # over what it returns because "also notified subscribers" is |
349 | + # actually a composite of bug contacts, structural subscribers |
350 | + # and assignees. As such, it's not possible to get them all with |
351 | + # one query. |
352 | + also_notified_subscribers = self.getAlsoNotifiedSubscribers() |
353 | + |
354 | + return person in also_notified_subscribers |
355 | + |
356 | + def personIsSubscribedToDuplicate(self, person): |
357 | + """See `IBug`.""" |
358 | + store = Store.of(self) |
359 | + subscriptions_from_dupes = store.find( |
360 | + BugSubscription, |
361 | + Bug.duplicateof == self, |
362 | + BugSubscription.bugID == Bug.id, |
363 | + BugSubscription.person == person) |
364 | + |
365 | + return not subscriptions_from_dupes.is_empty() |
366 | + |
367 | |
368 | class BugSet: |
369 | """See BugSet.""" |
370 | |
371 | === modified file 'lib/lp/bugs/templates/bug-portlet-subscribers-content.pt' |
372 | --- lib/lp/bugs/templates/bug-portlet-subscribers-content.pt 2009-11-05 19:01:12 +0000 |
373 | +++ lib/lp/bugs/templates/bug-portlet-subscribers-content.pt 2009-11-25 15:15:29 +0000 |
374 | @@ -35,7 +35,7 @@ |
375 | tal:attributes=" |
376 | title string:Unsubscribe ${subscription/person/fmt:displayname}; |
377 | id string:unsubscribe-${subscription/css_name}; |
378 | - class python: view.subscription_class(subscription.person) |
379 | + class python: view.getSubscriptionClassForUser(subscription.person) |
380 | " |
381 | > |
382 | <img class="unsub-icon" src="/@@/remove" |
383 | |
384 | === modified file 'lib/lp/bugs/templates/bug-portlet-subscribers.pt' |
385 | --- lib/lp/bugs/templates/bug-portlet-subscribers.pt 2009-11-17 16:00:21 +0000 |
386 | +++ lib/lp/bugs/templates/bug-portlet-subscribers.pt 2009-11-25 15:15:29 +0000 |
387 | @@ -9,7 +9,7 @@ |
388 | <div class="section" tal:define="context_menu context/menu:context" |
389 | metal:define-slot="heading"> |
390 | <div |
391 | - tal:attributes="class python: view.subscription_class(view.user)" |
392 | + tal:attributes="class view/current_user_subscription_class" |
393 | tal:content="structure context_menu/subscription/render" /> |
394 | <div id="sub-unsub-spinner">Subscribing...</div> |
395 | <div tal:content="structure context_menu/addsubscriber/render" /> |
This branch fixes bug 487015 by adding methods to IBug that allow us to
check what kind of subscription (if any) a Person has to a bug using DB
queries rather than getting the list of subscribers and iterating over
it.
I've added three methods:
- personIsDirectS ubscriber( ): returns True only if the Person has an bedToDuplicate( ): returns True only if the Person has ifiedSubscriber (): returns True if the Person doesn't
explicit BugSubscription for the current bug.
- personIsSubscri
a BugSubscription to one of a bug's duplicates.
- personIsAlsoNot
have a BugSubscription for a bug but will receive bugmail anyway
(assignees, contacts and structural subscribers fall into this
category).
Note that personIsAlsoNot ifiedSubscriber () and personIsDirectS ubscriber( ) ubscriber( ) and bedToDuplicate( ) are not.
are mutually exclusive but personIsDirectS
personIsSubscri
For personIsDirectS ubscriber( ) and personIsSubscri bedToDuplicate( ) I've ifiedSubscriber () because "also notified
used Storm to create the necessary DB queries. However, I couldn't do
this for personIsAlsoNot
subscribers" come from a mish-mash of sources. I don't think this is a
problem (at least not right now) because there are usually less
"also notified" subscribers than there are subscribers from duplicates,
and the timeouts that raised this bug in the first place are due to the
number of duplicate subscribers and the amount of iteration we do over
that set.
I've updated BugViewMixin. subscription_ class, which was where the
timeout problem identified in bug 487015 originated, to use the new
methods when determining what CSS class to return for a subscription.
Hopefully this will speed matters up somewhat.
I've also done a drive-by conversion of bug.txt to ReST.
NOTE: `make lint` shows a lot of "Operator is not preceded by a space"
errors, which look like crack to me and I'm not sure what to do about
them. Any ideas?
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files: bugs/configure. zcml bugs/browser/ bug.py bugs/doc/ bug.txt bugs/interfaces /bug.py bugs/model/ bug.py
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
== Pylint notices ==
lib/lp/ bugs/browser/ bug.py MIMEMultipart' (No module named MIMEMultipart) .event' (No module named lifecycle) .snapshot' (No module named lifecycle) interfaces' (No module named restful)
28: [F0401] Unable to import 'email.
29: [F0401] Unable to import 'email.MIMEText' (No module named MIMEText)
43: [F0401] Unable to import 'lazr.enum' (No module named enum)
44: [F0401] Unable to import 'lazr.lifecycle
45: [F0401] Unable to import 'lazr.lifecycle
46: [F0401] Unable to import 'lazr.restful.
lib/lp/ bugs/interfaces /bug.py declarations' (No module named restful) fields' (No module named restful) interface' (No module named restful) TextLine( ), is_patch=Bool(),
49: [F0401] Unable to import 'lazr.restful.
55: [F0401] Unable to import 'lazr.restful.
56: [F0401] Unable to import 'lazr.restful.
466: [C0322, IBug.addAttachment] Operator not preceded by a space
comment=Text(), filename=
^
co...