Completed
Push — refactor-and-polish ( 1b7ee7...926e59 )
by Michael
10:03
created

Release.generate_notes()   A

Complexity

Conditions 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
c 0
b 0
f 0
dl 0
loc 12
rs 9.2
1
import re
2
import textwrap
3
from configparser import RawConfigParser
4
from enum import Enum
5
from pathlib import Path
6
7
import attr
8
import click
9
10
11
def changes_to_release_type(repository):
12
    pull_requests = repository.pull_requests_since_latest_version
13
14
    labels = set([
15
        label_name
16
        for pull_request in pull_requests
17
        for label_name in pull_request.label_names
18
    ])
19
20
    descriptions = [
21
        '\n'.join([
22
            pull_request.title, pull_request.description
23
        ])
24
        for pull_request in pull_requests
25
    ]
26
27
    return determine_release(
28
        repository.latest_version,
29
        descriptions,
30
        labels
31
    )
32
33
34
def determine_release(latest_version, descriptions, labels):
35
    if 'BREAKING CHANGE' in descriptions:
36
        return 'major', ReleaseType.BREAKING_CHANGE, latest_version.next_major()
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (80/79).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
37
    elif 'enhancement' in labels:
38
        return 'minor', ReleaseType.FEATURE, latest_version.next_minor()
39
    elif 'bug' in labels:
40
        return 'patch', ReleaseType.FIX, latest_version.next_patch()
41
    else:
42
        return None, ReleaseType.NO_CHANGE, latest_version
43
44
45
class ReleaseType(str, Enum):
46
    NO_CHANGE = 'no-changes'
47
    BREAKING_CHANGE = 'breaking'
48
    FEATURE = 'feature'
49
    FIX = 'fix'
50
51
52
@attr.s
53
class Release(object):
54
    release_date = attr.ib()
55
    version = attr.ib()
56
    description = attr.ib(default=attr.Factory(str))
57
    name = attr.ib(default=attr.Factory(str))
58
    notes = attr.ib(default=attr.Factory(dict))
59
60
    @property
61
    def title(self):
62
        return '{version} ({release_date})'.format(
63
            version=self.version,
64
            release_date=self.release_date
65
        ) + (' ' + self.name) if self.name else ''
66
67
    @property
68
    def release_note_filename(self):
69
        return self.version
70
        # print(attr.asdict(self))
71
        # x = '{version}-{release_date}'.format(
72
        #     version=self.version,
73
        #     release_date=self.release_date
74
        # ) + ('-' + self.name) if self.name else ''
75
        # print(x)
76
        # # return 'FOO'
77
        # return x
78
79
    @classmethod
80
    def generate_notes(cls, project_labels, pull_requests_since_latest_version):
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (80/79).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
81
        for label, properties in project_labels.items():
82
            pull_requests_with_label = [
83
                pull_request
84
                for pull_request in pull_requests_since_latest_version
85
                if label in pull_request.label_names
86
            ]
87
88
            project_labels[label]['pull_requests'] = pull_requests_with_label
89
90
        return project_labels
91
92
93
@attr.s
94
class BumpVersion(object):
95
    DRAFT_OPTIONS = [
96
        '--dry-run', '--verbose',
97
        '--no-commit', '--no-tag',
98
        '--allow-dirty',
99
    ]
100
    STAGE_OPTIONS = [
101
        '--verbose', '--allow-dirty',
102
        '--no-commit', '--no-tag',
103
    ]
104
105
    current_version = attr.ib()
106
    version_files_to_replace = attr.ib(default=attr.Factory(list))
107
108
    @classmethod
109
    def load(cls, latest_version):
110
        # TODO: look in other supported bumpversion config locations
111
        bumpversion = None
112
        bumpversion_config_path = Path('.bumpversion.cfg')
113
        if not bumpversion_config_path.exists():
114
            user_supplied_versioned_file_paths = []
115
116
            version_file_path_answer = None
117
            input_terminator = '.'
118
            while not version_file_path_answer == input_terminator:
119
                version_file_path_answer = click.prompt(
120
                    'Enter a path to a file that contains a version number '
121
                    "(enter a path of '.' when you're done selecting files)",
122
                    type=click.Path(
123
                        exists=True,
124
                        dir_okay=True,
125
                        file_okay=True,
126
                        readable=True
127
                    )
128
                )
129
130
                if version_file_path_answer != input_terminator:
131
                    user_supplied_versioned_file_paths.append(version_file_path_answer)
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (87/79).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
132
133
            bumpversion = cls(
134
                current_version=latest_version,
135
                version_files_to_replace=user_supplied_versioned_file_paths,
136
            )
137
            bumpversion.write_to_file(bumpversion_config_path)
138
139
        return bumpversion
140
141
    @classmethod
142
    def read_from_file(cls, config_path: Path):
143
        config = RawConfigParser('')
144
        config.readfp(config_path.open('rt', encoding='utf-8'))
145
146
        current_version = config.get("bumpversion", 'current_version')
147
148
        filenames = []
149
        for section_name in config.sections():
150
151
            section_name_match = re.compile("^bumpversion:(file|part):(.+)").match(section_name)
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (96/79).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
152
153
            if not section_name_match:
154
                continue
155
156
            section_prefix, section_value = section_name_match.groups()
157
158
            if section_prefix == "file":
159
                filenames.append(section_value)
160
161
        return cls(
162
            current_version=current_version,
163
            version_files_to_replace=filenames,
164
        )
165
166
    def write_to_file(self, config_path: Path):
167
        bumpversion_cfg = textwrap.dedent(
168
            """\
169
            [bumpversion]
170
            current_version = {current_version}
171
172
            """
173
        ).format(**attr.asdict(self))
174
175
        bumpversion_files = '\n\n'.join([
176
            '[bumpversion:file:{}]'.format(file_name)
177
            for file_name in self.version_files_to_replace
178
        ])
179
180
        config_path.write_text(
181
            bumpversion_cfg + bumpversion_files
182
        )
183