Merge lp:~savoirfairelinux-openerp/lp-community-utils/nag_refactor into lp:lp-community-utils
- nag_refactor
- Merge into openerp-reviewers-nag
Status: | Superseded |
---|---|
Proposed branch: | lp:~savoirfairelinux-openerp/lp-community-utils/nag_refactor |
Merge into: | lp:lp-community-utils |
Diff against target: |
508 lines (+254/-223) 2 files modified
lp.py (+252/-0) openerp-nag (+2/-223) |
To merge this branch: | bzr merge lp:~savoirfairelinux-openerp/lp-community-utils/nag_refactor |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Guewen Baconnier @ Camptocamp | code review | Needs Fixing | |
Review via email: mp+205257@code.launchpad.net |
This proposal has been superseded by a proposal from 2014-04-06.
Commit message
Description of the change
Cleaned up code of nag
* Put branch analysis code in nag_lib
* Changed Nag to a class
* Removed unused parameters
* Fixed sign/sort issue with age
* General readability improvements
This will allow for other scripts that the general nag script to use some shared functionality
- 24. By Sandy Carter (http://www.savoirfairelinux.com)
-
Adds handy progressbar if progressbar library is available
- 25. By Guewen Baconnier @ Camptocamp
-
[MRG] Display the votes of the reviews in the nag list, display a different sentence for the proposals passing the merging rules
Guewen Baconnier @ Camptocamp (gbaconnier-c2c) wrote : | # |
Sandy Carter (http://www.savoirfairelinux.com) (sandy-carter) wrote : | # |
@gbaconnier-c2c
Good point about adding functions to Nag, the reason for turning it into a class is design and optimization.
It gives the option, like to said, to add functions but also, the ability to inherit from it.
I do have big plans with the lib, for example, automatic diff-validating in the style of (https:/
I have proposed something already: https:/
As for the --authenticated option, I think I took it away because it wasn't referenced or used, I can put it back now, however I will have no impact. This was a while ago, though, I could have just taken it out accidentally.
The problem with age was that it declared it negatively, then did * -1 again while sorting. I just took out the (-1 * -1 = 1) redundancy. It also has a one off error where an MP from today is displayed as 1 day old.
Guewen Baconnier @ Camptocamp (gbaconnier-c2c) wrote : | # |
> On 03/07/2014 07:45 PM, Sandy Carter (http://
> wrote:
> @gbaconnier-c2c
> Good point about adding functions to Nag, the reason for turning it
> into a class is design and optimization.
The rationale about design is very subjective and the one about
optimization is dubious:
http://
Anyway, if it needs methods, and I think the display should be one, then
it makes sense.
> It gives the option, like to said, to add functions but also, the
> ability to inherit from it.
>
> I do have big plans with the lib, for example, automatic
> diff-validating in the style of
> (https:/
> specifically checking style, seeing if the diff introduces pep8
> errors, removes them, testing the patches with jenkins, commenting on
> the MPs, etc.
> I have proposed something already:
> https:/
>
>
> As for the --authenticated option, I think I took it away because it
> wasn't referenced or used, I can put it back now, however I will have
> no impact. This was a while ago, though, I could have just taken it
> out accidentally.
That's right at the time you made the proposal, the option seemed to be
not useful.
>
> The problem with age was that it declared it negatively, then did * -1
> again while sorting. I just took out the (-1 * -1 = 1) redundancy. It
> also has a one off error where an MP from today is displayed as 1 day
> old.
Ok.
Sandy Carter (http://www.savoirfairelinux.com) (sandy-carter) wrote : | # |
> > On 03/07/2014 07:45 PM, Sandy Carter (http://
> > wrote:
> > @gbaconnier-c2c
> > Good point about adding functions to Nag, the reason for turning it
> > into a class is design and optimization.
> The rationale about design is very subjective and the one about
> optimization is dubious:
>
> http://
Declaring a class puts members together in memory and reduces the rate of cache misses, you can see this when performing lookups, but mostly when doing operations.
Try this: http://
That's close to 15% increase in speed for summing members.
>
> Anyway, if it needs methods, and I think the display should be one, then
> it makes sense.
>
I agree
> > It gives the option, like to said, to add functions but also, the
> > ability to inherit from it.
> >
> > I do have big plans with the lib, for example, automatic
> > diff-validating in the style of
> > (https:/
> > specifically checking style, seeing if the diff introduces pep8
> > errors, removes them, testing the patches with jenkins, commenting on
> > the MPs, etc.
> > I have proposed something already:
> > https:/
> utils/branch_
> >
> >
> > As for the --authenticated option, I think I took it away because it
> > wasn't referenced or used, I can put it back now, however I will have
> > no impact. This was a while ago, though, I could have just taken it
> > out accidentally.
>
> That's right at the time you made the proposal, the option seemed to be
> not useful.
>
> >
> > The problem with age was that it declared it negatively, then did * -1
> > again while sorting. I just took out the (-1 * -1 = 1) redundancy. It
> > also has a one off error where an MP from today is displayed as 1 day
> > old.
>
> Ok.
Guewen Baconnier @ Camptocamp (gbaconnier-c2c) wrote : | # |
On 03/10/2014 02:12 PM, Sandy Carter (http://
wrote:
>> > On 03/07/2014 07:45 PM, Sandy Carter (http://
>>> wrote:
>>> @gbaconnier-c2c
>>> Good point about adding functions to Nag, the reason for turning it
>>> into a class is design and optimization.
>> The rationale about design is very subjective and the one about
>> optimization is dubious:
>>
>> http://
>
> Declaring a class puts members together in memory and reduces the rate of cache misses, you can see this when performing lookups, but mostly when doing operations.
> Try this: http://
> That's close to 15% increase in speed for summing members.
>
Enlightening, thanks for the explanation.
- 26. By Sandy Carter (http://www.savoirfairelinux.com)
-
[IMP] PEP 394 (partial) invoke env instead of system python
- 27. By Yannick Vaucher @ Camptocamp
-
add multi-company project
- 28. By Sandy Carter (http://www.savoirfairelinux.com)
-
[MRG] A tool I use to quickly check pep8 on MPs.
A few people have asked what tool I use, so I though I would put it up here.
The command I use is:
./checkout-flake8. sh lp:launchpad_openerp_repo module_name
for example:
./checkout-flake8. sh lp:~savoirfairelinux-openerp/partner-contact-management/user-firstname/+merge/210710 partner_firstname It checks it out in /tmp and runs flake8 on the module
Sandy Carter (http://www.savoirfairelinux.com) (sandy-carter) wrote : | # |
@Guewen
> Couldn't already the module be used as a lib? (as there is if __name__ ==
> "__main__":) If the filename is an issue (it is, i we want to import it),
> maybe we can consider to rename it?
The problem right now is that you can't import from it due to its filename (dash in filename and no .py)
- 29. By Sandy Carter (http://www.savoirfairelinux.com)
-
Moved common code out of openerp-nag into lp.py so it can be used by other scripts
Unmerged revisions
- 29. By Sandy Carter (http://www.savoirfairelinux.com)
-
Moved common code out of openerp-nag into lp.py so it can be used by other scripts
Preview Diff
1 | === added file 'lp.py' | |||
2 | --- lp.py 1970-01-01 00:00:00 +0000 | |||
3 | +++ lp.py 2014-04-06 16:36:47 +0000 | |||
4 | @@ -0,0 +1,252 @@ | |||
5 | 1 | # Copyright 2012 Canonical Ltd. | ||
6 | 2 | # Written by: | ||
7 | 3 | # Zygmunt Krynicki <zygmunt.krynicki@canonical.com> | ||
8 | 4 | # Hacked for the OpenERP Community Reviewers version by: | ||
9 | 5 | # Guewen Baconnier <guewen.baconnier@camptocamp.com> | ||
10 | 6 | # | ||
11 | 7 | # This program is free software: you can redistribute it and/or modify | ||
12 | 8 | # it under the terms of the GNU General Public License version 3, | ||
13 | 9 | # as published by the Free Software Foundation. | ||
14 | 10 | # | ||
15 | 11 | # This program is distributed in the hope that it will be useful, | ||
16 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
17 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
18 | 14 | # GNU General Public License for more details. | ||
19 | 15 | # | ||
20 | 16 | # You should have received a copy of the GNU General Public License | ||
21 | 17 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
22 | 18 | |||
23 | 19 | |||
24 | 20 | from __future__ import unicode_literals, print_function | ||
25 | 21 | |||
26 | 22 | import collections | ||
27 | 23 | import datetime | ||
28 | 24 | import logging | ||
29 | 25 | |||
30 | 26 | try: | ||
31 | 27 | from progressbar import ProgressBar, Bar, Percentage, ETA | ||
32 | 28 | except ImportError: | ||
33 | 29 | ProgressBar = None | ||
34 | 30 | |||
35 | 31 | from launchpadlib.launchpad import Launchpad | ||
36 | 32 | |||
37 | 33 | |||
38 | 34 | # Nag: | ||
39 | 35 | # Indication that we want to nag the 'person'... | ||
40 | 36 | # ...with the specified 'action' | ||
41 | 37 | # ...about a particular 'subject' | ||
42 | 38 | Nag = collections.namedtuple( | ||
43 | 39 | "Nag", "person action subject sort_class sort_priority sort_age project_name votes") | ||
44 | 40 | |||
45 | 41 | SORT_CLASS_BUG, SORT_CLASS_MERGE = range(2) | ||
46 | 42 | |||
47 | 43 | |||
48 | 44 | def show_lp_object(obj): | ||
49 | 45 | print(obj) | ||
50 | 46 | print("attributes:", obj.lp_attributes) | ||
51 | 47 | for attr in obj.lp_attributes: | ||
52 | 48 | print("attribute[%s]: %s" % (attr, getattr(obj, attr))) | ||
53 | 49 | print("entries:", obj.lp_entries) | ||
54 | 50 | for entry in obj.lp_entries: | ||
55 | 51 | print("entry[%s]: %s" % (entry, getattr(obj, entry))) | ||
56 | 52 | print("collections:", obj.lp_collections) | ||
57 | 53 | print("operations:", obj.lp_operations) | ||
58 | 54 | print("-" * 80) | ||
59 | 55 | |||
60 | 56 | |||
61 | 57 | class DefaultPolicy: | ||
62 | 58 | |||
63 | 59 | mps_need_commit_message = False | ||
64 | 60 | |||
65 | 61 | max_review_age = 0 | ||
66 | 62 | |||
67 | 63 | max_bug_new_age = 7 | ||
68 | 64 | |||
69 | 65 | # minimal number of reviewers before merging | ||
70 | 66 | min_approve = 2 | ||
71 | 67 | |||
72 | 68 | # can't be merged before n days | ||
73 | 69 | days_before_merge = 5 | ||
74 | 70 | |||
75 | 71 | # can be merged before days_before_merge if at least n approvals | ||
76 | 72 | approvals_to_bypass = 3 | ||
77 | 73 | |||
78 | 74 | |||
79 | 75 | class UTC(datetime.tzinfo): | ||
80 | 76 | |||
81 | 77 | def dst(self, foo): | ||
82 | 78 | return datetime.timedelta(0, 0) | ||
83 | 79 | |||
84 | 80 | def utcoffset(self, foo): | ||
85 | 81 | return datetime.timedelta(0, 0) | ||
86 | 82 | |||
87 | 83 | |||
88 | 84 | UTC = UTC() | ||
89 | 85 | |||
90 | 86 | |||
91 | 87 | class Votes(object): | ||
92 | 88 | """ Holds votes of a proposal """ | ||
93 | 89 | |||
94 | 90 | __states__ = { | ||
95 | 91 | 'Approve': 'approve', | ||
96 | 92 | 'Needs Fixing': 'needs_fixing', | ||
97 | 93 | 'Needs Information': 'needs_information', | ||
98 | 94 | 'Abstain': 'abstain', | ||
99 | 95 | 'Disapprove': 'disapprove', | ||
100 | 96 | 'Resubmit': 'resubmit', | ||
101 | 97 | 'Pending': 'pending', | ||
102 | 98 | } | ||
103 | 99 | |||
104 | 100 | __signs__ = { | ||
105 | 101 | 'approve': '+', | ||
106 | 102 | 'needs_fixing': '!', | ||
107 | 103 | 'needs_information': '?', | ||
108 | 104 | 'abstain': '~', | ||
109 | 105 | 'disapprove': '-', | ||
110 | 106 | 'resubmit': 'R', | ||
111 | 107 | 'pending': '*', | ||
112 | 108 | } | ||
113 | 109 | |||
114 | 110 | __ignore_for_approval__ = ['abstain', 'pending'] | ||
115 | 111 | |||
116 | 112 | def __init__(self, votes): | ||
117 | 113 | self._votes = collections.Counter( | ||
118 | 114 | self.__states__[vote.comment.vote if vote.comment else 'Pending'] | ||
119 | 115 | for vote in votes | ||
120 | 116 | ) # no comment is a pending review, usually the team | ||
121 | 117 | |||
122 | 118 | def __getattr__(self, name): | ||
123 | 119 | if name in self._votes: | ||
124 | 120 | return self._votes[name] | ||
125 | 121 | elif name in self.__states__.values(): | ||
126 | 122 | return 0 | ||
127 | 123 | else: | ||
128 | 124 | raise AttributeError | ||
129 | 125 | |||
130 | 126 | def __str__(self): | ||
131 | 127 | signs = ['%d%s' % (count, self.__signs__[state]) for | ||
132 | 128 | state, count in self._votes.iteritems()] | ||
133 | 129 | return ', '.join(signs) | ||
134 | 130 | |||
135 | 131 | def total(self, for_approval=False): | ||
136 | 132 | if for_approval: | ||
137 | 133 | return sum(count for state, count in self._votes.iteritems() | ||
138 | 134 | if state not in self.__ignore_for_approval__) | ||
139 | 135 | return sum(self._votes.values()) | ||
140 | 136 | |||
141 | 137 | @classmethod | ||
142 | 138 | def legend(cls): | ||
143 | 139 | return dict((state, cls.__signs__[code]) for | ||
144 | 140 | state, code in cls.__states__.iteritems()) | ||
145 | 141 | |||
146 | 142 | |||
147 | 143 | def gen_project_nags(lp, policy, project_name): | ||
148 | 144 | # TODO: Detect project groups and redirect the nag to all projects | ||
149 | 145 | # underneath that project | ||
150 | 146 | # Access the project that we care about | ||
151 | 147 | logging.debug("Accessing project %r", project_name) | ||
152 | 148 | project = lp.projects[project_name] | ||
153 | 149 | # Re-yield all the merge proposal nags | ||
154 | 150 | # NOTE: change to yield from in py3k3 | ||
155 | 151 | for nag in gen_merge_proposal_nags(lp, policy, project): | ||
156 | 152 | yield nag | ||
157 | 153 | |||
158 | 154 | |||
159 | 155 | def gen_merge_proposal_nags(lp, policy, project): | ||
160 | 156 | # XXX: cannnot use utcnow as launchpad returns tz-aware objects here | ||
161 | 157 | now = datetime.datetime.now(tz=UTC) | ||
162 | 158 | logging.debug("Looking for merge proposals in project %r", project) | ||
163 | 159 | # NOTE: Workaround for project.getMergeProposals() crashing on timeouts | ||
164 | 160 | try: | ||
165 | 161 | merge_proposals = project.getMergeProposals(status='Needs review') | ||
166 | 162 | except AttributeError: | ||
167 | 163 | logging.warning("The project %s has no code to look at", project) | ||
168 | 164 | return | ||
169 | 165 | for proposal in merge_proposals: | ||
170 | 166 | logging.debug("Looking at merge proposal %r", proposal) | ||
171 | 167 | # Skip everything that is still not requested for review | ||
172 | 168 | if proposal.date_review_requested is None: | ||
173 | 169 | continue | ||
174 | 170 | # Skip everything that is already merged | ||
175 | 171 | if proposal.date_merged is not None: | ||
176 | 172 | continue | ||
177 | 173 | votes = Votes(proposal.votes) | ||
178 | 174 | |||
179 | 175 | # Nag about missing commit message on merge requests | ||
180 | 176 | if policy.mps_need_commit_message and proposal.commit_message is None: | ||
181 | 177 | yield Nag( | ||
182 | 178 | person=proposal.registrant.display_name, | ||
183 | 179 | action="set a commit message on merge request", | ||
184 | 180 | subject=proposal.web_link, | ||
185 | 181 | sort_class=SORT_CLASS_MERGE, | ||
186 | 182 | sort_priority=None, # TODO: get from max(linked bugs) | ||
187 | 183 | sort_age=(proposal.date_review_requested - now).days, | ||
188 | 184 | project_name=project.name, | ||
189 | 185 | votes=votes, | ||
190 | 186 | ) | ||
191 | 187 | |||
192 | 188 | age = (now - proposal.date_review_requested).days | ||
193 | 189 | |||
194 | 190 | if (votes.approve == votes.total(for_approval=True) and | ||
195 | 191 | (votes.approve >= policy.approvals_to_bypass or | ||
196 | 192 | votes.approve >= policy.min_approve and | ||
197 | 193 | age >= policy.days_before_merge)): | ||
198 | 194 | yield Nag( | ||
199 | 195 | person='A committer', | ||
200 | 196 | action="consider to merge the proposal", | ||
201 | 197 | subject=proposal.web_link, | ||
202 | 198 | sort_class=SORT_CLASS_MERGE, | ||
203 | 199 | sort_priority=None, # TODO: get from max(linked bugs) | ||
204 | 200 | sort_age=(proposal.date_review_requested - now).days, | ||
205 | 201 | project_name=project.name, | ||
206 | 202 | votes=votes, | ||
207 | 203 | ) | ||
208 | 204 | continue | ||
209 | 205 | |||
210 | 206 | # Nag about aging merge requests | ||
211 | 207 | if age >= policy.max_review_age: | ||
212 | 208 | yield Nag( | ||
213 | 209 | person='Someone', | ||
214 | 210 | action="review the merge request", | ||
215 | 211 | subject=proposal.web_link, | ||
216 | 212 | sort_class=SORT_CLASS_MERGE, | ||
217 | 213 | sort_priority=None, # TODO: get from max(linked bugs) | ||
218 | 214 | sort_age=(proposal.date_review_requested - now).days, | ||
219 | 215 | project_name=project.name, | ||
220 | 216 | votes=votes, | ||
221 | 217 | ) | ||
222 | 218 | |||
223 | 219 | |||
224 | 220 | def gen_bug_nags(lp, policy, project): | ||
225 | 221 | now = datetime.datetime.now(tz=UTC) | ||
226 | 222 | new_threshold = now - datetime.timedelta(days=policy.max_bug_new_age) | ||
227 | 223 | # Nag about bugs that are "New" for too long | ||
228 | 224 | logging.debug("Looking at 'new' bugs that are 8 days or older") | ||
229 | 225 | aging_new_bugs = project.searchTasks(status='New', | ||
230 | 226 | created_before=new_threshold) | ||
231 | 227 | for bug_task in aging_new_bugs: | ||
232 | 228 | yield Nag( | ||
233 | 229 | person='everyone', | ||
234 | 230 | action='triage the bug', | ||
235 | 231 | subject=bug_task.web_link, | ||
236 | 232 | sort_class=SORT_CLASS_BUG, | ||
237 | 233 | sort_priority=None, # TODO: convert from importance name | ||
238 | 234 | sort_age=None, | ||
239 | 235 | votes=None) | ||
240 | 236 | |||
241 | 237 | |||
242 | 238 | def parse_projects_file(filename): | ||
243 | 239 | """ Parse a file containing name of the projects | ||
244 | 240 | A line per project | ||
245 | 241 | """ | ||
246 | 242 | with open(filename, mode='rU') as project_file: | ||
247 | 243 | content = project_file.read() | ||
248 | 244 | |||
249 | 245 | return [name.strip() for name in content.splitlines()] | ||
250 | 246 | |||
251 | 247 | |||
252 | 248 | def launchpad_login(anonymous, consumer_name, service_root): | ||
253 | 249 | if anonymous: | ||
254 | 250 | return Launchpad.login_anonymously(consumer_name, service_root) | ||
255 | 251 | else: | ||
256 | 252 | return Launchpad.login_with(consumer_name, service_root) | ||
257 | 0 | 253 | ||
258 | === modified file 'openerp-nag' | |||
259 | --- openerp-nag 2014-02-20 16:18:06 +0000 | |||
260 | +++ openerp-nag 2014-04-06 16:36:47 +0000 | |||
261 | @@ -42,234 +42,16 @@ | |||
262 | 42 | from __future__ import unicode_literals, print_function | 42 | from __future__ import unicode_literals, print_function |
263 | 43 | 43 | ||
264 | 44 | import argparse | 44 | import argparse |
265 | 45 | import collections | ||
266 | 46 | import datetime | ||
267 | 47 | import logging | ||
268 | 48 | 45 | ||
269 | 49 | try: | 46 | try: |
270 | 50 | from progressbar import ProgressBar, Bar, Percentage, ETA | 47 | from progressbar import ProgressBar, Bar, Percentage, ETA |
271 | 51 | except ImportError: | 48 | except ImportError: |
272 | 52 | ProgressBar = None | 49 | ProgressBar = None |
275 | 53 | from launchpadlib.launchpad import Launchpad | 50 | from lp import * |
274 | 54 | |||
276 | 55 | 51 | ||
277 | 56 | consumer_name = 'OpenERP Community Reviewers Nagging Scripts' | 52 | consumer_name = 'OpenERP Community Reviewers Nagging Scripts' |
278 | 57 | 53 | ||
279 | 58 | 54 | ||
280 | 59 | # Nag: | ||
281 | 60 | # Indication that we want to nag the 'person'... | ||
282 | 61 | # ...with the specified 'action' | ||
283 | 62 | # ...about a particular 'subject' | ||
284 | 63 | Nag = collections.namedtuple( | ||
285 | 64 | "Nag", "person action subject sort_class sort_priority sort_age project_name votes") | ||
286 | 65 | |||
287 | 66 | SORT_CLASS_BUG, SORT_CLASS_MERGE = range(2) | ||
288 | 67 | |||
289 | 68 | |||
290 | 69 | def show_lp_object(obj): | ||
291 | 70 | print(obj) | ||
292 | 71 | print("attributes:", obj.lp_attributes) | ||
293 | 72 | for attr in obj.lp_attributes: | ||
294 | 73 | print("attribute[%s]: %s" % (attr, getattr(obj, attr))) | ||
295 | 74 | print("entries:", obj.lp_entries) | ||
296 | 75 | for entry in obj.lp_entries: | ||
297 | 76 | print("entry[%s]: %s" % (entry, getattr(obj, entry))) | ||
298 | 77 | print("collections:", obj.lp_collections) | ||
299 | 78 | print("operations:", obj.lp_operations) | ||
300 | 79 | print("-" * 80) | ||
301 | 80 | |||
302 | 81 | |||
303 | 82 | class DefaultPolicy: | ||
304 | 83 | |||
305 | 84 | mps_need_commit_message = False | ||
306 | 85 | |||
307 | 86 | max_review_age = 0 | ||
308 | 87 | |||
309 | 88 | max_bug_new_age = 7 | ||
310 | 89 | |||
311 | 90 | # minimal number of reviewers before merging | ||
312 | 91 | min_approve = 2 | ||
313 | 92 | |||
314 | 93 | # can't be merged before n days | ||
315 | 94 | days_before_merge = 5 | ||
316 | 95 | |||
317 | 96 | # can be merged before days_before_merge if at least n approvals | ||
318 | 97 | approvals_to_bypass = 3 | ||
319 | 98 | |||
320 | 99 | |||
321 | 100 | class UTC(datetime.tzinfo): | ||
322 | 101 | |||
323 | 102 | def dst(self, foo): | ||
324 | 103 | return datetime.timedelta(0, 0) | ||
325 | 104 | |||
326 | 105 | def utcoffset(self, foo): | ||
327 | 106 | return datetime.timedelta(0, 0) | ||
328 | 107 | |||
329 | 108 | |||
330 | 109 | UTC = UTC() | ||
331 | 110 | |||
332 | 111 | |||
333 | 112 | class Votes(object): | ||
334 | 113 | """ Holds votes of a proposal """ | ||
335 | 114 | |||
336 | 115 | __states__ = { | ||
337 | 116 | 'Approve': 'approve', | ||
338 | 117 | 'Needs Fixing': 'needs_fixing', | ||
339 | 118 | 'Needs Information': 'needs_information', | ||
340 | 119 | 'Abstain': 'abstain', | ||
341 | 120 | 'Disapprove': 'disapprove', | ||
342 | 121 | 'Resubmit': 'resubmit', | ||
343 | 122 | 'Pending': 'pending', | ||
344 | 123 | } | ||
345 | 124 | |||
346 | 125 | __signs__ = { | ||
347 | 126 | 'approve': '+', | ||
348 | 127 | 'needs_fixing': '!', | ||
349 | 128 | 'needs_information': '?', | ||
350 | 129 | 'abstain': '~', | ||
351 | 130 | 'disapprove': '-', | ||
352 | 131 | 'resubmit': 'R', | ||
353 | 132 | 'pending': '*', | ||
354 | 133 | } | ||
355 | 134 | |||
356 | 135 | __ignore_for_approval__ = ['abstain', 'pending'] | ||
357 | 136 | |||
358 | 137 | def __init__(self, votes): | ||
359 | 138 | self._votes = collections.Counter( | ||
360 | 139 | self.__states__[vote.comment.vote if vote.comment else 'Pending'] | ||
361 | 140 | for vote in votes | ||
362 | 141 | ) # no comment is a pending review, usually the team | ||
363 | 142 | |||
364 | 143 | def __getattr__(self, name): | ||
365 | 144 | if name in self._votes: | ||
366 | 145 | return self._votes[name] | ||
367 | 146 | elif name in self.__states__.values(): | ||
368 | 147 | return 0 | ||
369 | 148 | else: | ||
370 | 149 | raise AttributeError | ||
371 | 150 | |||
372 | 151 | def __str__(self): | ||
373 | 152 | signs = ['%d%s' % (count, self.__signs__[state]) for | ||
374 | 153 | state, count in self._votes.iteritems()] | ||
375 | 154 | return ', '.join(signs) | ||
376 | 155 | |||
377 | 156 | def total(self, for_approval=False): | ||
378 | 157 | if for_approval: | ||
379 | 158 | return sum(count for state, count in self._votes.iteritems() | ||
380 | 159 | if state not in self.__ignore_for_approval__) | ||
381 | 160 | return sum(self._votes.values()) | ||
382 | 161 | |||
383 | 162 | @classmethod | ||
384 | 163 | def legend(cls): | ||
385 | 164 | return dict((state, cls.__signs__[code]) for | ||
386 | 165 | state, code in cls.__states__.iteritems()) | ||
387 | 166 | |||
388 | 167 | |||
389 | 168 | def gen_project_nags(lp, policy, project_name): | ||
390 | 169 | # TODO: Detect project groups and redirect the nag to all projects | ||
391 | 170 | # underneath that project | ||
392 | 171 | # Access the project that we care about | ||
393 | 172 | logging.debug("Accessing project %r", project_name) | ||
394 | 173 | project = lp.projects[project_name] | ||
395 | 174 | # Re-yield all the merge proposal nags | ||
396 | 175 | # NOTE: change to yield from in py3k3 | ||
397 | 176 | for nag in gen_merge_proposal_nags(lp, policy, project): | ||
398 | 177 | yield nag | ||
399 | 178 | |||
400 | 179 | |||
401 | 180 | def gen_merge_proposal_nags(lp, policy, project): | ||
402 | 181 | # XXX: cannnot use utcnow as launchpad returns tz-aware objects here | ||
403 | 182 | now = datetime.datetime.now(tz=UTC) | ||
404 | 183 | logging.debug("Looking for merge proposals in project %r", project) | ||
405 | 184 | # NOTE: Workaround for project.getMergeProposals() crashing on timeouts | ||
406 | 185 | try: | ||
407 | 186 | merge_proposals = project.getMergeProposals(status='Needs review') | ||
408 | 187 | except AttributeError: | ||
409 | 188 | logging.warning("The project %s has no code to look at", project) | ||
410 | 189 | return | ||
411 | 190 | for proposal in merge_proposals: | ||
412 | 191 | logging.debug("Looking at merge proposal %r", proposal) | ||
413 | 192 | # Skip everything that is still not requested for review | ||
414 | 193 | if proposal.date_review_requested is None: | ||
415 | 194 | continue | ||
416 | 195 | # Skip everything that is already merged | ||
417 | 196 | if proposal.date_merged is not None: | ||
418 | 197 | continue | ||
419 | 198 | votes = Votes(proposal.votes) | ||
420 | 199 | |||
421 | 200 | # Nag about missing commit message on merge requests | ||
422 | 201 | if policy.mps_need_commit_message and proposal.commit_message is None: | ||
423 | 202 | yield Nag( | ||
424 | 203 | person=proposal.registrant.display_name, | ||
425 | 204 | action="set a commit message on merge request", | ||
426 | 205 | subject=proposal.web_link, | ||
427 | 206 | sort_class=SORT_CLASS_MERGE, | ||
428 | 207 | sort_priority=None, # TODO: get from max(linked bugs) | ||
429 | 208 | sort_age=(proposal.date_review_requested - now).days, | ||
430 | 209 | project_name=project.name, | ||
431 | 210 | votes=votes, | ||
432 | 211 | ) | ||
433 | 212 | |||
434 | 213 | age = (now - proposal.date_review_requested).days | ||
435 | 214 | |||
436 | 215 | if (votes.approve == votes.total(for_approval=True) and | ||
437 | 216 | (votes.approve >= policy.approvals_to_bypass or | ||
438 | 217 | votes.approve >= policy.min_approve and | ||
439 | 218 | age >= policy.days_before_merge)): | ||
440 | 219 | yield Nag( | ||
441 | 220 | person='A committer', | ||
442 | 221 | action="consider to merge the proposal", | ||
443 | 222 | subject=proposal.web_link, | ||
444 | 223 | sort_class=SORT_CLASS_MERGE, | ||
445 | 224 | sort_priority=None, # TODO: get from max(linked bugs) | ||
446 | 225 | sort_age=(proposal.date_review_requested - now).days, | ||
447 | 226 | project_name=project.name, | ||
448 | 227 | votes=votes, | ||
449 | 228 | ) | ||
450 | 229 | continue | ||
451 | 230 | |||
452 | 231 | # Nag about aging merge requests | ||
453 | 232 | if age >= policy.max_review_age: | ||
454 | 233 | yield Nag( | ||
455 | 234 | person='Someone', | ||
456 | 235 | action="review the merge request", | ||
457 | 236 | subject=proposal.web_link, | ||
458 | 237 | sort_class=SORT_CLASS_MERGE, | ||
459 | 238 | sort_priority=None, # TODO: get from max(linked bugs) | ||
460 | 239 | sort_age=(proposal.date_review_requested - now).days, | ||
461 | 240 | project_name=project.name, | ||
462 | 241 | votes=votes, | ||
463 | 242 | ) | ||
464 | 243 | |||
465 | 244 | |||
466 | 245 | def gen_bug_nags(lp, policy, project): | ||
467 | 246 | now = datetime.datetime.now(tz=UTC) | ||
468 | 247 | new_threshold = now - datetime.timedelta(days=policy.max_bug_new_age) | ||
469 | 248 | # Nag about bugs that are "New" for too long | ||
470 | 249 | logging.debug("Looking at 'new' bugs that are 8 days or older") | ||
471 | 250 | aging_new_bugs = project.searchTasks(status='New', | ||
472 | 251 | created_before=new_threshold) | ||
473 | 252 | for bug_task in aging_new_bugs: | ||
474 | 253 | yield Nag( | ||
475 | 254 | person='everyone', | ||
476 | 255 | action='triage the bug', | ||
477 | 256 | subject=bug_task.web_link, | ||
478 | 257 | sort_class=SORT_CLASS_BUG, | ||
479 | 258 | sort_priority=None, # TODO: convert from importance name | ||
480 | 259 | sort_age=None, | ||
481 | 260 | votes=None) | ||
482 | 261 | |||
483 | 262 | |||
484 | 263 | def parse_projects_file(filename): | ||
485 | 264 | """ Parse a file containing name of the projects | ||
486 | 265 | A line per project | ||
487 | 266 | """ | ||
488 | 267 | with open(filename, mode='rU') as project_file: | ||
489 | 268 | content = project_file.read() | ||
490 | 269 | |||
491 | 270 | return [name.strip() for name in content.splitlines()] | ||
492 | 271 | |||
493 | 272 | |||
494 | 273 | def main(): | 55 | def main(): |
495 | 274 | parser = argparse.ArgumentParser() | 56 | parser = argparse.ArgumentParser() |
496 | 275 | group = parser.add_argument_group(title="debugging") | 57 | group = parser.add_argument_group(title="debugging") |
497 | @@ -304,10 +86,7 @@ | |||
498 | 304 | if args.debug: | 86 | if args.debug: |
499 | 305 | logging.basicConfig(level=logging.DEBUG) | 87 | logging.basicConfig(level=logging.DEBUG) |
500 | 306 | # Access Lauchpad object as the current (system) user or anonymously | 88 | # Access Lauchpad object as the current (system) user or anonymously |
505 | 307 | if args.anonymous: | 89 | lp = launchpad_login(args.anonymous, consumer_name, args.service_root) |
502 | 308 | lp = Launchpad.login_anonymously(consumer_name, args.service_root) | ||
503 | 309 | else: | ||
504 | 310 | lp = Launchpad.login_with(consumer_name, args.service_root) | ||
506 | 311 | 90 | ||
507 | 312 | if not args.project and not args.projects_file: | 91 | if not args.project and not args.projects_file: |
508 | 313 | parser.print_usage() | 92 | parser.print_usage() |
Sorry I didn't saw your proposal before (even if I use openerp-nag...) whereas it deserves some attention.
Thanks for your work so far.
Seems good to me even if some changes seems not really necessary to me, surely you had some reasons.
What is the rationale to change the Nag namedtuple to a class? A reason could be that you added methods, but not. On that topic, couldn't we move the big print of the end to a method on Nag() (it seems that it could even be the __str__).
Couldn't already the module be used as a lib? (as there is if __name__ == "__main__":) If the filename is an issue (it is, i we want to import it), maybe we can consider to rename it? Not that I strongly disagree with the split but it seem a bit overkill to me here. But maybe do you have great plans to extend this tool and so it makes more sense ;-)
I strongly disagree to remove the --authenticated option: the Launchpad API is limited with anonymous users and it is used now for the votes (the votes are only visible by authenticated users). BTW you may want to rebase on the master since votes have been integrated meanwhile.
What was the bug with the age?