Skip to content

Commit 0bddc7b

Browse files
QuLogicksunden
authored andcommitted
Clean up GitHub statistics script
This clears up the flake8 exception, and corrects the title and trailing whitespace that I always need to clean up when committing the file.
1 parent 0c8598c commit 0bddc7b

File tree

2 files changed

+137
-105
lines changed

2 files changed

+137
-105
lines changed

.flake8

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ exclude =
3636
doc/tutorials
3737
# External files.
3838
tools/gh_api.py
39-
tools/github_stats.py
4039
.tox
4140
.eggs
4241

tools/github_stats.py

Lines changed: 137 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#!/usr/bin/env python
22
"""Simple tools to query github.com and gather stats about issues.
33
4-
To generate a report for IPython 2.0, run:
4+
To generate a report for Matplotlib 3.0.0, run:
55
6-
python github_stats.py --milestone 2.0 --since-tag rel-1.0.0
6+
python github_stats.py --milestone 3.0.0 --since-tag v2.0.0
77
"""
8-
#-----------------------------------------------------------------------------
8+
# -----------------------------------------------------------------------------
99
# Imports
10-
#-----------------------------------------------------------------------------
10+
# -----------------------------------------------------------------------------
1111

1212
import sys
1313

@@ -19,33 +19,72 @@
1919
get_paged_request, make_auth_header, get_pull_request, is_pull_request,
2020
get_milestone_id, get_issues_list, get_authors,
2121
)
22-
#-----------------------------------------------------------------------------
22+
# -----------------------------------------------------------------------------
2323
# Globals
24-
#-----------------------------------------------------------------------------
24+
# -----------------------------------------------------------------------------
2525

2626
ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
2727
PER_PAGE = 100
2828

29-
#-----------------------------------------------------------------------------
29+
REPORT_TEMPLATE = """\
30+
.. _github-stats:
31+
32+
{title}
33+
{title_underline}
34+
35+
GitHub statistics for {since_day} (tag: {tag}) - {today}
36+
37+
These lists are automatically generated, and may be incomplete or contain duplicates.
38+
39+
We closed {n_issues} issues and merged {n_pulls} pull requests.
40+
{milestone}
41+
The following {nauthors} authors contributed {ncommits} commits.
42+
43+
{unique_authors}
44+
{links}
45+
46+
Previous GitHub statistics
47+
--------------------------
48+
49+
.. toctree::
50+
:maxdepth: 1
51+
:glob:
52+
:reversed:
53+
54+
prev_whats_new/github_stats_*"""
55+
MILESTONE_TEMPLATE = (
56+
'The full list can be seen `on GitHub '
57+
'<https://github.com/{project}/milestone/{milestone_id}?closed=1>`__\n')
58+
LINKS_TEMPLATE = """
59+
GitHub issues and pull requests:
60+
61+
Pull Requests ({n_pulls}):
62+
63+
{pull_request_report}
64+
65+
Issues ({n_issues}):
66+
67+
{issue_report}
68+
"""
69+
70+
# -----------------------------------------------------------------------------
3071
# Functions
31-
#-----------------------------------------------------------------------------
72+
# -----------------------------------------------------------------------------
73+
3274

3375
def round_hour(dt):
34-
return dt.replace(minute=0,second=0,microsecond=0)
76+
return dt.replace(minute=0, second=0, microsecond=0)
77+
3578

3679
def _parse_datetime(s):
3780
"""Parse dates in the format returned by the GitHub API."""
38-
if s:
39-
return datetime.strptime(s, ISO8601)
40-
else:
41-
return datetime.fromtimestamp(0)
81+
return datetime.strptime(s, ISO8601) if s else datetime.fromtimestamp(0)
82+
4283

4384
def issues2dict(issues):
4485
"""Convert a list of issues to a dict, keyed by issue number."""
45-
idict = {}
46-
for i in issues:
47-
idict[i['number']] = i
48-
return idict
86+
return {i['number']: i for i in issues}
87+
4988

5089
def split_pulls(all_issues, project="matplotlib/matplotlib"):
5190
"""Split a list of closed issues into non-PR Issues and Pull Requests."""
@@ -60,9 +99,12 @@ def split_pulls(all_issues, project="matplotlib/matplotlib"):
6099
return issues, pulls
61100

62101

63-
def issues_closed_since(period=timedelta(days=365), project="matplotlib/matplotlib", pulls=False):
64-
"""Get all issues closed since a particular point in time. period
65-
can either be a datetime object, or a timedelta object. In the
102+
def issues_closed_since(period=timedelta(days=365),
103+
project='matplotlib/matplotlib', pulls=False):
104+
"""
105+
Get all issues closed since a particular point in time.
106+
107+
*period* can either be a datetime object, or a timedelta object. In the
66108
latter case, it is used as a time before the present.
67109
"""
68110

@@ -72,60 +114,73 @@ def issues_closed_since(period=timedelta(days=365), project="matplotlib/matplotl
72114
since = round_hour(datetime.utcnow() - period)
73115
else:
74116
since = period
75-
url = "https://api.github.com/repos/%s/%s?state=closed&sort=updated&since=%s&per_page=%i" % (project, which, since.strftime(ISO8601), PER_PAGE)
117+
url = (
118+
f'https://api.github.com/repos/{project}/{which}'
119+
f'?state=closed'
120+
f'&sort=updated'
121+
f'&since={since.strftime(ISO8601)}'
122+
f'&per_page={PER_PAGE}')
76123
allclosed = get_paged_request(url, headers=make_auth_header())
77124

78-
filtered = [ i for i in allclosed if _parse_datetime(i['closed_at']) > since ]
125+
filtered = (i for i in allclosed
126+
if _parse_datetime(i['closed_at']) > since)
79127
if pulls:
80-
filtered = [ i for i in filtered if _parse_datetime(i['merged_at']) > since ]
128+
filtered = (i for i in filtered
129+
if _parse_datetime(i['merged_at']) > since)
81130
# filter out PRs not against main (backports)
82-
filtered = [ i for i in filtered if i['base']['ref'] == 'main' ]
131+
filtered = (i for i in filtered if i['base']['ref'] == 'main')
83132
else:
84-
filtered = [ i for i in filtered if not is_pull_request(i) ]
133+
filtered = (i for i in filtered if not is_pull_request(i))
85134

86-
return filtered
135+
return list(filtered)
87136

88137

89138
def sorted_by_field(issues, field='closed_at', reverse=False):
90139
"""Return a list of issues sorted by closing date date."""
91-
return sorted(issues, key = lambda i:i[field], reverse=reverse)
140+
return sorted(issues, key=lambda i: i[field], reverse=reverse)
92141

93142

94143
def report(issues, show_urls=False):
95144
"""Summary report about a list of issues, printing number and title."""
145+
lines = []
96146
if show_urls:
97147
for i in issues:
98148
role = 'ghpull' if 'merged_at' in i else 'ghissue'
99-
print('* :%s:`%d`: %s' % (role, i['number'],
100-
i['title'].replace('`', '``')))
149+
number = i['number']
150+
title = i['title'].replace('`', '``').strip()
151+
lines.append(f'* :{role}:`{number}`: {title}')
101152
else:
102153
for i in issues:
103-
print('* %d: %s' % (i['number'], i['title'].replace('`', '``')))
154+
number = i['number']
155+
title = i['title'].replace('`', '``').strip()
156+
lines.append('* {number}: {title}')
157+
return '\n'.join(lines)
104158

105-
#-----------------------------------------------------------------------------
159+
# -----------------------------------------------------------------------------
106160
# Main script
107-
#-----------------------------------------------------------------------------
161+
# -----------------------------------------------------------------------------
108162

109163
if __name__ == "__main__":
110164
# Whether to add reST urls for all issues in printout.
111165
show_urls = True
112166

113167
parser = ArgumentParser()
114-
parser.add_argument('--since-tag', type=str,
115-
help="The git tag to use for the starting point (typically the last major release)."
116-
)
117-
parser.add_argument('--milestone', type=str,
118-
help="The GitHub milestone to use for filtering issues [optional]."
119-
)
120-
parser.add_argument('--days', type=int,
121-
help="The number of days of data to summarize (use this or --since-tag)."
122-
)
123-
parser.add_argument('--project', type=str, default="matplotlib/matplotlib",
124-
help="The project to summarize."
125-
)
126-
parser.add_argument('--links', action='store_true', default=False,
127-
help="Include links to all closed Issues and PRs in the output."
128-
)
168+
parser.add_argument(
169+
'--since-tag', type=str,
170+
help='The git tag to use for the starting point '
171+
'(typically the last major release).')
172+
parser.add_argument(
173+
'--milestone', type=str,
174+
help='The GitHub milestone to use for filtering issues [optional].')
175+
parser.add_argument(
176+
'--days', type=int,
177+
help='The number of days of data to summarize (use this or --since-tag).')
178+
parser.add_argument(
179+
'--project', type=str, default='matplotlib/matplotlib',
180+
help='The project to summarize.')
181+
parser.add_argument(
182+
'--links', action='store_true', default=False,
183+
help='Include links to all closed Issues and PRs in the output.')
129184

130185
opts = parser.parse_args()
131186
tag = opts.since_tag
@@ -135,9 +190,10 @@ def report(issues, show_urls=False):
135190
since = datetime.utcnow() - timedelta(days=opts.days)
136191
else:
137192
if not tag:
138-
tag = check_output(['git', 'describe', '--abbrev=0']).strip().decode('utf8')
193+
tag = check_output(['git', 'describe', '--abbrev=0'],
194+
encoding='utf8').strip()
139195
cmd = ['git', 'log', '-1', '--format=%ai', tag]
140-
tagday, tz = check_output(cmd).strip().decode('utf8').rsplit(' ', 1)
196+
tagday, tz = check_output(cmd, encoding='utf8').strip().rsplit(' ', 1)
141197
since = datetime.strptime(tagday, "%Y-%m-%d %H:%M:%S")
142198
h = int(tz[1:3])
143199
m = int(tz[3:])
@@ -152,21 +208,19 @@ def report(issues, show_urls=False):
152208
milestone = opts.milestone
153209
project = opts.project
154210

155-
print("fetching GitHub stats since %s (tag: %s, milestone: %s)" % (since, tag, milestone), file=sys.stderr)
211+
print(f'fetching GitHub stats since {since} (tag: {tag}, milestone: {milestone})',
212+
file=sys.stderr)
156213
if milestone:
157214
milestone_id = get_milestone_id(project=project, milestone=milestone,
158-
auth=True)
159-
issues_and_pulls = get_issues_list(project=project,
160-
milestone=milestone_id,
161-
state='closed',
162-
auth=True,
163-
)
215+
auth=True)
216+
issues_and_pulls = get_issues_list(project=project, milestone=milestone_id,
217+
state='closed', auth=True)
164218
issues, pulls = split_pulls(issues_and_pulls, project=project)
165219
else:
166220
issues = issues_closed_since(since, project=project, pulls=False)
167221
pulls = issues_closed_since(since, project=project, pulls=True)
168222

169-
# For regular reports, it's nice to show them in reverse chronological order
223+
# For regular reports, it's nice to show them in reverse chronological order.
170224
issues = sorted_by_field(issues, reverse=True)
171225
pulls = sorted_by_field(pulls, reverse=True)
172226

@@ -175,71 +229,50 @@ def report(issues, show_urls=False):
175229
since_day = since.strftime("%Y/%m/%d")
176230
today = datetime.today()
177231

178-
# Print summary report we can directly include into release notes.
179-
print('.. _github-stats:')
180-
print()
181-
title = 'GitHub statistics ' + today.strftime('(%b %d, %Y)')
182-
print(title)
183-
print('=' * len(title))
184-
185-
print()
186-
print("GitHub statistics for %s (tag: %s) - %s" % (since_day, tag, today.strftime("%Y/%m/%d"), ))
187-
print()
188-
print("These lists are automatically generated, and may be incomplete or contain duplicates.")
189-
print()
232+
title = (f'GitHub statistics for {milestone.lstrip("v")} '
233+
f'{today.strftime("(%b %d, %Y)")}')
190234

191235
ncommits = 0
192236
all_authors = []
193237
if tag:
194238
# print git info, in addition to GitHub info:
195-
since_tag = tag+'..'
239+
since_tag = f'{tag}..'
196240
cmd = ['git', 'log', '--oneline', since_tag]
197241
ncommits += len(check_output(cmd).splitlines())
198242

199-
author_cmd = ['git', 'log', '--use-mailmap', "--format=* %aN", since_tag]
200-
all_authors.extend(check_output(author_cmd).decode('utf-8', 'replace').splitlines())
243+
author_cmd = ['git', 'log', '--use-mailmap', '--format=* %aN', since_tag]
244+
all_authors.extend(
245+
check_output(author_cmd, encoding='utf-8', errors='replace').splitlines())
201246

202247
pr_authors = []
203248
for pr in pulls:
204249
pr_authors.extend(get_authors(pr))
205250
ncommits = len(pr_authors) + ncommits - len(pulls)
206251
author_cmd = ['git', 'check-mailmap'] + pr_authors
207-
with_email = check_output(author_cmd).decode('utf-8', 'replace').splitlines()
252+
with_email = check_output(author_cmd,
253+
encoding='utf-8', errors='replace').splitlines()
208254
all_authors.extend(['* ' + a.split(' <')[0] for a in with_email])
209255
unique_authors = sorted(set(all_authors), key=lambda s: s.lower())
210256

211-
print("We closed %d issues and merged %d pull requests." % (n_issues, n_pulls))
212257
if milestone:
213-
print("The full list can be seen `on GitHub <https://github.com/%s/milestone/%s?closed=1>`__"
214-
% (project, milestone_id)
215-
)
216-
217-
print()
218-
print("The following %i authors contributed %i commits." % (len(unique_authors), ncommits))
219-
print()
220-
print('\n'.join(unique_authors))
258+
milestone_str = MILESTONE_TEMPLATE.format(project=project,
259+
milestone_id=milestone_id)
260+
else:
261+
milestone_str = ''
221262

222263
if opts.links:
223-
print()
224-
print("GitHub issues and pull requests:")
225-
print()
226-
print('Pull Requests (%d):\n' % n_pulls)
227-
report(pulls, show_urls)
228-
print()
229-
print('Issues (%d):\n' % n_issues)
230-
report(issues, show_urls)
231-
print()
232-
print()
233-
print("""\
234-
Previous GitHub statistics
235-
--------------------------
236-
237-
238-
.. toctree::
239-
:maxdepth: 1
240-
:glob:
241-
:reversed:
242-
243-
prev_whats_new/github_stats_*
264+
links = LINKS_TEMPLATE.format(n_pulls=n_pulls,
265+
pull_request_report=report(pulls, show_urls),
266+
n_issues=n_issues,
267+
issue_report=report(issues, show_urls))
268+
else:
269+
links = ''
244270

245-
""")
271+
# Print summary report we can directly include into release notes.
272+
print(REPORT_TEMPLATE.format(title=title, title_underline='=' * len(title),
273+
since_day=since_day, tag=tag,
274+
today=today.strftime('%Y/%m/%d'),
275+
n_issues=n_issues, n_pulls=n_pulls,
276+
milestone=milestone_str,
277+
nauthors=len(unique_authors), ncommits=ncommits,
278+
unique_authors='\n'.join(unique_authors), links=links))

0 commit comments

Comments
 (0)