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
1212import sys
1313
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
2626ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
2727PER_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
3375def 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
3679def _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
4384def 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
5089def 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
89138def 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
94143def 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
109163if __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