Completed
Pull Request — master (#43)
by Gonzalo
50s
created

format_changelog()   D

Complexity

Conditions 10

Size

Total Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

Changes 14
Bugs 2 Features 0
Metric Value
cc 10
c 14
b 2
f 0
dl 0
loc 59
rs 4.8648

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like format_changelog() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
# -----------------------------------------------------------------------------
3
# Copyright (c) The Spyder Development Team
4
#
5
# Licensed under the terms of the MIT License
6
# (See LICENSE.txt for details)
7
# -----------------------------------------------------------------------------
8
"""Build a list of issues and pull requests per Github milestone."""
9
10
from __future__ import print_function
11
12
# Standard library imports
13
import argparse
14
import getpass
15
import re
16
import sys
17
import time
18
19
# Third party imports
20
from jinja2 import Template
21
22
# Local imports
23
from loghub.repo import GitHubRepo
24
from loghub.templates import (CHANGELOG_GROUPS_TEMPLATE_PATH,
25
                              CHANGELOG_TEMPLATE_PATH,
26
                              RELEASE_GROUPS_TEMPLATE_PATH,
27
                              RELEASE_TEMPLATE_PATH)
28
29
PY2 = sys.version[0] == '2'
30
31
32
def main():
33
    """Main script."""
34
    parse_arguments(skip=False)
35
36
37
def parse_arguments(skip=False):
38
    """Parse CLI arguments."""
39
    # Cli options
40
    parser = argparse.ArgumentParser(
41
        description='Script to print the list of issues and pull requests '
42
        'closed in a given milestone')
43
    parser.add_argument(
44
        'repository',
45
        help="Repository name to generate the Changelog for, in the form "
46
        "user/repo or org/repo (e.g. spyder-ide/spyder)")
47
    parser.add_argument(
48
        '-m',
49
        '--milestone',
50
        action="store",
51
        dest="milestone",
52
        default='',
53
        help="Github milestone to get issues and pull requests for")
54
    parser.add_argument(
55
        '-ilg',
56
        '--issue-label-group',
57
        action="append",
58
        nargs='+',
59
        dest="issue_label_groups",
60
        help="Groups the generated issues by the specified label. This option"
61
        "Takes 1 or 2 arguments, where the first one is the label to "
62
        "match and the second one is the label to print on the final"
63
        "output")
64
    parser.add_argument(
65
        '-ilr',
66
        '--issue-label-regex',
67
        action="store",
68
        dest="issue_label_regex",
69
        default='',
70
        help="Label issue filter using a regular expression filter")
71
    parser.add_argument(
72
        '-plr',
73
        '--pr-label-regex',
74
        action="store",
75
        dest="pr_label_regex",
76
        default='',
77
        help="Label pull requets filter using a regular expression filter")
78
    parser.add_argument(
79
        '-st',
80
        '--since-tag',
81
        action="store",
82
        dest="since_tag",
83
        default='',
84
        help="Github issues and pull requests since tag")
85
    parser.add_argument(
86
        '-ut',
87
        '--until-tag',
88
        action="store",
89
        dest="until_tag",
90
        default='',
91
        help="Github issues and pull requests until tag")
92
    parser.add_argument(
93
        '-b',
94
        '--branch',
95
        action="store",
96
        dest="branch",
97
        default='',
98
        help="Github base branch for merged PRs")
99
    parser.add_argument(
100
        '-f',
101
        '--format',
102
        action="store",
103
        dest="output_format",
104
        default='changelog',
105
        help="Format for print, either 'changelog' (for "
106
        "Changelog.md file) or 'release' (for the Github "
107
        "Releases page). Default is 'changelog'. The "
108
        "'release' option doesn't generate Markdown "
109
        "hyperlinks.")
110
    parser.add_argument(
111
        '--template',
112
        action="store",
113
        dest="template",
114
        default='',
115
        help="Use a custom Jinja2 template file ")
116
    parser.add_argument(
117
        '-u',
118
        '--user',
119
        action="store",
120
        dest="user",
121
        default='',
122
        help="Github user name")
123
    parser.add_argument(
124
        '-p',
125
        '--password',
126
        action="store",
127
        dest="password",
128
        default='',
129
        help="Github user password")
130
    parser.add_argument(
131
        '-t',
132
        '--token',
133
        action="store",
134
        dest="token",
135
        default='',
136
        help="Github access token")
137
    options = parser.parse_args()
138
139
    username = options.user
140
    password = options.password
141
    milestone = options.milestone
142
    issue_label_groups = options.issue_label_groups
143
144
    if username and not password:
145
        password = getpass.getpass()
146
147
    # Check if repo given
148
    if not options.repository:
149
        print('LOGHUB: Please define a repository name to this script. '
150
              'See its help')
151
        sys.exit(1)
152
153
    # Check if milestone or tag given
154
    if not milestone and not options.since_tag:
155
        print('\nLOGHUB: Querying all issues\n')
156
    elif milestone:
157
        print('\nLOGHUB: Querying issues for milestone {0}'
158
              '\n'.format(milestone))
159
160
    new_issue_label_groups = []
161
    if issue_label_groups:
162
        for item in issue_label_groups:
163
            dic = {}
164
            if len(item) == 1:
165
                dic['label'] = item[0]
166
                dic['name'] = item[0]
167
            elif len(item) >= 2:
168
                dic['label'] = item[0]
169
                dic['name'] = item[1]
170
            new_issue_label_groups.append(dic)
171
172
    if not skip:
173
        create_changelog(
174
            repo=options.repository,
175
            username=username,
176
            password=password,
177
            token=options.token,
178
            milestone=milestone,
179
            since_tag=options.since_tag,
180
            until_tag=options.until_tag,
181
            branch=options.branch,
182
            issue_label_regex=options.issue_label_regex,
183
            pr_label_regex=options.pr_label_regex,
184
            output_format=options.output_format,
185
            template_file=options.template,
186
            issue_label_groups=new_issue_label_groups)
187
188
    return options
189
190
191
def create_changelog(repo=None,
192
                     username=None,
193
                     password=None,
194
                     token=None,
195
                     milestone=None,
196
                     since_tag=None,
197
                     until_tag=None,
198
                     branch=None,
199
                     output_format='changelog',
200
                     issue_label_regex='',
201
                     pr_label_regex='',
202
                     template_file=None,
203
                     issue_label_groups=None):
204
    """Create changelog data."""
205
    # Instantiate Github API
206
    gh = GitHubRepo(
207
        username=username,
208
        password=password,
209
        token=token,
210
        repo=repo, )
211
212
    version = until_tag or None
213
    milestone_number = None
214
    closed_at = None
215
    since = None
216
    until = None
217
218
    # Set milestone or from tag
219
    if milestone and not since_tag:
220
        milestone_data = gh.milestone(milestone)
221
        milestone_number = milestone_data['number']
222
        closed_at = milestone_data['closed_at']
223
        version = milestone.replace('v', '')
224
    elif not milestone and since_tag:
225
        since = gh.tag(since_tag)['tagger']['date']
226
        if until_tag:
227
            until = gh.tag(until_tag)['tagger']['date']
228
            closed_at = until
229
230
    # This returns issues and pull requests
231
    issues = gh.issues(
232
        milestone=milestone_number,
233
        state='closed',
234
        since=since,
235
        until=until,
236
        branch=branch, )
237
238
    # Filter by regex if available
239
    filtered_issues, filtered_prs = [], []
240
    issue_pattern = re.compile(issue_label_regex)
241
    pr_pattern = re.compile(pr_label_regex)
242
    for issue in issues:
243
        is_pr = bool(issue.get('pull_request'))
244
        is_issue = not is_pr
245
        labels = ' '.join(issue.get('loghub_label_names'))
246
247
        if is_issue and issue_label_regex:
248
            issue_valid = bool(issue_pattern.search(labels))
249
            if issue_valid:
250
                filtered_issues.append(issue)
251
        elif is_pr and pr_label_regex:
252
            pr_valid = bool(pr_pattern.search(labels))
253
            if pr_valid:
254
                filtered_prs.append(issue)
255
        elif is_issue and not issue_label_regex:
256
            filtered_issues.append(issue)
257
        elif is_pr and not pr_label_regex:
258
            filtered_prs.append(issue)
259
260
    # If issue label grouping, filter issues
261
    new_filtered_issues = []
262
    if issue_label_groups:
263
        for issue in issues:
264
            for label_group_dic in issue_label_groups:
265
                labels = issue.get('loghub_label_names')
266
                label = label_group_dic['label']
267
                if label in labels:
268
                    new_filtered_issues.append(issue)
269
    else:
270
        new_filtered_issues = filtered_issues
271
272
    return format_changelog(
273
        repo,
274
        new_filtered_issues,
275
        filtered_prs,
276
        version,
277
        closed_at=closed_at,
278
        output_format=output_format,
279
        template_file=template_file,
280
        issue_label_groups=issue_label_groups)
281
282
283
def format_changelog(repo,
284
                     issues,
285
                     prs,
286
                     version,
287
                     closed_at=None,
288
                     output_format='changelog',
289
                     output_file='CHANGELOG.temp',
290
                     template_file=None,
291
                     issue_label_groups=None):
292
    """Create changelog data."""
293
    # Header
294
    if version and version[0] == 'v':
295
        version = version.replace('v', '')
296
    else:
297
        version = '<RELEASE_VERSION>'
298
299
    if closed_at:
300
        close_date = closed_at.split('T')[0]
301
    else:
302
        close_date = time.strftime("%Y/%m/%d")
303
304
    if issue_label_groups:
305
        if output_format == 'changelog':
306
            filepath = CHANGELOG_GROUPS_TEMPLATE_PATH
307
        else:
308
            filepath = RELEASE_GROUPS_TEMPLATE_PATH
309
310
    # Load template
311
    if template_file:
312
        filepath = template_file
313
    else:
314
        if output_format == 'changelog':
315
            filepath = CHANGELOG_TEMPLATE_PATH
316
        else:
317
            filepath = RELEASE_TEMPLATE_PATH
318
319
    with open(filepath) as f:
320
        data = f.read()
321
322
    repo_owner, repo_name = repo.split('/')
323
    template = Template(data)
324
    rendered = template.render(
325
        issues=issues,
326
        pull_requests=prs,
327
        version=version,
328
        close_date=close_date,
329
        repo_full_name=repo,
330
        repo_owner=repo_owner,
331
        repo_name=repo_name,
332
        issue_label_groups=issue_label_groups, )
333
334
    print('#' * 79)
335
    print(rendered)
336
    print('#' * 79)
337
338
    with open(output_file, 'w') as f:
339
        f.write(rendered)
340
341
    return rendered
342
343
344
if __name__ == '__main__':  # yapf: disable
345
    main()
346