Skip to content

Commit abd6730

Browse files
committed
Initial version of release notes generator (robotframework#2074)
1 parent 1bd51b9 commit abd6730

File tree

1 file changed

+212
-0
lines changed

1 file changed

+212
-0
lines changed

doc/releasenotes/generate.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
from os.path import abspath, dirname
2+
import sys
3+
import time
4+
5+
from fnmatch import fnmatchcase
6+
7+
try:
8+
from github import Github
9+
except ImportError:
10+
raise ImportError('Required PyGitHub module missing: pip install PyGithub')
11+
12+
sys.path.insert(0, dirname(dirname(dirname(abspath(__file__)))))
13+
from tasks import get_version_from_file, VERSION_RE
14+
15+
16+
class ReleaseNoteGenerator(object):
17+
"""Create release notes template based on issues on GitHub.
18+
19+
Requires PyGithub <https://github.com/jacquev6/PyGithub>.
20+
"""
21+
repository = 'robotframework/robotframework'
22+
23+
def __init__(self, stream=sys.stdout):
24+
self._stream = stream
25+
26+
def generate(self, version=get_version_from_file(), login=None,
27+
password=None):
28+
milestone, preview, preview_number = self._split_version(version)
29+
issues = self._get_issues(milestone, preview, preview_number, login,
30+
password)
31+
self._write_intro(version)
32+
self._write_most_important_enhancements(issues)
33+
self._write_backwards_incompatible_changes(issues)
34+
self._write_deprecated_features(issues)
35+
self._write_acknowledgements(issues)
36+
self._write_issue_table(issues, milestone, preview)
37+
38+
def _split_version(self, version):
39+
match = VERSION_RE.match(version)
40+
if not match:
41+
raise ValueError("Invalid version '{}'.".format(version))
42+
milestone, _, _, _, preview, preview_number = match.groups()
43+
return milestone, preview, preview_number
44+
45+
def _get_issues(self, milestone, preview, preview_number, login=None,
46+
password=None):
47+
repo = Github(login_or_token=login,
48+
password=password).get_repo(self.repository)
49+
milestone = self._get_milestone(repo, milestone)
50+
issues = [Issue(issue) for issue in repo.get_issues(milestone=milestone, state='all')]
51+
preview_matcher = PreviewMatcher(preview, preview_number)
52+
if preview_matcher:
53+
issues = [issue for issue in issues if preview_matcher.matches(issue.labels)]
54+
return sorted(issues)
55+
56+
def _get_milestone(self, repo, milestone):
57+
for m in repo.get_milestones(state='all'):
58+
if m.title == milestone:
59+
return m
60+
raise ValueError("Milestone '{}' not found from repository '{}'!"
61+
.format(milestone, repo.name))
62+
63+
def _write_intro(self, version):
64+
self._write_header("Robot Framework {}".format(version), level=1)
65+
intro = '''
66+
Robot Framework {version} is a new release with **UPDATE** enhancements and bug
67+
fixes. It was released on {date}.
68+
69+
Questions and comments related to the release can be sent to the
70+
`robotframework-users <http://groups.google.com/group/robotframework-users>`_
71+
and possible bugs submitted to the
72+
`issue tracker <https://github.com/robotframework/robotframework/issues>`__.
73+
74+
If you have `pip <http://pip-installer.org>`_ installed, just run
75+
``pip install --update robotframework``. Otherwise see `installation
76+
instructions <../../INSTALL.rst>`_.
77+
'''.strip()
78+
self._write(intro, version=version, underline='=' * len(version),
79+
date=time.strftime("%A %B %d, %Y"))
80+
81+
def _write_most_important_enhancements(self, issues):
82+
self._write_issues_with_label('Most important enhancements',
83+
issues, 'prio-critical', 'prio-high')
84+
85+
def _write_backwards_incompatible_changes(self, issues):
86+
self._write_issues_with_label('Backwards incompatible changes',
87+
issues, 'bwic')
88+
89+
def _write_deprecated_features(self, issues):
90+
self._write_issues_with_label('Deprecated features', issues, 'depr')
91+
92+
def _write_acknowledgements(self, issues):
93+
self._write_header('Acknowledgements')
94+
self._write('**UPDATE** based on AUTHORS.txt.')
95+
96+
def _write_issue_table(self, issues, milestone, preview):
97+
self._write_header('Full list of fixes and enhancements')
98+
self._write('''
99+
.. list-table::
100+
:header-rows: 1
101+
102+
* - ID
103+
- Type
104+
- Priority
105+
- Summary
106+
'''.strip())
107+
prefix1 = ' * - '
108+
prefix2 = ' - '
109+
if preview:
110+
self._write(' - Added')
111+
for issue in issues:
112+
self._write(prefix1 + issue.id)
113+
for item in (issue.type, issue.priority, issue.summary):
114+
self._write(prefix2 + item)
115+
if preview:
116+
self._write(prefix2 + issue.preview)
117+
self._write()
118+
self._write('Altogether {} issues. View on `issue tracker '
119+
'<https://github.com/{}/issues?q=milestone%3A{}>`__.',
120+
len(issues), self.repository, milestone)
121+
122+
def _write_header(self, header, level=2):
123+
if level > 1:
124+
self._write()
125+
underline = {1: '=', 2: '-', 3: '~', 4: "'"}[level] * len(header)
126+
self._write(header)
127+
self._write(underline, newlines=2)
128+
129+
def _write_issues_with_label(self, header, issues, *labels):
130+
issues = [issue for issue in issues
131+
if any(label in issue.labels for label in labels)]
132+
if not issues:
133+
return
134+
self._write_header(header)
135+
self._write('**EXPLAIN** or remove these.', newlines=2)
136+
for issue in issues:
137+
self._write('- {} {}', issue.id, issue.summary, newlines=0)
138+
if issue.preview:
139+
self._write(' ({})', issue.preview)
140+
else:
141+
self._write()
142+
143+
def _write(self, message='', *args, **kwargs):
144+
message += ('\n' * kwargs.pop('newlines', 1))
145+
if args or kwargs:
146+
message = message.format(*args, **kwargs)
147+
self._stream.write(message)
148+
149+
150+
class Issue(object):
151+
PRIORITIES = ['critical', 'high', 'medium', 'low', '']
152+
153+
def __init__(self, issue):
154+
self.id = '#{}'.format(issue.number)
155+
self.summary = issue.title
156+
self.labels = [label.name for label in issue.get_labels()]
157+
self.type = self._get_label('bug', 'enhancement')
158+
self.priority = self._get_priority()
159+
160+
def _get_label(self, *values):
161+
for value in values:
162+
if value in self.labels:
163+
return value
164+
return None
165+
166+
def _get_priority(self):
167+
labels = ['prio-' + p for p in self.PRIORITIES if p]
168+
priority = self._get_label(*labels)
169+
return priority.split('-')[1] if priority else ''
170+
171+
def __cmp__(self, other):
172+
return cmp(self.order, other.order)
173+
174+
@property
175+
def order(self):
176+
return (self.PRIORITIES.index(self.priority),
177+
0 if self.type == 'bug' else 1,
178+
self.id)
179+
180+
@property
181+
def preview(self):
182+
for label in self.labels:
183+
if label.startswith(('alpha ', 'beta ', 'rc ')):
184+
return label
185+
return ''
186+
187+
188+
class PreviewMatcher(object):
189+
190+
def __init__(self, preview, number):
191+
self._patterns = self._get_patterns(preview, number)
192+
193+
def _get_patterns(self, preview, number):
194+
if not preview:
195+
return ()
196+
return {'a': (self._range('alpha', number),),
197+
'b': ('alpha ?', self._range('beta', number)),
198+
'rc': ('alpha ?', 'beta ?', self._range('rc', number))}[preview]
199+
200+
def _range(self, name, number):
201+
return '%s [%s]' % (name, ''.join(str(i+1) for i in range(int(number))))
202+
203+
def matches(self, labels):
204+
return any(fnmatchcase(l, p) for p in self._patterns for l in labels)
205+
206+
def __nonzero__(self):
207+
return bool(self._patterns)
208+
209+
210+
if __name__ == '__main__':
211+
generator = ReleaseNoteGenerator()
212+
generator.generate(*sys.argv[1:])

0 commit comments

Comments
 (0)