Completed
Push — master ( 5e7443...88772f )
by Michael
25:54 queued 15:53
created

load_settings()   D

Complexity

Conditions 8

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 8

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 8
c 2
b 0
f 0
dl 0
loc 38
ccs 20
cts 20
cp 1
crap 8
rs 4
1 3
import textwrap
2 3
from collections import OrderedDict
3
from os.path import exists, expanduser, expandvars, join, curdir
4 3
import io
5 3
import os
6 3
import sys
7
8 3
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...
9 3
from pathlib import Path
10 3
11
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...
12 3
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...
13 3
14
import changes
15
from changes.models import GitRepository
16
from .commands import info, note
17
18 3
19
AUTH_TOKEN_ENVVAR = 'GITHUB_AUTH_TOKEN'
20
21
# via https://github.com/jakubroztocil/httpie/blob/6bdfc7a/httpie/config.py#L9
22 3
IS_WINDOWS = 'win32' in str(sys.platform).lower()
23
DEFAULT_CONFIG_FILE = str(os.environ.get(
24
    'CHANGES_CONFIG_FILE',
25
    expanduser('~/.changes') if not IS_WINDOWS else
26
    expandvars(r'%APPDATA%\\.changes')
27
))
28
29 3
PROJECT_CONFIG_FILE = '.changes.toml'
30 3
DEFAULT_RELEASES_DIRECTORY = 'docs/releases'
31 3
32 3
33 3
@attr.s
34 3
class Changes(object):
35
    auth_token = attr.ib()
36 3
37
38 3
def load_settings():
39
    tool_config_path = Path(str(os.environ.get(
40 3
        'CHANGES_CONFIG_FILE',
41 3
        expanduser('~/.changes') if not IS_WINDOWS else
42 3
        expandvars(r'%APPDATA%\\.changes')
43 3
    )))
44 3
45
    tool_settings = None
46
    if tool_config_path.exists():
47
        tool_settings = Changes(
48
            **(toml.load(tool_config_path.open())['changes'])
49 3
        )
50
51
    if not (tool_settings and tool_settings.auth_token):
52 3
        # prompt for auth token
53
        auth_token = os.environ.get(AUTH_TOKEN_ENVVAR)
54 3
        if auth_token:
55
            info('Found Github Auth Token in the environment')
56 3
57
        while not auth_token:
58 3
            info('No auth token found, asking for it')
59 3
            # to interact with the Git*H*ub API
60 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...
61
            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...
62 3
                        'token" page, to create a token for changes.')
63
            click.launch('https://github.com/settings/tokens/new')
64
            auth_token = click.prompt('Enter your changes token')
65 3
66 3
        if not tool_settings:
67
            tool_settings = Changes(auth_token=auth_token)
68
69 3
        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...
70 3
            toml.dumps({
71
                'changes': attr.asdict(tool_settings)
72 3
            })
73 3
        )
74
75
    return tool_settings
76
77
78
@attr.s
79
class Project(object):
80
    releases_directory = attr.ib()
81
    repository = attr.ib(default=None)
82
    bumpversion = attr.ib(default=None)
83
    labels = attr.ib(default=attr.Factory(dict))
84
85
    @property
86
    def bumpversion_configured(self):
87
        return isinstance(self.bumpversion, BumpVersion)
88
89
    @property
90
    def labels_selected(self):
91
        return len(self.labels) > 0
92
93
94
@attr.s
95
class BumpVersion(object):
96
    DRAFT_OPTIONS = [
97
        '--dry-run', '--verbose',
98
        '--no-commit', '--no-tag',
99
        '--allow-dirty',
100
    ]
101
    STAGE_OPTIONS = [
102
        '--verbose',
103
        '--no-commit', '--no-tag',
104
    ]
105
106
    current_version = attr.ib()
107
    version_files_to_replace = attr.ib(default=attr.Factory(list))
108
109
    def write_to_file(self, config_path: Path):
110
        bumpversion_cfg = textwrap.dedent(
111
            """\
112
            [bumpversion]
113
            current_version = {current_version}
114
115
            """
116
        ).format(**attr.asdict(self))
117
118
        bumpversion_files = '\n\n'.join([
119
            '[bumpversion:file:{}]'.format(file_name)
120
            for file_name in self.version_files_to_replace
121
        ])
122
123
        config_path.write_text(
124
            bumpversion_cfg + bumpversion_files
125
        )
126
127
128
def load_project_settings():
129
    project_settings = configure_changes()
130
131
    info('Indexing repository')
132
    project_settings.repository = GitRepository(
133
        auth_token=changes.settings.auth_token
134
    )
135
136
    project_settings.bumpversion = configure_bumpversion(project_settings)
137
138
    project_settings.labels = configure_labels(project_settings)
139
140
    return project_settings
141
142
143
def configure_labels(project_settings):
144
    if project_settings.labels_selected:
145
        return project_settings.labels
146
147
    github_labels = project_settings.repository.github_labels
148
149
    # since there are no labels defined
150
    # let's ask which github tags they want to track
151
    # TODO: streamlined support for github defaults: enhancement, bug
152
    changelog_worthy_labels = read_user_choices(
153
        'labels',
154
        [
155
            properties['name']
156
            for label, properties in github_labels.items()
157
        ]
158
    )
159
160
    # TODO: if not project_settings.labels_have_descriptions:
161
    described_labels = {}
162
    # auto-generate label descriptions
163
    for label_name in changelog_worthy_labels:
164
        label_properties = github_labels[label_name]
165
        # Auto-generate description as titlecase label name
166
        label_properties['description'] = label_name.title()
167
        described_labels[label_name] = label_properties
168
169
    return described_labels
170
171
172
def configure_changes():
173
    changes_project_config_path = Path(PROJECT_CONFIG_FILE)
174
    project_settings = None
175
    if changes_project_config_path.exists():
176
        project_settings = Project(
177
            **(toml.load(changes_project_config_path.open())['changes'])
178
        )
179
    if not project_settings:
180
        project_settings = Project(
181
            releases_directory=str(Path(click.prompt(
182
                'Enter the directory to store your releases notes',
183
                DEFAULT_RELEASES_DIRECTORY,
184
                type=click.Path(exists=True, dir_okay=True)
185
            )))
186
        )
187
        # write config file
188
        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...
189
            toml.dumps({
190
                'changes': attr.asdict(project_settings)
191
            })
192
        )
193
194
    return project_settings
195
196
197
def configure_bumpversion(project_settings):
198
    # TODO: look in other supported bumpversion config locations
199
    bumpversion = None
200
    bumpversion_config_path = Path('.bumpversion.cfg')
201
    if not bumpversion_config_path.exists():
202
        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...
203
204
        version_file_path = None
205
        while not version_file_path == Path('.'):
206
            version_file_path = Path(click.prompt(
207
                'Enter a path to a file that contains a version number '
208
                "(enter a path of '.' when you're done selecting files)",
209
                type=click.Path(
210
                    exists=True,
211
                    dir_okay=True,
212
                    file_okay=True,
213
                    readable=True
214
                )
215
            ))
216
217
            if version_file_path != Path('.'):
218
                user_supplied_versioned_file_paths.append(version_file_path)
219
220
        bumpversion = BumpVersion(
221
            current_version=project_settings.repository.latest_version,
222
            version_files_to_replace=user_supplied_versioned_file_paths,
223
        )
224
        bumpversion.write_to_file(bumpversion_config_path)
225
    else:
226
        raise NotImplemented('')
0 ignored issues
show
Best Practice introduced by
NotImplemented raised - should raise NotImplementedError
Loading history...
227
228
    return bumpversion
229
230
231
def read_user_choices(var_name, options):
232
    """Prompt the user to choose from several options for the given variable.
233
234
    # cookiecutter/cookiecutter/prompt.py
235
    The first item will be returned if no input happens.
236
237
    :param str var_name: Variable as specified in the context
238
    :param list options: Sequence of options that are available to select from
239
    :return: Exactly one item of ``options`` that has been chosen by the user
240
    """
241
    raise NotImplementedError()
242
    #
243
244
    # Please see http://click.pocoo.org/4/api/#click.prompt
245
    if not isinstance(options, list):
246
        raise TypeError
247
248
    if not options:
249
        raise ValueError
250
251
    choice_map = OrderedDict(
252
        (u'{}'.format(i), value) for i, value in enumerate(options, 1)
253
    )
254
    choices = choice_map.keys()
255
    default = u'1'
256
257
    choice_lines = [u'{} - {}'.format(*c) for c in choice_map.items()]
258
    prompt = u'\n'.join((
259
        u'Select {}:'.format(var_name),
260
        u'\n'.join(choice_lines),
261
        u'Choose from {}'.format(u', '.join(choices))
262
    ))
263
264
    # TODO: multi-select
265
    user_choice = click.prompt(
266
        prompt, type=click.Choice(choices), default=default
267
    )
268
    return choice_map[user_choice]
269
270
DEFAULTS = {
271
    'changelog': 'CHANGELOG.md',
272
    'readme': 'README.md',
273
    'github_auth_token': None,
274
}
275
276
277
class Config:
278
    test_command = None
279
    pypi = None
280
    skip_changelog = None
281
    changelog_content = None
282
    repo = None
283
284
    def __init__(self, module_name, dry_run, debug, no_input, requirements,
285
                 new_version, current_version, repo_url, version_prefix):
286
        self.module_name = module_name
287
        # module_name => project_name => curdir
288
        self.dry_run = dry_run
289
        self.debug = debug
290
        self.no_input = no_input
291
        self.requirements = requirements
292
        self.new_version = (
293
            version_prefix + new_version
294
            if version_prefix
295
            else new_version
296
        )
297
        self.current_version = current_version
298
299
300
def project_config():
301
    """Deprecated"""
302
    project_name = curdir
303
304
    config_path = Path(join(project_name, PROJECT_CONFIG_FILE))
305
306
    if not exists(config_path):
307
        store_settings(DEFAULTS.copy())
308
        return DEFAULTS
309
310
    return toml.load(io.open(config_path)) or {}
311
312
313
def store_settings(settings):
314
    pass
315
316