Completed
Push — stage ( b436a1...8ef157 )
by Michael
05:59
created

Project.labels_have_descriptions()   A

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
dl 0
loc 5
rs 9.4285
ccs 0
cts 0
cp 0
crap 6
1 3
import textwrap
2 3
from os.path import exists, expanduser, expandvars, join, curdir
3
import io
4 3
import os
5 3
import sys
6 3
7
import click
0 ignored issues
show
Configuration introduced by
The import click could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
8 3
from pathlib import Path
9 3
10 3
import toml
0 ignored issues
show
Configuration introduced by
The import toml could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
11
import attr
0 ignored issues
show
Configuration introduced by
The import attr could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
12 3
13 3
import changes
14
from changes.models import GitRepository
15
from .commands import info, note
16
17
18 3
AUTH_TOKEN_ENVVAR = 'GITHUB_AUTH_TOKEN'
19
20
# via https://github.com/jakubroztocil/httpie/blob/6bdfc7a/httpie/config.py#L9
21
IS_WINDOWS = 'win32' in str(sys.platform).lower()
22 3
DEFAULT_CONFIG_FILE = str(os.environ.get(
23
    'CHANGES_CONFIG_FILE',
24
    expanduser('~/.changes') if not IS_WINDOWS else
25
    expandvars(r'%APPDATA%\\.changes')
26
))
27
28
PROJECT_CONFIG_FILE = '.changes.toml'
29 3
DEFAULT_RELEASES_DIRECTORY = 'docs/releases'
30 3
31 3
32 3
@attr.s
33 3
class Changes(object):
34 3
    auth_token = attr.ib()
35
36 3
37
def load_settings():
38 3
    tool_config_path = Path(str(os.environ.get(
39
        'CHANGES_CONFIG_FILE',
40 3
        expanduser('~/.changes') if not IS_WINDOWS else
41 3
        expandvars(r'%APPDATA%\\.changes')
42 3
    )))
43 3
44 3
    tool_settings = None
45
    if tool_config_path.exists():
46
        tool_settings = Changes(
47
            **(toml.load(tool_config_path.open())['changes'])
48
        )
49 3
50
    if not (tool_settings and tool_settings.auth_token):
51
        # prompt for auth token
52 3
        auth_token = os.environ.get(AUTH_TOKEN_ENVVAR)
53
        if auth_token:
54 3
            info('Found Github Auth Token in the environment')
55
56 3
        while not auth_token:
57
            info('No auth token found, asking for it')
58 3
            # to interact with the Git*H*ub API
59 3
            note('You need a Github Auth Token for changes to create a release.')
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (81/79).

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

Loading history...
60 3
            click.pause('Press [enter] to launch the GitHub "New personal access '
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (82/79).

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

Loading history...
61
                        'token" page, to create a token for changes.')
62 3
            click.launch('https://github.com/settings/tokens/new')
63
            auth_token = click.prompt('Enter your changes token')
64
65 3
        if not tool_settings:
66 3
            tool_settings = Changes(auth_token=auth_token)
67
68
        tool_config_path.write_text(
0 ignored issues
show
Bug introduced by
The Instance of Path does not seem to have a member named write_text.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
69 3
            toml.dumps({
70 3
                'changes': attr.asdict(tool_settings)
71
            })
72 3
        )
73 3
74
    return tool_settings
75
76
77
@attr.s
78
class Project(object):
79
    releases_directory = attr.ib()
80
    repository = attr.ib(default=None)
81
    bumpversion = attr.ib(default=None)
82
    labels = attr.ib(default=attr.Factory(dict))
83
84
    @property
85
    def bumpversion_configured(self):
86
        return isinstance(self.bumpversion, BumpVersion)
87
88
    @property
89
    def labels_selected(self):
90
        return len(self.labels) > 0
91
92
    @property
93
    def labels_have_descriptions(self):
94
        return all([
95
            'description' in properties and len(properties['description']) > 0
96
            for label, properties in self.labels.items()
97
        ])
98
99
100
@attr.s
101
class BumpVersion(object):
102
    DRAFT_OPTIONS = [
103
        '--dry-run', '--verbose',
104
        '--no-commit', '--no-tag',
105
        '--allow-dirty',
106
    ]
107
    STAGE_OPTIONS = [
108
        '--verbose',
109
        '--no-commit', '--no-tag',
110
    ]
111
112
    current_version = attr.ib()
113
    version_files_to_replace = attr.ib(default=attr.Factory(list))
114
115
    def write_to_file(self, config_path: Path):
116
        bumpversion_cfg = textwrap.dedent(
117
            """\
118
            [bumpversion]
119
            current_version = {current_version}
120
121
            """
122
        ).format(**attr.asdict(self))
123
124
        bumpversion_files = '\n\n'.join([
125
            '[bumpversion:file:{}]'.format(file_name)
126
            for file_name in self.version_files_to_replace
127
        ])
128
129
        config_path.write_text(
130
            bumpversion_cfg + bumpversion_files
131
        )
132
133
def load_project_settings():
134
    changes_project_config_path = Path(PROJECT_CONFIG_FILE)
135
136
    project_settings = None
137
    if changes_project_config_path.exists():
138
        project_settings = Project(
139
            **(toml.load(changes_project_config_path.open())['changes'])
140
        )
141
142
    if not project_settings:
143
        project_settings = Project(
144
            releases_directory=str(Path(click.prompt(
145
                'Enter the directory to store your releases notes',
146
                DEFAULT_RELEASES_DIRECTORY,
147
                type=click.Path(exists=True, dir_okay=True)
148
            )))
149
        )
150
        # write config file
151
        changes_project_config_path.write_text(
0 ignored issues
show
Bug introduced by
The Instance of Path does not seem to have a member named write_text.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
152
            toml.dumps({
153
                'changes': attr.asdict(project_settings)
154
            })
155
        )
156
157
    # Initialise environment
158
    info('Indexing repository')
159
    project_settings.repository = GitRepository(
160
        auth_token=changes.settings.auth_token
161
    )
162
163
    project_settings.bumpversion = configure_bumpversion(project_settings)
164
165
    if not project_settings.labels_selected:
166
        # defaults: enhancement, bug
167
        project_settings.changelog_worthy_labels = read_user_choices(
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'read_user_choices'
Loading history...
168
            'labels',
169
            [
170
                properties['name']
171
                for label, properties in project_settings.labels.items()
172
            ]
173
        )
174
175
    # auto generate descriptions, note: to customise .changes.toml
176
    if not project_settings.labels_have_descriptions:
177
        described_labels = {}
178
        for label in project_settings.changelog_worthy_labels:
179
            label_properties = project_settings.labels[label]
180
            label_properties['description'] = label.title()
181
            described_labels[label] = label_properties
182
        # FIXME: for label, properties in project_settings.???
183
        # FIXME: name from the context of the consumer in `stage`
184
        project_settings.xx = described_labels
0 ignored issues
show
Coding Style Naming introduced by
The name xx does not conform to the attribute naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
185
186
    return project_settings
187
188
189
def configure_bumpversion(project_settings):
190
    # TODO: look in other supported bumpversion config locations
191
    bumpversion = None
192
    bumpversion_config_path = Path('.bumpversion.cfg')
193
    if not bumpversion_config_path.exists():
194
        user_supplied_versioned_file_paths = []
0 ignored issues
show
Coding Style Naming introduced by
The name user_supplied_versioned_file_paths does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
195
196
        version_file_path = None
197
        while not version_file_path == Path('.'):
198
            version_file_path = Path(click.prompt(
199
                'Enter a path to a file that contains a version number '
200
                "(enter a path of '.' when you're done selecting files)",
201
                type=click.Path(
202
                    exists=True,
203
                    dir_okay=True,
204
                    file_okay=True,
205
                    readable=True
206
                )
207
            ))
208
209
            if version_file_path != Path('.'):
210
                user_supplied_versioned_file_paths.append(version_file_path)
211
212
        bumpversion = BumpVersion(
213
            current_version=project_settings.repository.latest_version,
214
            version_files_to_replace=user_supplied_versioned_file_paths,
215
        )
216
        bumpversion.write_to_file(bumpversion_config_path)
217
    else:
218
        raise NotImplemented('')
0 ignored issues
show
Best Practice introduced by
NotImplemented raised - should raise NotImplementedError
Loading history...
219
220
    return bumpversion
221
222
223
224
DEFAULTS = {
225
    'changelog': 'CHANGELOG.md',
226
    'readme': 'README.md',
227
    'github_auth_token': None,
228
}
229
230
231
class Config:
232
    test_command = None
233
    pypi = None
234
    skip_changelog = None
235
    changelog_content = None
236
    repo = None
237
238
    def __init__(self, module_name, dry_run, debug, no_input, requirements,
239
                 new_version, current_version, repo_url, version_prefix):
240
        self.module_name = module_name
241
        # module_name => project_name => curdir
242
        self.dry_run = dry_run
243
        self.debug = debug
244
        self.no_input = no_input
245
        self.requirements = requirements
246
        self.new_version = (
247
            version_prefix + new_version
248
            if version_prefix
249
            else new_version
250
        )
251
        self.current_version = current_version
252
253
254
def project_config():
255
    """Deprecated"""
256
    project_name = curdir
257
258
    config_path = Path(join(project_name, PROJECT_CONFIG_FILE))
259
260
    if not exists(config_path):
261
        store_settings(DEFAULTS.copy())
262
        return DEFAULTS
263
264
    return toml.load(io.open(config_path)) or {}
265
266
267
def store_settings(settings):
268
    pass
269
270